From 7c94afacc25f0246644b64383537b31f13cb8416 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:34:19 +0700 Subject: [PATCH] Delete lite.js --- dist/better-xcloud.lite.user.js | 6891 ------------------------------- 1 file changed, 6891 deletions(-) delete mode 100755 dist/better-xcloud.lite.user.js diff --git a/dist/better-xcloud.lite.user.js b/dist/better-xcloud.lite.user.js deleted file mode 100755 index ee19cb6..0000000 --- a/dist/better-xcloud.lite.user.js +++ /dev/null @@ -1,6891 +0,0 @@ -// ==UserScript== -// @name Better xCloud (Lite) -// @namespace https://github.com/redphx -// @version 6.3.0-beta-1 -// @description Improve Xbox Cloud Gaming (xCloud) experience -// @author redphx -// @license MIT -// @match https://www.xbox.com/*/play* -// @match https://www.xbox.com/*/auth/msa?*loggedIn* -// @run-at document-end -// @grant none -// ==/UserScript== -"use strict"; -class BxLogger { -static info = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#008746", tag, ...args); -static warning = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#c1a404", tag, ...args); -static error = (tag, ...args) => BxLogger.log("#c10404", tag, ...args); -static log(color, tag, ...args) { -console.log("%c[BxC]", `color:${color};font-weight:bold;`, tag, "//", ...args); -} -} -window.BxLogger = BxLogger; -/* ADDITIONAL CODE */ -var DEFAULT_FLAGS = { -Debug: !1, -CheckForUpdate: !0, -EnableXcloudLogging: !1, -SafariWorkaround: !0, -ForceNativeMkbTitles: [], -FeatureGates: null, -DeviceInfo: { -deviceType: "unknown" -} -}, BX_FLAGS = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {}); -try { -delete window.BX_FLAGS; -} catch (e) {} -if (!BX_FLAGS.DeviceInfo.userAgent) BX_FLAGS.DeviceInfo.userAgent = window.navigator.userAgent; -BxLogger.info("BxFlags", BX_FLAGS); -var NATIVE_FETCH = window.fetch; -var ALL_PREFS = { -global: [ -"audio.mic.onPlaying", -"audio.volume.booster.enabled", -"block.features", -"block.tracking", -"gameBar.position", -"game.fortnite.forceConsole", -"loadingScreen.gameArt.show", -"loadingScreen.rocket", -"loadingScreen.waitTime.show", -"mkb.enabled", -"mkb.cursor.hideIdle", -"nativeMkb.forcedGames", -"nativeMkb.mode", -"xhome.enabled", -"xhome.video.resolution", -"screenshot.applyFilters", -"server.bypassRestriction", -"server.ipv6.prefer", -"server.region", -"stream.video.codecProfile", -"stream.video.combineAudio", -"stream.video.maxBitrate", -"stream.locale", -"stream.video.resolution", -"touchController.autoOff", -"touchController.opacity.default", -"touchController.mode", -"touchController.style.custom", -"touchController.style.standard", -"ui.controllerFriendly", -"ui.controllerStatus.show", -"ui.feedbackDialog.disabled", -"ui.gameCard.waitTime.show", -"ui.hideSections", -"ui.systemMenu.hideHandle", -"ui.imageQuality", -"ui.layout", -"ui.reduceAnimations", -"ui.hideScrollbar", -"ui.streamMenu.simplify", -"ui.splashVideo.skip", -"version.current", -"version.lastCheck", -"version.latest", -"bx.locale", -"userAgent.profile" -], -stream: [ -"audio.volume", -"controller.pollingRate", -"controller.settings", -"deviceVibration.intensity", -"deviceVibration.mode", -"keyboardShortcuts.preset.inGameId", -"localCoOp.enabled", -"mkb.p1.preset.mappingId", -"mkb.p1.slot", -"mkb.p2.preset.mappingId", -"mkb.p2.slot", -"nativeMkb.scroll.sensitivityX", -"nativeMkb.scroll.sensitivityY", -"stats.colors", -"stats.items", -"stats.opacity.all", -"stats.opacity.background", -"stats.position", -"stats.quickGlance.enabled", -"stats.showWhenPlaying", -"stats.textSize", -"video.brightness", -"video.contrast", -"video.maxFps", -"video.player.type", -"video.position", -"video.player.powerPreference", -"video.processing", -"video.ratio", -"video.saturation", -"video.processing.sharpness" -] -}; -var SMART_TV_UNIQUE_ID = "FC4A1DA2-711C-4E9C-BC7F-047AF8A672EA", CHROMIUM_VERSION = "125.0.0.0"; -if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) { -let match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/); -if (match) CHROMIUM_VERSION = match[1]; -} -class UserAgent { -static STORAGE_KEY = "BetterXcloud.UserAgent"; -static #config; -static #isMobile = null; -static #isSafari = null; -static #isSafariMobile = null; -static #USER_AGENTS = { -"windows-edge": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`, -"macos-safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1", -"smarttv-generic": `${window.navigator.userAgent} Smart-TV`, -"smarttv-tizen": `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`, -"vr-oculus": window.navigator.userAgent + " OculusBrowser VR" -}; -static init() { -if (UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}"), !UserAgent.#config.profile) UserAgent.#config.profile = BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default"; -if (!UserAgent.#config.custom) UserAgent.#config.custom = ""; -UserAgent.spoof(); -} -static updateStorage(profile, custom) { -let config = UserAgent.#config; -if (config.profile = profile, profile === "custom" && typeof custom !== "undefined") config.custom = custom; -window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(config)); -} -static getDefault() { -return window.navigator.orgUserAgent || window.navigator.userAgent; -} -static get(profile) { -let defaultUserAgent = window.navigator.userAgent; -switch (profile) { -case "default": -return defaultUserAgent; -case "custom": -return UserAgent.#config.custom || defaultUserAgent; -default: -return UserAgent.#USER_AGENTS[profile] || defaultUserAgent; -} -} -static isSafari() { -if (this.#isSafari !== null) return this.#isSafari; -let userAgent = UserAgent.getDefault().toLowerCase(), result = userAgent.includes("safari") && !userAgent.includes("chrom"); -return this.#isSafari = result, result; -} -static isSafariMobile() { -if (this.#isSafariMobile !== null) return this.#isSafariMobile; -let userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile"); -return this.#isSafariMobile = result, result; -} -static isMobile() { -if (this.#isMobile !== null) return this.#isMobile; -let userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent); -return this.#isMobile = result, result; -} -static spoof() { -let profile = UserAgent.#config.profile; -if (profile === "default") return; -let newUserAgent = UserAgent.get(profile); -if ("userAgentData" in window.navigator) window.navigator.orgUserAgentData = window.navigator.userAgentData, Object.defineProperty(window.navigator, "userAgentData", {}); -window.navigator.orgUserAgent = window.navigator.userAgent, Object.defineProperty(window.navigator, "userAgent", { -value: newUserAgent -}); -} -} -var SCRIPT_VERSION = "6.3.0-beta-1", SCRIPT_VARIANT = "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 -}; -function deepClone(obj) { -if (!obj) return {}; -if ("structuredClone" in window) return structuredClone(obj); -return JSON.parse(JSON.stringify(obj)); -} -var BxEvent; -((BxEvent) => { -BxEvent.POPSTATE = "bx-popstate", BxEvent.STREAM_SESSION_READY = "bx-stream-session-ready", BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED = "bx-custom-touch-layouts-loaded", BxEvent.TOUCH_LAYOUT_MANAGER_READY = "bx-touch-layout-manager-ready", BxEvent.REMOTE_PLAY_READY = "bx-remote-play-ready", BxEvent.REMOTE_PLAY_FAILED = "bx-remote-play-failed", BxEvent.CAPTURE_SCREENSHOT = "bx-capture-screenshot", BxEvent.POINTER_LOCK_REQUESTED = "bx-pointer-lock-requested", BxEvent.POINTER_LOCK_EXITED = "bx-pointer-lock-exited", BxEvent.NAVIGATION_FOCUS_CHANGED = "bx-nav-focus-changed", BxEvent.XCLOUD_GUIDE_MENU_SHOWN = "bx-xcloud-guide-menu-shown", BxEvent.XCLOUD_POLLING_MODE_CHANGED = "bx-xcloud-polling-mode-changed", BxEvent.XCLOUD_RENDERING_COMPONENT = "bx-xcloud-rendering-component", BxEvent.XCLOUD_ROUTER_HISTORY_READY = "bx-xcloud-router-history-ready"; -function dispatch(target, eventName, data) { -if (!target) return; -if (!eventName) { -alert("BxEvent.dispatch(): eventName is null"); -return; -} -let event = new Event(eventName); -if (data) for (let key in data) -event[key] = data[key]; -target.dispatchEvent(event), AppInterface && AppInterface.onEvent(eventName), BX_FLAGS.Debug && BxLogger.warning("BxEvent", "dispatch", target, eventName, data); -} -BxEvent.dispatch = dispatch; -})(BxEvent ||= {}); -window.BxEvent = BxEvent; -class BxEventBus { -listeners = new Map; -group; -appJsInterfaces; -static Script = new BxEventBus("script", { -"dialog.shown": "onDialogShown", -"dialog.dismissed": "onDialogDismissed" -}); -static Stream = new BxEventBus("stream", { -"state.loading": "onStreamPlaying", -"state.playing": "onStreamPlaying", -"state.stopped": "onStreamStopped" -}); -constructor(group, appJsInterfaces) { -this.group = group, this.appJsInterfaces = appJsInterfaces; -} -on(event, callback) { -if (!this.listeners.has(event)) this.listeners.set(event, new Set); -this.listeners.get(event).add(callback), BX_FLAGS.Debug && BxLogger.warning("EventBus", "on", event, callback); -} -once(event, callback) { -let wrapper = (...args) => { -callback(...args), this.off(event, wrapper); -}; -this.on(event, wrapper); -} -off(event, callback) { -if (BX_FLAGS.Debug && BxLogger.warning("EventBus", "off", event, callback), !callback) { -this.listeners.delete(event); -return; -} -let callbacks = this.listeners.get(event); -if (!callbacks) return; -if (callbacks.delete(callback), callbacks.size === 0) this.listeners.delete(event); -} -offAll() { -this.listeners.clear(); -} -emit(event, payload) { -let callbacks = this.listeners.get(event) || []; -for (let callback of callbacks) -callback(payload); -if (AppInterface) try { -if (event in this.appJsInterfaces) { -let method = this.appJsInterfaces[event]; -AppInterface[method] && AppInterface[method](); -} else AppInterface.onEventBus(this.group + "." + event); -} catch (e) { -console.log(e); -} -BX_FLAGS.Debug && BxLogger.warning("EventBus", "emit", `${this.group}.${event}`, payload); -} -} -window.BxEventBus = BxEventBus; -class GhPagesUtils { -static fetchLatestCommit() { -NATIVE_FETCH("https://api.github.com/repos/redphx/better-xcloud/branches/gh-pages", { -method: "GET", -headers: { -Accept: "application/vnd.github.v3+json" -} -}).then((response) => response.json()).then((data) => { -let latestCommitHash = data.commit.sha; -window.localStorage.setItem("BetterXcloud.GhPages.CommitHash", latestCommitHash); -}).catch((error) => { -BxLogger.error("GhPagesUtils", "Error fetching the latest commit:", error); -}); -} -static getUrl(path) { -if (path[0] === "/") alert('`path` must not starts with "/"'); -let prefix = "https://raw.githubusercontent.com/redphx/better-xcloud", latestCommitHash = window.localStorage.getItem("BetterXcloud.GhPages.CommitHash"); -if (latestCommitHash) return `${prefix}/${latestCommitHash}/${path}`; -else return `${prefix}/refs/heads/gh-pages/${path}`; -} -static getNativeMkbCustomList(update = !1) { -let key = "BetterXcloud.GhPages.ForceNativeMkb"; -update && NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => { -if (json.$schemaVersion === 1) window.localStorage.setItem(key, JSON.stringify(json)), BxEventBus.Script.emit("list.forcedNativeMkb.updated", { -data: json -}); -else window.localStorage.removeItem(key); -}); -let info = JSON.parse(window.localStorage.getItem(key) || "{}"); -if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), {}; -return info.data; -} -static getTouchControlCustomList() { -let key = "BetterXcloud.GhPages.CustomTouchLayouts"; -return NATIVE_FETCH(GhPagesUtils.getUrl("touch-layouts/ids.json")).then((response) => response.json()).then((json) => { -if (Array.isArray(json)) window.localStorage.setItem(key, JSON.stringify(json)); -}), JSON.parse(window.localStorage.getItem(key) || "[]"); -} -static getLocalCoOpList() { -let key = "BetterXcloud.GhPages.LocalCoOp"; -NATIVE_FETCH(GhPagesUtils.getUrl("local-co-op/ids.json")).then((response) => response.json()).then((json) => { -if (json.$schemaVersion === 1) { -window.localStorage.setItem(key, JSON.stringify(json)); -let ids = new Set(Object.keys(json.data)); -BxEventBus.Script.emit("list.localCoOp.updated", { ids }); -} else window.localStorage.removeItem(key), BxEventBus.Script.emit("list.localCoOp.updated", { ids: new Set }); -}); -let info = JSON.parse(window.localStorage.getItem(key) || "{}"); -if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), new Set; -return new Set(Object.keys(info.data || {})); -} -} -var SUPPORTED_LANGUAGES = { -"en-US": "English (US)", -"ca-CA": "Català", -"da-DK": "dansk", -"de-DE": "Deutsch", -"en-ID": "Bahasa Indonesia", -"es-ES": "español (España)", -"fr-FR": "français", -"it-IT": "italiano", -"ja-JP": "日本語", -"ko-KR": "한국어", -"pl-PL": "polski", -"pt-BR": "português (Brasil)", -"ru-RU": "русский", -"th-TH": "ภาษาไทย", -"tr-TR": "Türkçe", -"uk-UA": "українська", -"vi-VN": "Tiếng Việt", -"zh-CN": "中文(简体)", -"zh-TW": "中文(繁體)" -}, Texts = { -achievements: "Achievements", -activate: "Activate", -activated: "Activated", -active: "Active", -advanced: "Advanced", -"all-games": "All games", -"always-off": "Always off", -"always-on": "Always on", -"amd-fidelity-cas": "AMD FidelityFX CAS", -"app-settings": "App settings", -apply: "Apply", -"aspect-ratio": "Aspect ratio", -"aspect-ratio-note": "Don't use with native touch games", -audio: "Audio", -auto: "Auto", -availability: "Availability", -"back-to-home": "Back to home", -"back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?", -"background-opacity": "Background opacity", -battery: "Battery", -"battery-saving": "Battery saving", -"better-xcloud": "Better xCloud", -"bitrate-audio-maximum": "Maximum audio bitrate", -"bitrate-video-maximum": "Maximum video bitrate", -bottom: "Bottom", -"bottom-half": "Bottom half", -"bottom-left": "Bottom-left", -"bottom-right": "Bottom-right", -brazil: "Brazil", -brightness: "Brightness", -"browser-unsupported-feature": "Your browser doesn't support this feature", -"button-xbox": "Xbox button", -"bypass-region-restriction": "Bypass region restriction", -"can-stream-xbox-360-games": "Can stream Xbox 360 games", -cancel: "Cancel", -"cant-stream-xbox-360-games": "Can't stream Xbox 360 games", -center: "Center", -chat: "Chat", -"clarity-boost": "Clarity boost", -"clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON", -clear: "Clear", -"clear-data": "Clear data", -"clear-data-confirm": "Do you want to clear all Better xCloud settings and data?", -"clear-data-success": "Data cleared! Refresh the page to apply the changes.", -clock: "Clock", -close: "Close", -"close-app": "Close app", -"combine-audio-video-streams": "Combine audio & video streams", -"combine-audio-video-streams-summary": "May fix the laggy audio problem", -"conditional-formatting": "Conditional formatting text color", -"confirm-delete-preset": "Do you want to delete this preset?", -"confirm-reload-stream": "Do you want to refresh the stream?", -connected: "Connected", -"console-connect": "Connect", -"continent-asia": "Asia", -"continent-australia": "Australia", -"continent-europe": "Europe", -"continent-north-america": "North America", -"continent-south-america": "South America", -contrast: "Contrast", -controller: "Controller", -"controller-customization": "Controller customization", -"controller-customization-input-latency-note": "May slightly increase input latency", -"controller-friendly-ui": "Controller-friendly UI", -"controller-shortcuts": "Controller shortcuts", -"controller-shortcuts-connect-note": "Connect a controller to use this feature", -"controller-shortcuts-xbox-note": "Button to open the Guide menu", -"controller-vibration": "Controller vibration", -copy: "Copy", -"create-shortcut": "Shortcut", -custom: "Custom", -"deadzone-counterweight": "Deadzone counterweight", -decrease: "Decrease", -default: "Default", -"default-opacity": "Default opacity", -"default-preset-note": "You can't modify default presets. Create a new one to customize it.", -delete: "Delete", -"detect-controller-button": "Detect controller button", -device: "Device", -"device-unsupported-touch": "Your device doesn't have touch support", -"device-vibration": "Device vibration", -"device-vibration-not-using-gamepad": "On when not using gamepad", -disable: "Disable", -"disable-features": "Disable features", -"disable-home-context-menu": "Disable context menu in Home page", -"disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog", -"disable-social-features": "Disable social features", -"disable-xcloud-analytics": "Disable xCloud analytics", -disabled: "Disabled", -disconnected: "Disconnected", -download: "Download", -downloaded: "Downloaded", -edit: "Edit", -"enable-controller-shortcuts": "Enable controller shortcuts", -"enable-local-co-op-support": "Enable local co-op support", -"enable-local-co-op-support-note": "Only works with some games", -"enable-mic-on-startup": "Enable microphone on game launch", -"enable-mkb": "Emulate controller with Mouse & Keyboard", -"enable-quick-glance-mode": 'Enable "Quick Glance" mode', -"enable-remote-play-feature": 'Enable the "Remote Play" feature', -"enable-volume-control": "Enable volume control feature", -enabled: "Enabled", -experimental: "Experimental", -export: "Export", -fast: "Fast", -"force-native-mkb-games": "Force native Mouse & Keyboard for these games", -"fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile', -"fortnite-force-console-version": "Fortnite: force console version", -"friends-followers": "Friends and followers", -"game-bar": "Game Bar", -"getting-consoles-list": "Getting the list of consoles...", -guide: "Guide", -help: "Help", -hide: "Hide", -"hide-idle-cursor": "Hide mouse cursor on idle", -"hide-scrollbar": "Hide web page's scrollbar", -"hide-sections": "Hide sections", -"hide-system-menu-icon": "Hide System menu's icon", -"hide-touch-controller": "Hide touch controller", -"high-performance": "High performance", -"highest-quality": "Highest quality", -"highest-quality-note": "Your device may not be powerful enough to use these settings", -"horizontal-scroll-sensitivity": "Horizontal scroll sensitivity", -"horizontal-sensitivity": "Horizontal sensitivity", -"how-to-fix": "How to fix", -"how-to-improve-app-performance": "How to improve app's performance", -ignore: "Ignore", -"image-quality": "Website's image quality", -import: "Import", -"in-game-controller-customization": "In-game controller customization", -"in-game-controller-shortcuts": "In-game controller shortcuts", -"in-game-keyboard-shortcuts": "In-game keyboard shortcuts", -"in-game-shortcuts": "In-game shortcuts", -increase: "Increase", -"install-android": "Better xCloud app for Android", -invites: "Invites", -japan: "Japan", -jitter: "Jitter", -"keyboard-key": "Keyboard key", -"keyboard-shortcuts": "Keyboard shortcuts", -korea: "Korea", -language: "Language", -large: "Large", -layout: "Layout", -"left-stick": "Left stick", -"left-stick-deadzone": "Left stick deadzone", -"left-trigger-range": "Left trigger range", -"limit-fps": "Limit FPS", -"load-failed-message": "Failed to run Better xCloud", -"loading-screen": "Loading screen", -"local-co-op": "Local co-op", -"lowest-quality": "Lowest quality", -manage: "Manage", -"map-mouse-to": "Map mouse to", -"may-not-work-properly": "May not work properly!", -menu: "Menu", -microphone: "Microphone", -"mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings", -"mkb-click-to-activate": "Click to activate", -"mkb-disclaimer": "This could be viewed as cheating when playing online", -"modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.", -"mouse-and-keyboard": "Mouse & Keyboard", -"mouse-click": "Mouse click", -"mouse-wheel": "Mouse wheel", -muted: "Muted", -name: "Name", -"native-mkb": "Native Mouse & Keyboard", -new: "New", -"new-version-available": [ -e => `Version ${e.version} available`, -e => `Versió ${e.version} disponible`, -, -e => `Version ${e.version} verfügbar`, -e => `Versi ${e.version} tersedia`, -e => `Versión ${e.version} disponible`, -e => `Version ${e.version} disponible`, -e => `Disponibile la versione ${e.version}`, -e => `Ver ${e.version} が利用可能です`, -e => `${e.version} 버전 사용가능`, -e => `Dostępna jest nowa wersja ${e.version}`, -e => `Versão ${e.version} disponível`, -e => `Версия ${e.version} доступна`, -e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`, -e => `${e.version} sayılı yeni sürüm mevcut`, -e => `Доступна версія ${e.version}`, -e => `Đã có phiên bản ${e.version}`, -e => `版本 ${e.version} 可供更新`, -e => `已可更新為 ${e.version} 版` -], -"no-consoles-found": "No consoles found", -"no-controllers-connected": "No controllers connected", -normal: "Normal", -notifications: "Notifications", -off: "Off", -official: "Official", -on: "On", -"only-supports-some-games": "Only supports some games", -opacity: "Opacity", -other: "Other", -playing: "Playing", -playtime: "Playtime", -poland: "Poland", -"polling-rate": "Polling rate", -position: "Position", -"powered-off": "Powered off", -"powered-on": "Powered on", -"prefer-ipv6-server": "Prefer IPv6 server", -"preferred-game-language": "Preferred game's language", -preset: "Preset", -press: "Press", -"press-any-button": "Press any button...", -"press-esc-to-cancel": "Press Esc to cancel", -"press-key-to-toggle-mkb": [ -e => `Press ${e.key} to toggle this feature`, -e => `Premeu ${e.key} per alternar aquesta funció`, -e => `Tryk på ${e.key} for at slå denne funktion til`, -e => `${e.key}: Funktion an-/ausschalten`, -e => `Tekan ${e.key} untuk mengaktifkan fitur ini`, -e => `Pulsa ${e.key} para alternar esta función`, -e => `Appuyez sur ${e.key} pour activer cette fonctionnalité`, -e => `Premi ${e.key} per attivare questa funzionalità`, -e => `${e.key} でこの機能を切替`, -e => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`, -e => `Naciśnij ${e.key} aby przełączyć tę funkcję`, -e => `Pressione ${e.key} para alternar este recurso`, -e => `Нажмите ${e.key} для переключения этой функции`, -e => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`, -e => `Etkinleştirmek için ${e.key} tuşuna basın`, -e => `Натисніть ${e.key} щоб перемкнути цю функцію`, -e => `Nhấn ${e.key} để bật/tắt tính năng này`, -e => `按下 ${e.key} 来切换此功能`, -e => `按下 ${e.key} 來啟用此功能` -], -"press-to-bind": "Press a key or do a mouse click to bind...", -"prompt-preset-name": "Preset's name:", -recommended: "Recommended", -"recommended-settings-for-device": [ -e => `Recommended settings for ${e.device}`, -e => `Configuració recomanada per a ${e.device}`, -, -e => `Empfohlene Einstellungen für ${e.device}`, -e => `Rekomendasi pengaturan untuk ${e.device}`, -e => `Ajustes recomendados para ${e.device}`, -e => `Paramètres recommandés pour ${e.device}`, -e => `Configurazioni consigliate per ${e.device}`, -e => `${e.device} の推奨設定`, -e => `다음 기기에서 권장되는 설정: ${e.device}`, -e => `Zalecane ustawienia dla ${e.device}`, -e => `Configurações recomendadas para ${e.device}`, -e => `Рекомендуемые настройки для ${e.device}`, -e => `การตั้งค่าที่แนะนำสำหรับ ${e.device}`, -e => `${e.device} için önerilen ayarlar`, -e => `Рекомендовані налаштування для ${e.device}`, -e => `Cấu hình được đề xuất cho ${e.device}`, -e => `${e.device} 的推荐设置`, -e => `${e.device} 推薦的設定` -], -"reduce-animations": "Reduce UI animations", -region: "Region", -"reload-page": "Reload page", -"remote-play": "Remote Play", -rename: "Rename", -renderer: "Renderer", -"renderer-configuration": "Renderer configuration", -"reset-highlighted-setting": "Reset highlighted setting", -"right-click-to-unbind": "Right-click on a key to unbind it", -"right-stick": "Right stick", -"right-stick-deadzone": "Right stick deadzone", -"right-trigger-range": "Right trigger range", -"rocket-always-hide": "Always hide", -"rocket-always-show": "Always show", -"rocket-animation": "Rocket animation", -"rocket-hide-queue": "Hide when queuing", -saturation: "Saturation", -save: "Save", -screen: "Screen", -"screenshot-apply-filters": "Apply video filters to screenshots", -"section-all-games": "All games", -"section-most-popular": "Most popular", -"section-native-mkb": "Play with mouse & keyboard", -"section-news": "News", -"section-play-with-friends": "Play with friends", -"section-touch": "Play with touch", -"separate-touch-controller": "Separate Touch controller & Controller #1", -"separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2", -server: "Server", -"server-locations": "Server locations", -settings: "Settings", -"settings-for": "Settings for", -"settings-reload": "Reload page to reflect changes", -"settings-reload-note": "Settings in this tab only go into effect on the next page load", -"settings-reloading": "Reloading...", -sharpness: "Sharpness", -"shortcut-keys": "Shortcut keys", -show: "Show", -"show-controller-connection-status": "Show controller connection status", -"show-game-art": "Show game art", -"show-hide": "Show/hide", -"show-stats-on-startup": "Show stats when starting the game", -"show-touch-controller": "Show touch controller", -"show-wait-time": "Show the estimated wait time", -"show-wait-time-in-game-card": "Show wait time in game card", -"simplify-stream-menu": "Simplify Stream's menu", -"skip-splash-video": "Skip Xbox splash video", -slow: "Slow", -small: "Small", -"smart-tv": "Smart TV", -sound: "Sound", -standard: "Standard", -standby: "Standby", -"stat-bitrate": "Bitrate", -"stat-decode-time": "Decode time", -"stat-fps": "FPS", -"stat-frames-lost": "Frames lost", -"stat-packets-lost": "Packets lost", -"stat-ping": "Ping", -stats: "Stats", -"stick-decay-minimum": "Stick decay minimum", -"stick-decay-strength": "Stick decay strength", -stream: "Stream", -"stream-settings": "Stream settings", -"stream-stats": "Stream stats", -"stream-your-own-game": "Stream your own game", -stretch: "Stretch", -"suggest-settings": "Suggest settings", -"suggest-settings-link": "Suggest recommended settings for this device", -"support-better-xcloud": "Support Better xCloud", -"swap-buttons": "Swap buttons", -"take-screenshot": "Take screenshot", -"target-resolution": "Target resolution", -"tc-all-white": "All white", -"tc-auto-off": "Off when controller found", -"tc-custom-layout-style": "Custom layout's button style", -"tc-muted-colors": "Muted colors", -"tc-standard-layout-style": "Standard layout's button style", -"text-size": "Text size", -toggle: "Toggle", -top: "Top", -"top-center": "Top-center", -"top-half": "Top half", -"top-left": "Top-left", -"top-right": "Top-right", -"touch-control-layout": "Touch control layout", -"touch-control-layout-by": [ -e => `Touch control layout by ${e.name}`, -e => `Format del control tàctil per ${e.name}`, -e => `Touch-kontrol layout af ${e.name}`, -e => `Touch-Steuerungslayout von ${e.name}`, -e => `Tata letak Sentuhan layar oleh ${e.name}`, -e => `Disposición del control táctil por ${e.nombre}`, -e => `Disposition du contrôleur tactile par ${e.name}`, -e => `Configurazione dei comandi su schermo creata da ${e.name}`, -e => `タッチ操作レイアウト作成者: ${e.name}`, -e => `${e.name} 제작, 터치 컨트롤 레이아웃`, -e => `Układ sterowania dotykowego stworzony przez ${e.name}`, -e => `Disposição de controle por toque feito por ${e.name}`, -e => `Сенсорная раскладка по ${e.name}`, -e => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`, -e => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`, -e => `Розташування сенсорного керування від ${e.name}`, -e => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`, -e => `由 ${e.name} 提供的虚拟按键样式`, -e => `觸控遊玩佈局由 ${e.name} 提供` -], -"touch-controller": "Touch controller", -"true-achievements": "TrueAchievements", -ui: "UI", -"unexpected-behavior": "May cause unexpected behavior", -"united-states": "United States", -unknown: "Unknown", -unlimited: "Unlimited", -unmuted: "Unmuted", -unofficial: "Unofficial", -"unofficial-game-list": "Unofficial game list", -"unsharp-masking": "Unsharp masking", -upload: "Upload", -uploaded: "Uploaded", -"use-mouse-absolute-position": "Use mouse's absolute position", -"use-this-at-your-own-risk": "Use this at your own risk", -"user-agent-profile": "User-Agent profile", -"vertical-scroll-sensitivity": "Vertical scroll sensitivity", -"vertical-sensitivity": "Vertical sensitivity", -"vibration-intensity": "Vibration intensity", -"vibration-status": "Vibration", -video: "Video", -"virtual-controller": "Virtual controller", -"virtual-controller-slot": "Virtual controller slot", -"visual-quality": "Visual quality", -"visual-quality-high": "High", -"visual-quality-low": "Low", -"visual-quality-normal": "Normal", -volume: "Volume", -"wait-time-countdown": "Countdown", -"wait-time-estimated": "Estimated finish time", -"waiting-for-input": "Waiting for input...", -wallpaper: "Wallpaper", -webgl2: "WebGL2" -}; -class Translations { -static EN_US = "en-US"; -static KEY_LOCALE = "BetterXcloud.Locale"; -static KEY_TRANSLATIONS = "BetterXcloud.Locale.Translations"; -static selectedLocaleIndex = -1; -static selectedLocale = "en-US"; -static supportedLocales = Object.keys(SUPPORTED_LANGUAGES); -static foreignTranslations = {}; -static enUsIndex = Translations.supportedLocales.indexOf(Translations.EN_US); -static async init() { -Translations.refreshLocale(), await Translations.loadTranslations(); -} -static refreshLocale(newLocale) { -let locale; -if (newLocale) localStorage.setItem(Translations.KEY_LOCALE, newLocale), locale = newLocale; -else locale = localStorage.getItem(Translations.KEY_LOCALE); -let supportedLocales = Translations.supportedLocales; -if (!locale) { -if (locale = window.navigator.language || Translations.EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.EN_US; -localStorage.setItem(Translations.KEY_LOCALE, locale); -} -Translations.selectedLocale = locale, Translations.selectedLocaleIndex = supportedLocales.indexOf(locale); -} -static get(key, values) { -let text = null; -if (Translations.foreignTranslations && Translations.selectedLocale !== Translations.EN_US) text = Translations.foreignTranslations[key]; -if (!text) text = Texts[key] || alert(`Missing translation key: ${key}`); -let translation; -if (Array.isArray(text)) return translation = text[Translations.selectedLocaleIndex] || text[Translations.enUsIndex], translation(values); -return translation = text, translation; -} -static async loadTranslations() { -if (Translations.selectedLocale === Translations.EN_US) return; -try { -Translations.foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.KEY_TRANSLATIONS)); -} catch (e) {} -if (!Translations.foreignTranslations) await this.downloadTranslations(Translations.selectedLocale); -} -static async updateTranslations(async = !1) { -if (Translations.selectedLocale === Translations.EN_US) { -localStorage.removeItem(Translations.KEY_TRANSLATIONS); -return; -} -if (async) Translations.downloadTranslationsAsync(Translations.selectedLocale); -else await Translations.downloadTranslations(Translations.selectedLocale); -} -static async downloadTranslations(locale) { -try { -let translations = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`))).json(); -if (localStorage.getItem(Translations.KEY_LOCALE) === locale) window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations; -return !0; -} catch (e) { -debugger; -} -return !1; -} -static downloadTranslationsAsync(locale) { -NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`)).then((resp) => resp.json()).then((translations) => { -window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations; -}); -} -static switchLocale(locale) { -localStorage.setItem(Translations.KEY_LOCALE, locale); -} -} -var t = Translations.get; -Translations.init(); -class NavigationUtils { -static setNearby($elm, nearby) { -$elm.nearby = $elm.nearby || {}; -let key; -for (key in nearby) -$elm.nearby[key] = nearby[key]; -} -} -var setNearby = NavigationUtils.setNearby; -var ButtonStyleClass = { -1: "bx-primary", -2: "bx-warning", -4: "bx-danger", -8: "bx-ghost", -16: "bx-frosted", -32: "bx-drop-shadow", -64: "bx-focusable", -128: "bx-full-width", -256: "bx-full-height", -512: "bx-auto-height", -1024: "bx-tall", -2048: "bx-circular", -4096: "bx-normal-case", -8192: "bx-normal-link" -}; -function createElement(elmName, props, ..._) { -let $elm, hasNs = props && "xmlns" in props; -if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns; -else $elm = document.createElement(elmName); -if (props) { -if (props._nearby) setNearby($elm, props._nearby), delete props._nearby; -if (props._on) { -for (let name in props._on) -$elm.addEventListener(name, props._on[name]); -delete props._on; -} -if (props._dataset) { -for (let name in props._dataset) -$elm.dataset[name] = props._dataset[name]; -delete props._dataset; -} -for (let key in props) { -if ($elm.hasOwnProperty(key)) continue; -let value = props[key]; -if (hasNs) $elm.setAttributeNS(null, key, value); -else $elm.setAttribute(key, value); -} -} -for (let i = 2, size = arguments.length;i < size; i++) { -let arg = arguments[i]; -if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.append(arg); -} -return $elm; -} -var domParser = new DOMParser; -function createSvgIcon(icon) { -return domParser.parseFromString(icon, "image/svg+xml").documentElement; -} -var ButtonStyleIndices = Object.keys(ButtonStyleClass).map((i) => parseInt(i)); -function createButton(options) { -let $btn; -if (options.url) $btn = CE("a", { -class: "bx-button", -href: options.url, -target: "_blank" -}); -else $btn = CE("button", { -class: "bx-button", -type: "button" -}), options.disabled && ($btn.disabled = !0); -let style = options.style || 0; -if (style) { -let index; -for (index of ButtonStyleIndices) -style & index && $btn.classList.add(ButtonStyleClass[index]); -} -if (options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", !1, options.label)), options.title && $btn.setAttribute("title", options.title), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0, options.secondaryText) $btn.classList.add("bx-button-multi-lines"), $btn.appendChild(CE("span", !1, options.secondaryText)); -for (let key in options.attributes) -if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]); -return $btn; -} -function createSettingRow(label, $control, options = {}) { -let $label, $row = CE("label", { -class: "bx-settings-row" -}, $label = CE("span", { class: "bx-settings-label" }, options.icon && createSvgIcon(options.icon), label, options.$note), $control); -if (options.pref) $row.prefKey = options.pref; -if (options.onContextMenu) $row.addEventListener("contextmenu", options.onContextMenu); -let $link = $label.querySelector("a"); -if ($link) $link.classList.add("bx-focusable"), setNearby($label, { -focus: $link -}); -if (setNearby($row, { -orientation: options.multiLines ? "vertical" : "horizontal" -}), options.multiLines) -$row.dataset.multiLines = "true"; -if ($control instanceof HTMLElement && $control.id) $row.htmlFor = $control.id; -return $row; -} -function isElementVisible($elm) { -let rect = $elm.getBoundingClientRect(); -return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height; -} -function removeChildElements($parent) { -if ($parent instanceof HTMLDivElement && $parent.classList.contains("bx-select")) $parent = $parent.querySelector("select"); -while ($parent.firstElementChild) -$parent.firstElementChild.remove(); -} -function clearDataSet($elm) { -Object.keys($elm.dataset).forEach((key) => { -delete $elm.dataset[key]; -}); -} -function calculateSelectBoxes($root) { -let selects = Array.from($root.querySelectorAll("div.bx-select:not([data-calculated]) select")); -for (let $select of selects) { -let $parent = $select.parentElement; -if ($parent.classList.contains("bx-full-width")) { -$parent.dataset.calculated = "true"; -continue; -} -let rect = $select.getBoundingClientRect(), $label, width = Math.ceil(rect.width); -if (!width) continue; -if ($label = $parent.querySelector($select.multiple ? ".bx-select-value" : "div"), $parent.isControllerFriendly) { -if ($select.multiple) width += 20; -if ($select.querySelector("optgroup")) width -= 15; -} else width += 10; -$select.style.left = "0", $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; -} -} -var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"]; -function humanFileSize(size) { -let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); -return (size / Math.pow(1024, i)).toFixed(1) + " " + FILE_SIZE_UNITS[i]; -} -function secondsToHm(seconds) { -let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1; -if (m === 60) h += 1, m = 0; -let output = []; -return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); -} -function escapeCssSelector(name) { -return name.replaceAll(".", "-"); -} -var CE = createElement; -window.BX_CE = createElement; -class Toast { -static instance; -static getInstance = () => Toast.instance ?? (Toast.instance = new Toast); -LOG_TAG = "Toast"; -$wrapper; -$msg; -$status; -stack = []; -isShowing = !1; -timeoutId; -DURATION = 3000; -constructor() { -BxLogger.info(this.LOG_TAG, "constructor()"), this.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, this.$msg = CE("span", { class: "bx-toast-msg" }), this.$status = CE("span", { class: "bx-toast-status" })), this.$wrapper.addEventListener("transitionend", (e) => { -let classList = this.$wrapper.classList; -if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), this.showNext(); -}), document.documentElement.appendChild(this.$wrapper); -} -show(msg, status, options = {}) { -options = options || {}; -let args = Array.from(arguments); -if (options.instant) this.stack = [args], this.showNext(); -else this.stack.push(args), !this.isShowing && this.showNext(); -} -showNext() { -if (!this.stack.length) { -this.isShowing = !1; -return; -} -this.isShowing = !0, this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, this.DURATION); -let [msg, status, options] = this.stack.shift(); -if (options && options.html) this.$msg.innerHTML = msg; -else this.$msg.textContent = msg; -if (status) this.$status.classList.remove("bx-gone"), this.$status.textContent = status; -else this.$status.classList.add("bx-gone"); -let classList = this.$wrapper.classList; -classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); -} -hide = () => { -this.timeoutId = null; -let classList = this.$wrapper.classList; -classList.remove("bx-show"), classList.add("bx-hide"); -}; -static show(msg, status, options = {}) { -Toast.getInstance().show(msg, status, options); -} -static showNext() { -Toast.getInstance().showNext(); -} -} -class LocalDb { -static instance; -static getInstance = () => LocalDb.instance ?? (LocalDb.instance = new LocalDb); -static DB_NAME = "BetterXcloud"; -static DB_VERSION = 4; -static TABLE_VIRTUAL_CONTROLLERS = "virtual_controllers"; -static TABLE_CONTROLLER_SHORTCUTS = "controller_shortcuts"; -static TABLE_CONTROLLER_CUSTOMIZATIONS = "controller_customizations"; -static TABLE_CONTROLLER_SETTINGS = "controller_settings"; -static TABLE_KEYBOARD_SHORTCUTS = "keyboard_shortcuts"; -db; -open() { -return new Promise((resolve, reject) => { -if (this.db) { -resolve(this.db); -return; -} -let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION); -request.onupgradeneeded = (e) => { -let db = e.target.result; -if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined"); -if (!db.objectStoreNames.contains(LocalDb.TABLE_VIRTUAL_CONTROLLERS)) db.createObjectStore(LocalDb.TABLE_VIRTUAL_CONTROLLERS, { -keyPath: "id", -autoIncrement: !0 -}); -if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SHORTCUTS, { -keyPath: "id", -autoIncrement: !0 -}); -if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SETTINGS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SETTINGS, { -keyPath: "id" -}); -if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS, { -keyPath: "id", -autoIncrement: !0 -}); -if (!db.objectStoreNames.contains(LocalDb.TABLE_KEYBOARD_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_KEYBOARD_SHORTCUTS, { -keyPath: "id", -autoIncrement: !0 -}); -}, request.onerror = (e) => { -console.log(e), alert(e.target.error.message), reject && reject(); -}, request.onsuccess = (e) => { -this.db = e.target.result, resolve(this.db); -}; -}); -} -} -var BypassServers = { -br: t("brazil"), -jp: t("japan"), -kr: t("korea"), -pl: t("poland"), -us: t("united-states") -}, BypassServerIps = { -br: "169.150.198.66", -kr: "121.125.60.151", -jp: "138.199.21.239", -pl: "45.134.212.66", -us: "143.244.47.65" -}; -class BaseSettingsStorage { -storage; -storageKey; -_settings; -definitions; -constructor(storageKey, definitions) { -this.storage = window.localStorage, this.storageKey = storageKey; -for (let [_, setting] of Object.entries(definitions)) { -if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants]; -setting.ready && setting.ready.call(this, setting); -} -this.definitions = definitions, this._settings = null; -} -get settings() { -if (this._settings) return this._settings; -let settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}"); -for (let key in settings) { -if (!this.definitions.hasOwnProperty(key)) { -delete settings[key]; -continue; -} -settings[key] = this.validateValue("get", key, settings[key]); -} -return this._settings = settings, settings; -} -getDefinition(key) { -if (!this.definitions[key]) return alert("Request invalid definition: " + key), {}; -return this.definitions[key]; -} -hasSetting(key) { -return key in this.settings; -} -getSetting(key, checkUnsupported = !0) { -let definition = this.definitions[key]; -if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default; -if (checkUnsupported && definition.unsupported) if ("unsupportedValue" in definition) return definition.unsupportedValue; -else return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default; -if (!(key in this.settings)) this.settings[key] = this.validateValue("get", key, null); -return isPlainObject(this.settings[key]) ? deepClone(this.settings[key]) : this.settings[key]; -} -setSetting(key, value, origin) { -if (value = this.validateValue("set", key, value), this.settings[key] = this.validateValue("get", key, value), this.saveSettings(), origin === "ui") if (isStreamPref(key)) BxEventBus.Stream.emit("setting.changed", { -storageKey: this.storageKey, -settingKey: key -}); -else BxEventBus.Script.emit("setting.changed", { -storageKey: this.storageKey, -settingKey: key -}); -return value; -} -saveSettings() { -this.storage.setItem(this.storageKey, JSON.stringify(this.settings)); -} -validateValue(action, key, value) { -let def = this.definitions[key]; -if (!def) return value; -if (typeof value === "undefined" || value === null) value = def.default; -if (def.transformValue && action === "get") value = def.transformValue.get.call(def, value); -if ("min" in def) value = Math.max(def.min, value); -if ("max" in def) value = Math.min(def.max, value); -if ("options" in def) { -if (!(value in def.options)) value = def.default; -} else if ("multipleOptions" in def) { -if (value.length) { -let validOptions = Object.keys(def.multipleOptions); -value.forEach((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(); -} -} -var BxIcon = { -BETTER_XCLOUD: "", -TRUE_ACHIEVEMENTS: "", -STREAM_SETTINGS: "", -STREAM_STATS: "", -CLOSE: "", -CONTROLLER: "", -CREATE_SHORTCUT: "", -DISPLAY: "", -EYE: "", -EYE_SLASH: "", -HOME: "", -LOCAL_CO_OP: "", -NATIVE_MKB: "", -NEW: "", -MANAGE: "", -COPY: "", -TRASH: "", -CURSOR_TEXT: "", -POWER: "", -QUESTION: "", -REFRESH: "", -REMOTE_PLAY: "", -CARET_LEFT: "", -CARET_RIGHT: "", -SCREENSHOT: "", -SPEAKER_MUTED: "", -TOUCH_CONTROL_ENABLE: "", -TOUCH_CONTROL_DISABLE: "", -MICROPHONE: "", -MICROPHONE_MUTED: "", -BATTERY: "", -PLAYTIME: "", -SERVER: "", -DOWNLOAD: "", -UPLOAD: "", -AUDIO: "" -}; -function getSupportedCodecProfiles() { -let options = { -default: t("default") -}; -if (!("getCapabilities" in RTCRtpReceiver)) return options; -let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1, codecs = RTCRtpReceiver.getCapabilities("video").codecs; -for (let codec of codecs) { -if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue; -let fmtp = codec.sdpFmtpLine.toLowerCase(); -if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0; -else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0; -else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0; -} -if (hasLowCodec) if (!hasNormalCodec && !hasHighCodec) options["default"] = `${t("visual-quality-low")} (${t("default")})`; -else options["low"] = t("visual-quality-low"); -if (hasNormalCodec) if (!hasLowCodec && !hasHighCodec) options["default"] = `${t("visual-quality-normal")} (${t("default")})`; -else options["normal"] = t("visual-quality-normal"); -if (hasHighCodec) if (!hasLowCodec && !hasNormalCodec) options["default"] = `${t("visual-quality-high")} (${t("default")})`; -else options["high"] = t("visual-quality-high"); -return options; -} -class GlobalSettingsStorage extends BaseSettingsStorage { -static DEFINITIONS = { -"version.lastCheck": { -default: 0 -}, -"version.latest": { -default: "" -}, -"version.current": { -default: "" -}, -"bx.locale": { -label: t("language"), -default: localStorage.getItem("BetterXcloud.Locale") || "en-US", -options: SUPPORTED_LANGUAGES -}, -"server.region": { -label: t("region"), -note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")), -default: "default" -}, -"server.bypassRestriction": { -label: t("bypass-region-restriction"), -note: "⚠️ " + t("use-this-at-your-own-risk"), -default: "off", -optionsGroup: t("region"), -options: Object.assign({ -off: t("off") -}, BypassServers) -}, -"stream.locale": { -label: t("preferred-game-language"), -default: "default", -options: { -default: t("default"), -"ar-SA": "العربية", -"bg-BG": "Български", -"cs-CZ": "čeština", -"da-DK": "dansk", -"de-DE": "Deutsch", -"el-GR": "Ελληνικά", -"en-GB": "English (UK)", -"en-US": "English (US)", -"es-ES": "español (España)", -"es-MX": "español (Latinoamérica)", -"fi-FI": "suomi", -"fr-FR": "français", -"he-IL": "עברית", -"hu-HU": "magyar", -"it-IT": "italiano", -"ja-JP": "日本語", -"ko-KR": "한국어", -"nb-NO": "norsk bokmål", -"nl-NL": "Nederlands", -"pl-PL": "polski", -"pt-BR": "português (Brasil)", -"pt-PT": "português (Portugal)", -"ro-RO": "Română", -"ru-RU": "русский", -"sk-SK": "slovenčina", -"sv-SE": "svenska", -"th-TH": "ไทย", -"tr-TR": "Türkçe", -"zh-CN": "中文(简体)", -"zh-TW": "中文 (繁體)" -} -}, -"stream.video.resolution": { -label: t("target-resolution"), -default: "auto", -options: { -auto: t("default"), -"720p": "720p", -"1080p": "1080p", -"1080p-hq": "1080p (HQ)" -}, -suggest: { -lowest: "720p", -highest: "1080p-hq" -} -}, -"stream.video.codecProfile": { -label: t("visual-quality"), -default: "default", -options: getSupportedCodecProfiles(), -ready: (setting) => { -let options = setting.options, keys = Object.keys(options); -if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature"); -setting.suggest = { -lowest: keys.length === 1 ? keys[0] : keys[1], -highest: keys[keys.length - 1] -}; -} -}, -"server.ipv6.prefer": { -label: t("prefer-ipv6-server"), -default: !1 -}, -"screenshot.applyFilters": { -requiredVariants: "full", -label: t("screenshot-apply-filters"), -default: !1 -}, -"ui.splashVideo.skip": { -label: t("skip-splash-video"), -default: !1 -}, -"ui.systemMenu.hideHandle": { -label: "⣿ " + t("hide-system-menu-icon"), -default: !1 -}, -"ui.imageQuality": { -requiredVariants: "full", -label: t("image-quality"), -default: 90, -min: 10, -max: 90, -params: { -steps: 10, -exactTicks: 20, -hideSlider: !0, -customTextValue(value, min, max) { -if (value === 90) return t("default"); -return value + "%"; -} -} -}, -"stream.video.combineAudio": { -requiredVariants: "full", -label: t("combine-audio-video-streams"), -default: !1, -experimental: !0, -note: t("combine-audio-video-streams-summary") -}, -"touchController.mode": { -requiredVariants: "full", -label: t("availability"), -default: "all", -options: { -default: t("default"), -off: t("off"), -all: t("all-games") -}, -unsupported: !STATES.userAgent.capabilities.touch, -unsupportedValue: "default" -}, -"touchController.autoOff": { -requiredVariants: "full", -label: t("tc-auto-off"), -default: !1, -unsupported: !STATES.userAgent.capabilities.touch -}, -"touchController.opacity.default": { -requiredVariants: "full", -label: t("default-opacity"), -default: 100, -min: 10, -max: 100, -params: { -steps: 10, -suffix: "%", -ticks: 10, -hideSlider: !0 -}, -unsupported: !STATES.userAgent.capabilities.touch -}, -"touchController.style.standard": { -requiredVariants: "full", -label: t("tc-standard-layout-style"), -default: "default", -options: { -default: t("default"), -white: t("tc-all-white"), -muted: t("tc-muted-colors") -}, -unsupported: !STATES.userAgent.capabilities.touch -}, -"touchController.style.custom": { -requiredVariants: "full", -label: t("tc-custom-layout-style"), -default: "default", -options: { -default: t("default"), -muted: t("tc-muted-colors") -}, -unsupported: !STATES.userAgent.capabilities.touch -}, -"ui.streamMenu.simplify": { -label: t("simplify-stream-menu"), -default: !1 -}, -"mkb.cursor.hideIdle": { -requiredVariants: "full", -label: t("hide-idle-cursor"), -default: !1 -}, -"ui.feedbackDialog.disabled": { -requiredVariants: "full", -label: t("disable-post-stream-feedback-dialog"), -default: !1 -}, -"stream.video.maxBitrate": { -requiredVariants: "full", -label: t("bitrate-video-maximum"), -note: "⚠️ " + t("unexpected-behavior"), -default: 0, -min: 102400, -max: 15360000, -transformValue: { -get(value) { -return value === 0 ? this.max : value; -}, -set(value) { -return value === this.max ? 0 : value; -} -}, -params: { -steps: 102400, -exactTicks: 5120000, -customTextValue: (value, min, max) => { -if (value = parseInt(value), value === max) return t("unlimited"); -else return (value / 1024000).toFixed(1) + " Mb/s"; -} -}, -suggest: { -highest: 0 -} -}, -"gameBar.position": { -requiredVariants: "full", -label: t("position"), -default: "bottom-left", -options: { -off: t("off"), -"bottom-left": t("bottom-left"), -"bottom-right": t("bottom-right") -} -}, -"ui.controllerStatus.show": { -label: t("show-controller-connection-status"), -default: !0 -}, -"mkb.enabled": { -requiredVariants: "full", -label: t("enable-mkb"), -default: !1, -unsupported: !STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb, -ready: (setting) => { -let note, url; -if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657"; -else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer"; -setting.unsupportedNote = () => CE("a", { -href: url, -target: "_blank" -}, "⚠️ " + note); -} -}, -"nativeMkb.mode": { -requiredVariants: "full", -label: t("native-mkb"), -default: "default", -options: { -default: t("default"), -off: t("off"), -on: t("on") -}, -ready: (setting) => { -if (STATES.browser.capabilities.emulatedNativeMkb) ; -else if (UserAgent.isMobile()) setting.unsupported = !0, setting.unsupportedValue = "off", delete setting.options["default"], delete setting.options["on"]; -else delete setting.options["on"]; -} -}, -"nativeMkb.forcedGames": { -label: t("force-native-mkb-games"), -default: [], -unsupported: !AppInterface && UserAgent.isMobile(), -ready: (setting) => { -if (!setting.unsupported) setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(!0), BxEventBus.Script.once("list.forcedNativeMkb.updated", (payload) => { -setting.multipleOptions = payload.data.data; -}); -}, -params: { -size: 6 -} -}, -"ui.reduceAnimations": { -label: t("reduce-animations"), -default: !1 -}, -"loadingScreen.gameArt.show": { -requiredVariants: "full", -label: t("show-game-art"), -default: !0 -}, -"loadingScreen.waitTime.show": { -label: t("show-wait-time"), -default: !0 -}, -"loadingScreen.rocket": { -label: t("rocket-animation"), -default: "show", -options: { -show: t("rocket-always-show"), -"hide-queue": t("rocket-hide-queue"), -hide: t("rocket-always-hide") -} -}, -"ui.controllerFriendly": { -label: t("controller-friendly-ui"), -default: BX_FLAGS.DeviceInfo.deviceType !== "unknown" -}, -"ui.layout": { -requiredVariants: "full", -label: t("layout"), -default: "default", -options: { -default: t("default"), -normal: t("normal"), -tv: t("smart-tv") -} -}, -"ui.hideScrollbar": { -label: t("hide-scrollbar"), -default: !1 -}, -"ui.hideSections": { -requiredVariants: "full", -label: t("hide-sections"), -default: [], -multipleOptions: { -news: t("section-news"), -friends: t("section-play-with-friends"), -"native-mkb": t("section-native-mkb"), -touch: t("section-touch"), -"most-popular": t("section-most-popular"), -"all-games": t("section-all-games") -}, -params: { -size: 0 -} -}, -"ui.gameCard.waitTime.show": { -requiredVariants: "full", -label: t("show-wait-time-in-game-card"), -default: !0 -}, -"block.tracking": { -label: t("disable-xcloud-analytics"), -default: !1 -}, -"block.features": { -requiredVariants: "full", -label: t("disable-features"), -default: [], -multipleOptions: { -chat: t("chat"), -friends: t("friends-followers"), -byog: t("stream-your-own-game"), -"notifications-invites": t("notifications") + ": " + t("invites"), -"notifications-achievements": t("notifications") + ": " + t("achievements") -} -}, -"userAgent.profile": { -label: t("user-agent-profile"), -note: "⚠️ " + t("unexpected-behavior"), -default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default", -options: { -default: t("default"), -"windows-edge": "Edge + Windows", -"macos-safari": "Safari + macOS", -"vr-oculus": "Android TV", -"smarttv-generic": "Smart TV", -"smarttv-tizen": "Samsung Smart TV", -custom: t("custom") -} -}, -"audio.mic.onPlaying": { -label: t("enable-mic-on-startup"), -default: !1 -}, -"audio.volume.booster.enabled": { -requiredVariants: "full", -label: t("enable-volume-control"), -default: !1 -}, -"xhome.enabled": { -requiredVariants: "full", -label: t("enable-remote-play-feature"), -labelIcon: BxIcon.REMOTE_PLAY, -default: !1 -}, -"xhome.video.resolution": { -requiredVariants: "full", -default: "1080p", -options: { -"720p": "720p", -"1080p": "1080p", -"1080p-hq": "1080p (HQ)" -} -}, -"game.fortnite.forceConsole": { -requiredVariants: "full", -label: "🎮 " + t("fortnite-force-console-version"), -default: !1, -note: t("fortnite-allow-stw-mode") -} -}; -constructor() { -super("BetterXcloud", GlobalSettingsStorage.DEFINITIONS); -} -} -class BaseLocalTable { -tableName; -constructor(tableName) { -this.tableName = tableName; -} -async prepareTable(type = "readonly") { -return (await LocalDb.getInstance().open()).transaction(this.tableName, type).objectStore(this.tableName); -} -call(method) { -return new Promise((resolve) => { -let request = method.call(null, ...Array.from(arguments).slice(1)); -request.onsuccess = (e) => { -resolve(e.target.result); -}; -}); -} -async count() { -let table = await this.prepareTable(); -return this.call(table.count.bind(table)); -} -async add(data) { -let table = await this.prepareTable("readwrite"); -return this.call(table.add.bind(table), ...arguments); -} -async put(data) { -let table = await this.prepareTable("readwrite"); -return this.call(table.put.bind(table), ...arguments); -} -async delete(id) { -let table = await this.prepareTable("readwrite"); -return this.call(table.delete.bind(table), ...arguments); -} -async get(id) { -let table = await this.prepareTable(); -return this.call(table.get.bind(table), ...arguments); -} -async getAll() { -let table = await this.prepareTable(), all = await this.call(table.getAll.bind(table), ...arguments), results = {}; -return all.forEach((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 ControllerCustomizationsTable extends BasePresetsTable { -static instance; -static getInstance = () => ControllerCustomizationsTable.instance ?? (ControllerCustomizationsTable.instance = new ControllerCustomizationsTable(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)); -TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS; -DEFAULT_PRESETS = { -[-1]: { -id: -1, -name: "ABXY ⇄ BAYX", -data: { -mapping: { -0: 1, -1: 0, -2: 3, -3: 2 -}, -settings: { -leftStickDeadzone: [0, 100], -rightStickDeadzone: [0, 100], -leftTriggerRange: [0, 100], -rightTriggerRange: [0, 100], -vibrationIntensity: 100 -} -} -} -}; -BLANK_PRESET_DATA = { -mapping: {}, -settings: { -leftTriggerRange: [0, 100], -rightTriggerRange: [0, 100], -leftStickDeadzone: [0, 100], -rightStickDeadzone: [0, 100], -vibrationIntensity: 100 -} -}; -DEFAULT_PRESET_ID = 0; -} -class ControllerShortcutsTable extends BasePresetsTable { -static instance; -static getInstance = () => ControllerShortcutsTable.instance ?? (ControllerShortcutsTable.instance = new ControllerShortcutsTable); -LOG_TAG = "ControllerShortcutsTable"; -TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_SHORTCUTS; -DEFAULT_PRESETS = { -[-1]: { -id: -1, -name: "Type A", -data: { -mapping: { -3: AppInterface ? "device.volume.inc" : "stream.volume.inc", -0: AppInterface ? "device.volume.dec" : "stream.volume.dec", -2: "stream.stats.toggle", -1: AppInterface ? "device.sound.toggle" : "stream.sound.toggle", -5: "stream.screenshot.capture", -9: "stream.menu.show" -} -} -}, -[-2]: { -id: -2, -name: "Type B", -data: { -mapping: { -12: AppInterface ? "device.volume.inc" : "stream.volume.inc", -13: AppInterface ? "device.volume.dec" : "stream.volume.dec", -15: "stream.stats.toggle", -14: AppInterface ? "device.sound.toggle" : "stream.sound.toggle", -4: "stream.screenshot.capture", -8: "stream.menu.show" -} -} -} -}; -BLANK_PRESET_DATA = { -mapping: {} -}; -DEFAULT_PRESET_ID = -1; -constructor() { -super(LocalDb.TABLE_CONTROLLER_SHORTCUTS); -BxLogger.info(this.LOG_TAG, "constructor()"); -} -} -class StreamSettingsStorage extends BaseSettingsStorage { -static DEFINITIONS = { -"deviceVibration.mode": { -requiredVariants: "full", -label: t("device-vibration"), -default: "off", -options: { -off: t("off"), -on: t("on"), -auto: t("device-vibration-not-using-gamepad") -} -}, -"deviceVibration.intensity": { -requiredVariants: "full", -label: t("vibration-intensity"), -default: 50, -min: 10, -max: 100, -params: { -steps: 10, -suffix: "%", -exactTicks: 20 -} -}, -"controller.pollingRate": { -requiredVariants: "full", -label: t("polling-rate"), -default: 4, -min: 4, -max: 60, -params: { -steps: 4, -exactTicks: 20, -reverse: !0, -customTextValue(value) { -value = parseInt(value); -let text = +(1000 / value).toFixed(2) + " Hz"; -if (value === 4) text = `${text} (${t("default")})`; -return text; -} -} -}, -"controller.settings": { -default: {} -}, -"nativeMkb.scroll.sensitivityX": { -requiredVariants: "full", -label: t("horizontal-scroll-sensitivity"), -default: 0, -min: 0, -max: 1e4, -params: { -steps: 10, -exactTicks: 2000, -customTextValue: (value) => { -if (!value) return t("default"); -return (value / 100).toFixed(1) + "x"; -} -} -}, -"nativeMkb.scroll.sensitivityY": { -requiredVariants: "full", -label: t("vertical-scroll-sensitivity"), -default: 0, -min: 0, -max: 1e4, -params: { -steps: 10, -exactTicks: 2000, -customTextValue: (value) => { -if (!value) return t("default"); -return (value / 100).toFixed(1) + "x"; -} -} -}, -"mkb.p1.preset.mappingId": { -requiredVariants: "full", -default: -1 -}, -"mkb.p1.slot": { -requiredVariants: "full", -default: 1, -min: 1, -max: 4, -params: { -hideSlider: !0 -} -}, -"mkb.p2.preset.mappingId": { -requiredVariants: "full", -default: 0 -}, -"mkb.p2.slot": { -requiredVariants: "full", -default: 0, -min: 0, -max: 4, -params: { -hideSlider: !0, -customTextValue(value) { -return value = parseInt(value), value === 0 ? t("off") : value.toString(); -} -} -}, -"keyboardShortcuts.preset.inGameId": { -requiredVariants: "full", -default: -1 -}, -"video.player.type": { -label: t("renderer"), -default: "default", -options: { -default: t("default"), -webgl2: t("webgl2") -}, -suggest: { -lowest: "default", -highest: "webgl2" -} -}, -"video.processing": { -label: t("clarity-boost"), -default: "usm", -options: { -usm: t("unsharp-masking"), -cas: t("amd-fidelity-cas") -}, -suggest: { -lowest: "usm", -highest: "cas" -} -}, -"video.player.powerPreference": { -label: t("renderer-configuration"), -default: "default", -options: { -default: t("default"), -"low-power": t("battery-saving"), -"high-performance": t("high-performance") -}, -suggest: { -highest: "low-power" -} -}, -"video.maxFps": { -label: t("limit-fps"), -default: 60, -min: 10, -max: 60, -params: { -steps: 10, -exactTicks: 10, -customTextValue: (value) => { -return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps"; -} -} -}, -"video.processing.sharpness": { -label: t("sharpness"), -default: 0, -min: 0, -max: 10, -params: { -exactTicks: 2, -customTextValue: (value) => { -return value = parseInt(value), value === 0 ? t("off") : value.toString(); -} -}, -suggest: { -lowest: 0, -highest: 2 -} -}, -"video.ratio": { -label: t("aspect-ratio"), -note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0, -default: "16:9", -options: { -"16:9": `16:9 (${t("default")})`, -"18:9": "18:9", -"21:9": "21:9", -"16:10": "16:10", -"4:3": "4:3", -fill: t("stretch") -} -}, -"video.position": { -label: t("position"), -note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0, -default: "center", -options: { -top: t("top"), -"top-half": t("top-half"), -center: `${t("center")} (${t("default")})`, -"bottom-half": t("bottom-half"), -bottom: t("bottom") -} -}, -"video.saturation": { -label: t("saturation"), -default: 100, -min: 50, -max: 150, -params: { -suffix: "%", -ticks: 25 -} -}, -"video.contrast": { -label: t("contrast"), -default: 100, -min: 50, -max: 150, -params: { -suffix: "%", -ticks: 25 -} -}, -"video.brightness": { -label: t("brightness"), -default: 100, -min: 50, -max: 150, -params: { -suffix: "%", -ticks: 25 -} -}, -"audio.volume": { -label: t("volume"), -default: 100, -min: 0, -max: 600, -params: { -steps: 10, -suffix: "%", -ticks: 100 -} -}, -"stats.items": { -label: t("stats"), -default: ["ping", "fps", "btr", "dt", "pl", "fl"], -multipleOptions: { -time: t("clock"), -play: t("playtime"), -batt: t("battery"), -ping: t("stat-ping"), -jit: t("jitter"), -fps: t("stat-fps"), -btr: t("stat-bitrate"), -dt: t("stat-decode-time"), -pl: t("stat-packets-lost"), -fl: t("stat-frames-lost"), -dl: t("downloaded"), -ul: t("uploaded") -}, -params: { -size: 0 -}, -ready: (setting) => { -let multipleOptions = setting.multipleOptions; -if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"]; -for (let key in multipleOptions) -multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key]; -} -}, -"stats.showWhenPlaying": { -label: t("show-stats-on-startup"), -default: !1 -}, -"stats.quickGlance.enabled": { -label: "👀 " + t("enable-quick-glance-mode"), -default: !0 -}, -"stats.position": { -label: t("position"), -default: "top-right", -options: { -"top-left": t("top-left"), -"top-center": t("top-center"), -"top-right": t("top-right") -} -}, -"stats.textSize": { -label: t("text-size"), -default: "0.9rem", -options: { -"0.9rem": t("small"), -"1.0rem": t("normal"), -"1.1rem": t("large") -} -}, -"stats.opacity.all": { -label: t("opacity"), -default: 80, -min: 50, -max: 100, -params: { -steps: 10, -suffix: "%", -ticks: 10 -} -}, -"stats.opacity.background": { -label: t("background-opacity"), -default: 100, -min: 0, -max: 100, -params: { -steps: 10, -suffix: "%", -ticks: 10 -} -}, -"stats.colors": { -label: t("conditional-formatting"), -default: !1 -}, -"localCoOp.enabled": { -requiredVariants: "full", -label: t("enable-local-co-op-support"), -labelIcon: BxIcon.LOCAL_CO_OP, -default: !1, -note: () => CE("div", !1, CE("a", { -href: "https://github.com/redphx/better-xcloud/discussions/275", -target: "_blank" -}, t("enable-local-co-op-support-note")), CE("br"), "⚠️ " + t("unexpected-behavior")) -} -}; -gameSettings = {}; -xboxTitleId = -1; -constructor() { -super("BetterXcloud.Stream", StreamSettingsStorage.DEFINITIONS); -} -setGameId(id) { -this.xboxTitleId = id; -} -getGameSettings(id) { -if (id > -1) { -if (!this.gameSettings[id]) this.gameSettings[id] = new GameSettingsStorage(id); -return this.gameSettings[id]; -} -return null; -} -getSetting(key, checkUnsupported) { -return this.getSettingByGame(this.xboxTitleId, key, !0, checkUnsupported); -} -getSettingByGame(id, key, returnBaseValue = !0, checkUnsupported) { -let gameSettings = this.getGameSettings(id); -if (gameSettings?.hasSetting(key)) return gameSettings.getSetting(key, checkUnsupported); -if (returnBaseValue) return super.getSetting(key, checkUnsupported); -return; -} -setSetting(key, value, origin) { -return this.setSettingByGame(this.xboxTitleId, key, value, origin); -} -setSettingByGame(id, key, value, origin) { -let gameSettings = this.getGameSettings(id); -if (gameSettings) return BxLogger.info("setSettingByGame", id, key, value), gameSettings.setSetting(key, value, origin); -return BxLogger.info("setSettingByGame", id, key, value), super.setSetting(key, value, origin); -} -hasGameSetting(id, key) { -let gameSettings = this.getGameSettings(id); -return !!(gameSettings && gameSettings.hasSetting(key)); -} -getControllerSetting(gamepadId) { -let controllerSetting = this.getSetting("controller.settings")[gamepadId]; -if (!controllerSetting) controllerSetting = {}; -if (!controllerSetting.hasOwnProperty("shortcutPresetId")) controllerSetting.shortcutPresetId = -1; -if (!controllerSetting.hasOwnProperty("customizationPresetId")) controllerSetting.customizationPresetId = 0; -return controllerSetting; -} -} -var STORAGE = { -Global: new GlobalSettingsStorage, -Stream: new StreamSettingsStorage -}, streamSettingsStorage = STORAGE.Stream, getStreamPrefDefinition = streamSettingsStorage.getDefinition.bind(streamSettingsStorage), getStreamPref = streamSettingsStorage.getSetting.bind(streamSettingsStorage), setStreamPref = streamSettingsStorage.setSetting.bind(streamSettingsStorage), getGamePref = streamSettingsStorage.getSettingByGame.bind(streamSettingsStorage), setGamePref = streamSettingsStorage.setSettingByGame.bind(streamSettingsStorage), setGameIdPref = streamSettingsStorage.setGameId.bind(streamSettingsStorage), hasGamePref = streamSettingsStorage.hasGameSetting.bind(streamSettingsStorage); -STORAGE.Stream = streamSettingsStorage; -var globalSettingsStorage = STORAGE.Global, getGlobalPrefDefinition = globalSettingsStorage.getDefinition.bind(globalSettingsStorage), getGlobalPref = globalSettingsStorage.getSetting.bind(globalSettingsStorage), setGlobalPref = globalSettingsStorage.setSetting.bind(globalSettingsStorage); -function isGlobalPref(prefKey) { -return ALL_PREFS.global.includes(prefKey); -} -function isStreamPref(prefKey) { -return ALL_PREFS.stream.includes(prefKey); -} -function getPrefInfo(prefKey) { -if (isGlobalPref(prefKey)) return { -storage: STORAGE.Global, -definition: getGlobalPrefDefinition(prefKey) -}; -else if (isStreamPref(prefKey)) return { -storage: STORAGE.Stream, -definition: getStreamPrefDefinition(prefKey) -}; -return alert("Missing pref definition: " + prefKey), {}; -} -function setPref(prefKey, value, origin) { -if (isGlobalPref(prefKey)) setGlobalPref(prefKey, value, origin); -else if (isStreamPref(prefKey)) setStreamPref(prefKey, value, origin); -} -function 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.Stream.on("keyboardShortcuts.updated", () => { -let $newButton = this.createActivateButton(); -this.$btnActivate.replaceWith($newButton), this.$btnActivate = $newButton; -}); -} -attachMkbHandler(handler) { -this.mkbHandler = handler, this.popupType = handler instanceof NativeMkbHandler ? "native" : "virtual", this.$popup.dataset.type = this.popupType, this.$title.innerText = t(this.popupType === "native" ? "native-mkb" : "virtual-controller"); -} -toggleVisibility(show) { -this.$popup.classList.toggle("bx-gone", !show), show && this.moveOffscreen(!1); -} -moveOffscreen(doMove) { -this.$popup.classList.toggle("bx-offscreen", doMove); -} -createActivateButton() { -let options = { -style: 1 | 1024 | 128, -label: t("activate"), -onClick: this.onActivate -}, shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); -if (shortcutKey) options.secondaryText = t("press-key-to-toggle-mkb", { key: KeyHelper.codeToKeyName(shortcutKey) }); -return createButton(options); -} -onActivate = (e) => { -e.preventDefault(), this.mkbHandler.toggle(!0); -}; -render() { -this.$popup = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, this.$title = CE("p"), this.$btnActivate = this.createActivateButton(), CE("div", !1, createButton({ -label: t("ignore"), -style: 8, -onClick: (e) => { -e.preventDefault(), this.mkbHandler.toggle(!1), this.mkbHandler.waitForMouseData(!1); -} -}), createButton({ -label: t("manage"), -icon: BxIcon.MANAGE, -style: 64, -onClick: () => { -let dialog = SettingsDialog.getInstance(); -dialog.focusTab("mkb"), dialog.show(); -} -}))), document.documentElement.appendChild(this.$popup); -} -reset() { -this.toggleVisibility(!0), this.moveOffscreen(!1); -} -} -class NativeMkbHandler extends MkbHandler { -static instance; -static getInstance() { -if (typeof NativeMkbHandler.instance === "undefined") if (NativeMkbHandler.isAllowed()) NativeMkbHandler.instance = new NativeMkbHandler; -else NativeMkbHandler.instance = null; -return NativeMkbHandler.instance; -} -LOG_TAG = "NativeMkbHandler"; -static isAllowed = () => { -return STATES.browser.capabilities.emulatedNativeMkb && getGlobalPref("nativeMkb.mode") === "on"; -}; -pointerClient; -enabled = !1; -mouseButtonsPressed = 0; -mouseVerticalMultiply = 0; -mouseHorizontalMultiply = 0; -inputChannel; -popup; -constructor() { -super(); -BxLogger.info(this.LOG_TAG, "constructor()"), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this); -} -onKeyboardEvent(e) { -if (e.type === "keyup" && e.code === "F8") { -e.preventDefault(), this.toggle(); -return; -} -} -onPointerLockRequested(e) { -AppInterface.requestPointerCapture(), this.start(); -} -onPointerLockExited(e) { -AppInterface.releasePointerCapture(), this.stop(); -} -onPollingModeChanged = (e) => { -let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none"; -this.popup.moveOffscreen(move); -}; -onDialogShown = () => { -document.pointerLockElement && document.exitPointerLock(); -}; -handleEvent(event) { -switch (event.type) { -case "keyup": -this.onKeyboardEvent(event); -break; -case BxEvent.POINTER_LOCK_REQUESTED: -this.onPointerLockRequested(event); -break; -case BxEvent.POINTER_LOCK_EXITED: -this.onPointerLockExited(event); -break; -case BxEvent.XCLOUD_POLLING_MODE_CHANGED: -this.onPollingModeChanged(event); -break; -} -} -init() { -this.pointerClient = PointerClient.getInstance(), this.inputChannel = window.BX_EXPOSED.inputChannel, this.updateInputConfigurationAsync(!1); -try { -this.pointerClient.start(STATES.pointerServerPort, this); -} catch (e) { -Toast.show("Cannot enable Mouse & Keyboard feature"); -} -this.mouseVerticalMultiply = getStreamPref("nativeMkb.scroll.sensitivityY"), this.mouseHorizontalMultiply = getStreamPref("nativeMkb.scroll.sensitivityX"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.on("dialog.shown", this.onDialogShown); -let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); -if (shortcutKey) { -let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` }); -Toast.show(msg, t("native-mkb"), { html: !0 }); -} -this.waitForMouseData(!1); -} -toggle(force) { -let setEnable; -if (typeof force !== "undefined") setEnable = force; -else setEnable = !this.enabled; -if (setEnable) document.documentElement.requestPointerLock(); -else document.exitPointerLock(); -} -updateInputConfigurationAsync(enabled) { -window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({ -enableKeyboardInput: enabled, -enableMouseInput: enabled, -enableAbsoluteMouse: !1, -enableTouchInput: !1 -}); -} -start() { -this.resetMouseInput(), this.enabled = !0, this.updateInputConfigurationAsync(!0), window.BX_EXPOSED.stopTakRendering = !0, this.waitForMouseData(!1), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 }); -} -stop() { -this.resetMouseInput(), this.enabled = !1, this.updateInputConfigurationAsync(!1), this.waitForMouseData(!0); -} -destroy() { -this.pointerClient?.stop(), this.stop(), window.removeEventListener("keyup", this), window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.waitForMouseData(!1), document.exitPointerLock(); -} -handleMouseMove(data) { -this.sendMouseInput({ -X: data.movementX, -Y: data.movementY, -Buttons: this.mouseButtonsPressed, -WheelX: 0, -WheelY: 0 -}); -} -handleMouseClick(data) { -let { pointerButton, pressed } = data; -if (pressed) this.mouseButtonsPressed |= pointerButton; -else this.mouseButtonsPressed ^= pointerButton; -this.mouseButtonsPressed = Math.max(0, this.mouseButtonsPressed), this.sendMouseInput({ -X: 0, -Y: 0, -Buttons: this.mouseButtonsPressed, -WheelX: 0, -WheelY: 0 -}); -} -handleMouseWheel(data) { -let { vertical, horizontal } = data, mouseWheelX = horizontal; -if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) mouseWheelX *= this.mouseHorizontalMultiply; -let mouseWheelY = vertical; -if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) mouseWheelY *= this.mouseVerticalMultiply; -return this.sendMouseInput({ -X: 0, -Y: 0, -Buttons: this.mouseButtonsPressed, -WheelX: mouseWheelX, -WheelY: mouseWheelY -}), !0; -} -setVerticalScrollMultiplier(vertical) { -this.mouseVerticalMultiply = vertical; -} -setHorizontalScrollMultiplier(horizontal) { -this.mouseHorizontalMultiply = horizontal; -} -waitForMouseData(showPopup) { -this.popup.toggleVisibility(showPopup); -} -isEnabled() { -return this.enabled; -} -sendMouseInput(data) { -data.Type = 0, this.inputChannel?.queueMouseInput(data); -} -resetMouseInput() { -this.mouseButtonsPressed = 0, this.sendMouseInput({ -X: 0, -Y: 0, -Buttons: 0, -WheelX: 0, -WheelY: 0 -}); -} -} -function showGamepadToast(gamepad) { -if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; -if (gamepad._noToast) return; -BxLogger.info("Gamepad", gamepad); -let text = "🎮"; -if (getStreamPref("localCoOp.enabled")) text += ` #${gamepad.index + 1}`; -let gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, ""); -text += ` - ${gamepadId}`; -let status; -if (gamepad.connected) status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status"); -else status = t("disconnected"); -Toast.show(text, status, { instant: !1 }); -} -function 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 -}, VIRTUAL_GAMEPAD_ID = "Better xCloud Virtual Controller"; -class WebSocketMouseDataProvider extends MouseDataProvider { -pointerClient; -isConnected = !1; -init() { -this.pointerClient = PointerClient.getInstance(), this.isConnected = !1; -try { -this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.isConnected = !0; -} catch (e) { -Toast.show("Cannot enable Mouse & Keyboard feature"); -} -} -start() { -this.isConnected && AppInterface.requestPointerCapture(); -} -stop() { -this.isConnected && AppInterface.releasePointerCapture(); -} -destroy() { -this.isConnected && this.pointerClient?.stop(); -} -} -class PointerLockMouseDataProvider extends MouseDataProvider { -start() { -window.addEventListener("mousemove", this.onMouseMoveEvent), window.addEventListener("mousedown", this.onMouseEvent), window.addEventListener("mouseup", this.onMouseEvent), window.addEventListener("wheel", this.onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.disableContextMenu); -} -stop() { -document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.onMouseMoveEvent), window.removeEventListener("mousedown", this.onMouseEvent), window.removeEventListener("mouseup", this.onMouseEvent), window.removeEventListener("wheel", this.onWheelEvent), window.removeEventListener("contextmenu", this.disableContextMenu); -} -onMouseMoveEvent = (e) => { -this.mkbHandler.handleMouseMove({ -movementX: e.movementX, -movementY: e.movementY -}); -}; -onMouseEvent = (e) => { -e.preventDefault(); -let data = { -mouseButton: e.button, -pressed: e.type === "mousedown" -}; -this.mkbHandler.handleMouseClick(data); -}; -onWheelEvent = (e) => { -if (!KeyHelper.getKeyFromEvent(e)) return; -let data = { -vertical: e.deltaY, -horizontal: e.deltaX -}; -if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); -}; -disableContextMenu = (e) => e.preventDefault(); -} -class EmulatedMkbHandler extends MkbHandler { -static instance; -static getInstance() { -if (typeof EmulatedMkbHandler.instance === "undefined") if (EmulatedMkbHandler.isAllowed()) EmulatedMkbHandler.instance = new EmulatedMkbHandler; -else EmulatedMkbHandler.instance = null; -return EmulatedMkbHandler.instance; -} -static LOG_TAG = "EmulatedMkbHandler"; -static isAllowed() { -return getGlobalPref("mkb.enabled") && (AppInterface || !UserAgent.isMobile()); -} -PRESET; -VIRTUAL_GAMEPAD = { -id: VIRTUAL_GAMEPAD_ID, -index: 0, -connected: !1, -hapticActuators: null, -mapping: "standard", -axes: [0, 0, 0, 0], -buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })), -timestamp: performance.now(), -vibrationActuator: null -}; -nativeGetGamepads; -xCloudGamepad = generateVirtualControllerMapping(0); -initialized = !1; -enabled = !1; -mouseDataProvider; -isPolling = !1; -prevWheelCode = null; -wheelStoppedTimeoutId = null; -detectMouseStoppedTimeoutId = null; -escKeyDownTime = -1; -LEFT_STICK_X = []; -LEFT_STICK_Y = []; -RIGHT_STICK_X = []; -RIGHT_STICK_Y = []; -popup; -STICK_MAP = { -102: [this.LEFT_STICK_X, -1], -103: [this.LEFT_STICK_X, 1], -100: [this.LEFT_STICK_Y, 1], -101: [this.LEFT_STICK_Y, -1], -202: [this.RIGHT_STICK_X, -1], -203: [this.RIGHT_STICK_X, 1], -200: [this.RIGHT_STICK_Y, 1], -201: [this.RIGHT_STICK_Y, -1] -}; -constructor() { -super(); -BxLogger.info(EmulatedMkbHandler.LOG_TAG, "constructor()"), this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this); -} -isEnabled = () => this.enabled; -patchedGetGamepads = () => { -let gamepads = this.nativeGetGamepads() || []; -return gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD, gamepads; -}; -getVirtualGamepad = () => this.VIRTUAL_GAMEPAD; -updateStick(stick, x, y) { -let gamepad = this.xCloudGamepad; -if (stick === 0) gamepad.LeftThumbXAxis = x, gamepad.LeftThumbYAxis = -y; -else gamepad.RightThumbXAxis = x, gamepad.RightThumbYAxis = -y; -window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]); -} -vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); -resetXcloudGamepads() { -let index = getStreamPref("mkb.p1.slot") - 1; -this.xCloudGamepad = generateVirtualControllerMapping(0, { -GamepadIndex: getStreamPref("localCoOp.enabled") ? index : 0, -Dirty: !0 -}), this.VIRTUAL_GAMEPAD.index = index; -} -pressButton(buttonIndex, pressed) { -let xCloudKey = toXcloudGamepadKey(buttonIndex); -if (buttonIndex >= 100) { -let [valueArr] = this.STICK_MAP[buttonIndex]; -for (let i = valueArr.length - 1;i >= 0; i--) -if (valueArr[i] === buttonIndex) valueArr.splice(i, 1); -pressed && valueArr.push(buttonIndex); -let value; -if (valueArr.length) value = this.STICK_MAP[valueArr[valueArr.length - 1]][1]; -else value = 0; -this.xCloudGamepad[xCloudKey] = value; -} else this.xCloudGamepad[xCloudKey] = pressed ? 1 : 0; -window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]); -} -onKeyboardEvent = (e) => { -let isKeyDown = e.type === "keydown"; -if (e.code === "Escape") { -if (e.preventDefault(), this.enabled && isKeyDown) { -if (this.escKeyDownTime === -1) this.escKeyDownTime = performance.now(); -else if (performance.now() - this.escKeyDownTime >= 1000) this.stop(); -} else this.escKeyDownTime = -1; -return; -} -if (!this.isPolling || !this.PRESET) return; -if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return; -let buttonIndex = this.PRESET.mapping[e.code || e.key]; -if (typeof buttonIndex === "undefined") return; -if (e.repeat) return; -e.preventDefault(), this.pressButton(buttonIndex, isKeyDown); -}; -onMouseStopped = () => { -if (this.detectMouseStoppedTimeoutId = null, !this.PRESET) return; -let analog = this.PRESET.mouse["mapTo"] === 1 ? 0 : 1; -this.updateStick(analog, 0, 0); -}; -handleMouseClick(data) { -let mouseButton; -if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton; -else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton]; -let key = { -code: "Mouse" + mouseButton -}; -if (!this.PRESET) return; -let buttonIndex = this.PRESET.mapping[key.code]; -if (typeof buttonIndex === "undefined") return; -this.pressButton(buttonIndex, data.pressed); -} -handleMouseMove(data) { -let preset = this.PRESET; -if (!preset) return; -let mouseMapTo = preset.mouse["mapTo"]; -if (mouseMapTo === 0) return; -this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId), this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50); -let deadzoneCounterweight = preset.mouse["deadzoneCounterweight"], x = data.movementX * preset.mouse["sensitivityX"], y = data.movementY * preset.mouse["sensitivityY"], length = this.vectorLength(x, y); -if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; -else if (length > 1.1) x *= 1.1 / length, y *= 1.1 / length; -let analog = mouseMapTo === 1 ? 0 : 1; -this.updateStick(analog, x, y); -} -handleMouseWheel(data) { -let code = ""; -if (data.vertical < 0) code = "ScrollUp"; -else if (data.vertical > 0) code = "ScrollDown"; -else if (data.horizontal < 0) code = "ScrollLeft"; -else if (data.horizontal > 0) code = "ScrollRight"; -if (!code) return !1; -if (!this.PRESET) return !1; -let key = { -code -}, buttonIndex = this.PRESET.mapping[key.code]; -if (typeof buttonIndex === "undefined") return !1; -if (this.prevWheelCode === null || this.prevWheelCode === key.code) this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId), this.pressButton(buttonIndex, !0); -return this.wheelStoppedTimeoutId = window.setTimeout(() => { -this.prevWheelCode = null, this.pressButton(buttonIndex, !1); -}, 20), !0; -} -async toggle(force) { -if (!this.initialized) return; -if (typeof force !== "undefined") this.enabled = force; -else this.enabled = !this.enabled; -if (this.enabled) try { -await document.body.requestPointerLock({ unadjustedMovement: !0 }); -} catch (e) { -document.body.requestPointerLock(), console.log(e); -} -else document.pointerLockElement && document.exitPointerLock(); -} -refreshPresetData() { -this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset, this.resetXcloudGamepads(); -} -waitForMouseData(showPopup) { -this.popup.toggleVisibility(showPopup); -} -onPollingModeChanged = (e) => { -let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none"; -this.popup.moveOffscreen(move); -}; -onDialogShown = () => { -document.pointerLockElement && document.exitPointerLock(); -}; -onPointerLockChange = () => { -if (document.pointerLockElement) this.start(); -else this.stop(); -}; -onPointerLockError = (e) => { -console.log(e), this.stop(); -}; -onPointerLockRequested = () => { -this.start(); -}; -onPointerLockExited = () => { -this.mouseDataProvider?.stop(); -}; -handleEvent(event) { -switch (event.type) { -case BxEvent.POINTER_LOCK_REQUESTED: -this.onPointerLockRequested(); -break; -case BxEvent.POINTER_LOCK_EXITED: -this.onPointerLockExited(); -break; -} -} -init() { -if (!STATES.browser.capabilities.mkb) { -this.initialized = !1; -return; -} -if (this.initialized = !0, this.refreshPresetData(), this.enabled = !1, AppInterface) this.mouseDataProvider = new WebSocketMouseDataProvider(this); -else this.mouseDataProvider = new PointerLockMouseDataProvider(this); -if (this.mouseDataProvider.init(), window.addEventListener("keydown", this.onKeyboardEvent), window.addEventListener("keyup", this.onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.on("dialog.shown", this.onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this); -else document.addEventListener("pointerlockchange", this.onPointerLockChange), document.addEventListener("pointerlockerror", this.onPointerLockError); -if (MkbPopup.getInstance().reset(), AppInterface) { -let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); -if (shortcutKey) { -let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` }); -Toast.show(msg, t("native-mkb"), { html: !0 }); -} -this.waitForMouseData(!1); -} else this.waitForMouseData(!0); -} -destroy() { -if (!this.initialized) return; -if (this.initialized = !1, this.isPolling = !1, this.enabled = !1, this.stop(), this.waitForMouseData(!1), document.exitPointerLock(), window.removeEventListener("keydown", this.onKeyboardEvent), window.removeEventListener("keyup", this.onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this); -else document.removeEventListener("pointerlockchange", this.onPointerLockChange), document.removeEventListener("pointerlockerror", this.onPointerLockError); -window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged); -} -start() { -if (!this.enabled) this.enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); -this.isPolling = !0, this.escKeyDownTime = -1, window.BX_EXPOSED.toggleLocalCoOp(getStreamPref("localCoOp.enabled")), this.resetXcloudGamepads(), window.navigator.getGamepads = this.patchedGetGamepads, this.waitForMouseData(!1), this.mouseDataProvider?.start(); -let virtualGamepad = this.getVirtualGamepad(); -virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", { -gamepad: virtualGamepad -}), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); -} -stop() { -this.enabled = !1, this.isPolling = !1, this.escKeyDownTime = -1; -let virtualGamepad = this.getVirtualGamepad(); -if (virtualGamepad.connected) this.resetXcloudGamepads(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", { -gamepad: virtualGamepad -}), window.navigator.getGamepads = this.nativeGetGamepads; -this.waitForMouseData(!0), this.mouseDataProvider?.stop(); -} -static setupEvents() {} -} -class StreamSettings { -static settings = { -settings: {}, -xCloudPollingMode: "all", -deviceVibrationIntensity: 0, -controllerPollingRate: 4, -controllers: {}, -mkbPreset: null, -keyboardShortcuts: {} -}; -static async refreshControllerSettings() { -let settings = StreamSettings.settings, controllers = {}, shortcutsTable = ControllerShortcutsTable.getInstance(), mappingTable = ControllerCustomizationsTable.getInstance(), gamepads = window.navigator.getGamepads(); -for (let gamepad of gamepads) { -if (!gamepad?.connected) continue; -if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; -let controllerSetting = STORAGE.Stream.getControllerSetting(gamepad.id), shortcutsPreset = await shortcutsTable.getPreset(controllerSetting.shortcutPresetId), shortcutsMapping = !shortcutsPreset ? null : shortcutsPreset.data.mapping, customizationPreset = await mappingTable.getPreset(controllerSetting.customizationPresetId), customizationData = StreamSettings.convertControllerCustomization(customizationPreset?.data); -controllers[gamepad.id] = { -shortcuts: shortcutsMapping, -customization: customizationData -}; -} -settings.controllers = controllers, settings.controllerPollingRate = getStreamPref("controller.pollingRate"), await StreamSettings.refreshDeviceVibration(); -} -static preCalculateControllerRange(obj, target, values) { -if (values && Array.isArray(values)) { -let [from, to] = values; -if (from > 1 || to < 100) obj[target] = [from / 100, to / 100]; -} -} -static convertControllerCustomization(customization) { -if (!customization) return null; -let converted = { -mapping: {}, -ranges: {}, -vibrationIntensity: 1 -}, gamepadKey; -for (gamepadKey in customization.mapping) { -let gamepadStr = toXcloudGamepadKey(gamepadKey); -if (!gamepadStr) continue; -let mappedKey = customization.mapping[gamepadKey]; -if (typeof mappedKey === "number") converted.mapping[gamepadStr] = toXcloudGamepadKey(mappedKey); -else converted.mapping[gamepadStr] = !1; -} -return StreamSettings.preCalculateControllerRange(converted.ranges, "LeftTrigger", customization.settings.leftTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "RightTrigger", customization.settings.rightTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "LeftThumb", customization.settings.leftStickDeadzone), StreamSettings.preCalculateControllerRange(converted.ranges, "RightThumb", customization.settings.rightStickDeadzone), converted.vibrationIntensity = customization.settings.vibrationIntensity / 100, converted; -} -static async refreshDeviceVibration() { -if (!STATES.browser.capabilities.deviceVibration) return; -let mode = getStreamPref("deviceVibration.mode"), intensity = 0; -if (mode === "on" || mode === "auto" && !hasGamepad()) intensity = getStreamPref("deviceVibration.intensity") / 100; -StreamSettings.settings.deviceVibrationIntensity = intensity, BxEventBus.Stream.emit("deviceVibration.updated", {}); -} -static async refreshMkbSettings() { -let settings = StreamSettings.settings, presetId = getStreamPref("mkb.p1.preset.mappingId"), orgPreset = await MkbMappingPresetsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data, converted = { -mapping: {}, -mouse: Object.assign({}, orgPresetData.mouse) -}, key; -for (key in orgPresetData.mapping) { -let buttonIndex = parseInt(key); -if (!orgPresetData.mapping[buttonIndex]) continue; -for (let keyName of orgPresetData.mapping[buttonIndex]) -if (typeof keyName === "string") converted.mapping[keyName] = buttonIndex; -} -let mouse = converted.mouse; -mouse["sensitivityX"] *= 0.001, mouse["sensitivityY"] *= 0.001, mouse["deadzoneCounterweight"] *= 0.01, settings.mkbPreset = converted, setStreamPref("mkb.p1.preset.mappingId", orgPreset.id, "direct"), BxEventBus.Stream.emit("mkb.setting.updated", {}); -} -static async refreshKeyboardShortcuts() { -let settings = StreamSettings.settings, presetId = getStreamPref("keyboardShortcuts.preset.inGameId"); -if (presetId === 0) { -settings.keyboardShortcuts = null, setStreamPref("keyboardShortcuts.preset.inGameId", presetId, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {}); -return; -} -let orgPreset = await KeyboardShortcutsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data.mapping, converted = {}, action; -for (action in orgPresetData) { -let info = orgPresetData[action], key = `${info.code}:${info.modifiers || 0}`; -converted[key] = action; -} -settings.keyboardShortcuts = converted, setStreamPref("keyboardShortcuts.preset.inGameId", orgPreset.id, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {}); -} -static async refreshAllSettings() { -window.BX_STREAM_SETTINGS = StreamSettings.settings, await StreamSettings.refreshControllerSettings(), await StreamSettings.refreshMkbSettings(), await StreamSettings.refreshKeyboardShortcuts(); -} -static findKeyboardShortcut(targetAction) { -let shortcuts = StreamSettings.settings.keyboardShortcuts; -for (let codeStr in shortcuts) -if (shortcuts[codeStr] === targetAction) return KeyHelper.parseFullKeyCode(codeStr); -return null; -} -static setup() { -let listener = () => { -StreamSettings.refreshControllerSettings(); -}; -window.addEventListener("gamepadconnected", listener), window.addEventListener("gamepaddisconnected", listener), StreamSettings.refreshAllSettings(); -} -} -class BxNumberStepper extends HTMLInputElement { -intervalId = null; -isHolding; -controlValue; -controlMin; -controlMax; -uiMin; -uiMax; -steps; -options; -onChange; -$text; -$btnInc; -$btnDec; -$range; -onRangeInput; -onClick; -onPointerUp; -onPointerDown; -setValue; -normalizeValue; -static create(key, value, min, max, options = {}, onChange) { -options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider; -let $text, $btnInc, $btnDec, $range, self = CE("div", { -class: "bx-number-stepper", -id: `bx_setting_${escapeCssSelector(key)}` -}, CE("div", !1, $btnDec = CE("button", { -_dataset: { -type: "dec" -}, -type: "button", -class: options.hideSlider ? "bx-focusable" : "", -tabindex: options.hideSlider ? 0 : -1 -}, "-"), $text = CE("span"), $btnInc = CE("button", { -_dataset: { -type: "inc" -}, -type: "button", -class: options.hideSlider ? "bx-focusable" : "", -tabindex: options.hideSlider ? 0 : -1 -}, "+"))); -if (self.$text = $text, self.$btnInc = $btnInc, self.$btnDec = $btnDec, self.onChange = onChange, self.onRangeInput = BxNumberStepper.onRangeInput.bind(self), self.onClick = BxNumberStepper.onClick.bind(self), self.onPointerUp = BxNumberStepper.onPointerUp.bind(self), self.onPointerDown = BxNumberStepper.onPointerDown.bind(self), self.controlMin = min, self.controlMax = max, self.isHolding = !1, self.options = options, self.uiMin = options.reverse ? -max : min, self.uiMax = options.reverse ? -min : max, self.steps = Math.max(options.steps || 1, 1), BxNumberStepper.setValue.call(self, value), options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), self.disabled = !0, self; -if ($range = CE("input", { -id: `bx_inp_setting_${key}`, -type: "range", -min: self.uiMin, -max: self.uiMax, -value: options.reverse ? -value : value, -step: self.steps, -tabindex: 0 -}), self.$range = $range, options.hideSlider && $range.classList.add("bx-gone"), self.addEventListener("input", self.onRangeInput), self.appendChild($range), options.ticks || options.exactTicks) { -let markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId }); -if ($range.setAttribute("list", markersId), options.exactTicks) { -let start = Math.max(Math.floor(min / options.exactTicks), 1) * options.exactTicks; -if (start === min) start += options.exactTicks; -for (let i = start;i < max; i += options.exactTicks) -$markers.appendChild(CE("option", { -value: options.reverse ? -i : i -})); -} else for (let i = self.uiMin + options.ticks;i < self.uiMax; i += options.ticks) -$markers.appendChild(CE("option", { value: i })); -self.appendChild($markers); -} -return BxNumberStepper.updateButtonsVisibility.call(self), self.addEventListener("click", self.onClick), self.addEventListener("pointerdown", self.onPointerDown), self.addEventListener("contextmenu", BxNumberStepper.onContextMenu), setNearby(self, { -focus: options.hideSlider ? $btnInc : $range -}), Object.defineProperty(self, "value", { -get() { -return self.controlValue; -}, -set(value2) { -BxNumberStepper.setValue.call(self, value2); -} -}), Object.defineProperty(self, "disabled", { -get() { -return $range.disabled; -}, -set(value2) { -$btnDec.disabled = value2, $btnInc.disabled = value2, $range.disabled = value2; -} -}), self; -} -static setValue(value) { -if (this.controlValue = BxNumberStepper.normalizeValue.call(this, value), this.$text.textContent = BxNumberStepper.updateTextValue.call(this), this.$range) this.$range.value = (this.options.reverse ? -this.controlValue : this.controlValue).toString(); -BxNumberStepper.updateButtonsVisibility.call(this); -} -static normalizeValue(value) { -return value = parseInt(value), value = Math.max(this.controlMin, value), value = Math.min(this.controlMax, value), value; -} -static onRangeInput(e) { -let value = parseInt(e.target.value); -if (this.options.reverse) value *= -1; -if (BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), !e.ignoreOnChange && this.onChange) this.onChange(e, value); -} -static onClick(e) { -if (e.preventDefault(), this.isHolding) return; -let $btn = e.target.closest("button"); -$btn && BxNumberStepper.buttonPressed.call(this, e, $btn), BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1; -} -static onPointerDown(e) { -BxNumberStepper.clearIntervalId.call(this); -let $btn = e.target.closest("button"); -if (!$btn) return; -this.isHolding = !0, e.preventDefault(), this.intervalId = window.setInterval((e2) => { -BxNumberStepper.buttonPressed.call(this, e2, $btn); -}, 200), window.addEventListener("pointerup", this.onPointerUp, { once: !0 }), window.addEventListener("pointercancel", this.onPointerUp, { once: !0 }); -} -static onPointerUp(e) { -BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1; -} -static onContextMenu(e) { -e.preventDefault(); -} -static updateTextValue() { -let value = this.controlValue, textContent = null; -if (this.options.customTextValue) textContent = this.options.customTextValue(value, this.controlMin, this.controlMax); -if (textContent === null) textContent = value.toString() + this.options.suffix; -return textContent; -} -static buttonPressed(e, $btn) { -BxNumberStepper.change.call(this, $btn.dataset.type); -} -static change(direction) { -let value = this.controlValue; -if (value = this.options.reverse ? -value : value, direction === "dec") value = Math.max(this.uiMin, value - this.steps); -else value = Math.min(this.uiMax, value + this.steps); -value = this.options.reverse ? -value : value, BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), this.onChange && this.onChange(null, this.controlValue); -} -static clearIntervalId() { -this.intervalId && clearInterval(this.intervalId), this.intervalId = null; -} -static updateButtonsVisibility() { -if (this.$btnDec.classList.toggle("bx-inactive", this.controlValue === this.uiMin), this.$btnInc.classList.toggle("bx-inactive", this.controlValue === this.uiMax), this.controlValue === this.uiMin || this.controlValue === this.uiMax) BxNumberStepper.clearIntervalId.call(this); -} -} -class SettingElement { -static renderOptions(key, setting, currentValue, onChange) { -let $control = CE("select", { -tabindex: 0 -}), $parent; -if (setting.optionsGroup) $parent = CE("optgroup", { -label: setting.optionsGroup -}), $control.appendChild($parent); -else $parent = $control; -for (let value in setting.options) { -let label = setting.options[value], $option = CE("option", { value }, label); -$parent.appendChild($option); -} -return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { -let target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value; -!e.ignoreOnChange && onChange(e, value); -}), $control.setValue = (value) => { -$control.value = value; -}, $control; -} -static renderMultipleOptions(key, setting, currentValue, onChange, params = {}) { -let $control = CE("select", { -multiple: !0, -tabindex: 0 -}), totalOptions = Object.keys(setting.multipleOptions).length, size = params.size ? Math.min(params.size, totalOptions) : totalOptions; -$control.setAttribute("size", size.toString()); -for (let value in setting.multipleOptions) { -let label = setting.multipleOptions[value], $option = CE("option", { value }, label); -$option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) { -e.preventDefault(); -let target = e.target; -target.selected = !target.selected; -let $parent = target.parentElement; -$parent.focus(), BxEvent.dispatch($parent, "input"); -}), $control.appendChild($option); -} -return $control.addEventListener("mousedown", function(e) { -let self = this, orgScrollTop = self.scrollTop; -window.setTimeout(() => self.scrollTop = orgScrollTop, 0); -}), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => { -let target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value); -!e.ignoreOnChange && onChange(e, values); -}), Object.defineProperty($control, "value", { -get() { -return Array.from($control.options).filter((option) => option.selected).map((option) => option.value); -}, -set(value) { -let values = value.split(","); -Array.from($control.options).forEach((option) => { -option.selected = values.includes(option.value); -}); -} -}), $control; -} -static renderCheckbox(key, setting, currentValue, onChange) { -let $control = CE("input", { type: "checkbox", tabindex: 0 }); -return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => { -!e.ignoreOnChange && onChange(e, e.target.checked); -}), $control.setValue = (value) => { -$control.checked = !!value; -}, $control; -} -static renderNumberStepper(key, setting, value, onChange, options = {}) { -return BxNumberStepper.create(key, value, setting.min, setting.max, options, onChange); -} -static METHOD_MAP = { -options: SettingElement.renderOptions, -"multiple-options": SettingElement.renderMultipleOptions, -"number-stepper": SettingElement.renderNumberStepper, -checkbox: SettingElement.renderCheckbox -}; -static render(type, key, setting, currentValue, onChange, options) { -let method = SettingElement.METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1)); -if (type !== "number-stepper") $control.id = `bx_setting_${escapeCssSelector(key)}`; -if (type === "options" || type === "multiple-options") $control.name = $control.id; -return $control; -} -static fromPref(key, onChange, overrideParams = {}) { -let { definition, storage } = getPrefInfo(key); -if (!definition) return null; -let currentValue = storage.getSetting(key), type; -if ("options" in definition) type = "options"; -else if ("multipleOptions" in definition) type = "multiple-options"; -else if (typeof definition.default === "number") type = "number-stepper"; -else type = "checkbox"; -let params = {}; -if ("params" in definition) params = Object.assign(overrideParams, definition.params || {}); -if (params.disabled) currentValue = definition.default; -return SettingElement.render(type, key, definition, currentValue, (e, value) => { -if (isGlobalPref(key)) setGlobalPref(key, value, "ui"); -else { -let id = SettingsManager.getInstance().getTargetGameId(); -setGamePref(id, key, value, "ui"); -} -onChange && onChange(e, value); -}, params); -} -} -class BxSelectElement extends HTMLSelectElement { -isControllerFriendly; -optionsList; -indicatorsList; -$indicators; -visibleIndex; -isMultiple; -$select; -$btnNext; -$btnPrev; -$label; -$checkBox; -static create($select, forceFriendly = !1) { -let isControllerFriendly = forceFriendly || getGlobalPref("ui.controllerFriendly"); -if ($select.multiple && !isControllerFriendly) return $select.classList.add("bx-select"), $select; -$select.removeAttribute("tabindex"); -let $wrapper = CE("div", { -class: "bx-select", -_dataset: { -controllerFriendly: isControllerFriendly -} -}); -if ($select.classList.contains("bx-full-width")) $wrapper.classList.add("bx-full-width"); -let $content, self = $wrapper; -self.isControllerFriendly = isControllerFriendly, self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = []; -let $btnPrev, $btnNext; -if (isControllerFriendly) { -$btnPrev = createButton({ -label: "<", -style: 64 -}), $btnNext = createButton({ -label: ">", -style: 64 -}), setNearby($wrapper, { -orientation: "horizontal", -focus: $btnNext -}), self.$btnNext = $btnNext, self.$btnPrev = $btnPrev; -let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self); -$btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext); -} else $select.addEventListener("change", (e) => { -self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); -}); -if (self.isMultiple) $content = CE("button", { -class: "bx-select-value bx-focusable", -tabindex: 0 -}, CE("div", !1, self.$checkBox = CE("input", { type: "checkbox" }), self.$label = CE("span", !1, "")), self.$indicators), $content.addEventListener("click", (e) => { -self.$checkBox.click(); -}), self.$checkBox.addEventListener("input", (e) => { -let $option = BxSelectElement.getOptionAtIndex.call(self, self.visibleIndex); -$option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); -}); -else $content = CE("div", !1, self.$label = CE("label", { for: $select.id + "_checkbox" }, ""), self.$indicators); -return $select.addEventListener("input", BxSelectElement.render.bind(self)), new MutationObserver((mutationList, observer2) => { -mutationList.forEach((mutation) => { -if (mutation.type === "childList" || mutation.type === "attributes") self.visibleIndex = $select.selectedIndex, self.optionsList = Array.from($select.querySelectorAll("option")), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); -}); -}).observe($select, { -subtree: !0, -childList: !0, -attributes: !0 -}), self.append($select, $btnPrev || "", $content, $btnNext || ""), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", { -get() { -return $select.value; -}, -set(value) { -self.optionsList = Array.from($select.querySelectorAll("option")), $select.value = value, self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); -} -}), Object.defineProperty(self, "disabled", { -get() { -return $select.disabled; -}, -set(value) { -$select.disabled = value; -} -}), self.addEventListener = function() { -$select.addEventListener.apply($select, arguments); -}, self.removeEventListener = function() { -$select.removeEventListener.apply($select, arguments); -}, self.dispatchEvent = function() { -return $select.dispatchEvent.apply($select, arguments); -}, self.appendChild = function(node) { -return $select.appendChild(node), node; -}, self; -} -static resetIndicators() { -let { -optionsList, -indicatorsList, -$indicators -} = this, targetSize = optionsList.length; -if (indicatorsList.length > targetSize) while (indicatorsList.length > targetSize) -indicatorsList.pop()?.remove(); -else if (indicatorsList.length < targetSize) while (indicatorsList.length < targetSize) { -let $indicator = CE("span", {}); -indicatorsList.push($indicator), $indicators.appendChild($indicator); -} -for (let $indicator of indicatorsList) -clearDataSet($indicator); -$indicators.classList.toggle("bx-invisible", targetSize <= 1); -} -static getOptionAtIndex(index) { -return this.optionsList[index]; -} -static render(e) { -let { -$label, -$btnNext, -$btnPrev, -$checkBox, -optionsList, -indicatorsList -} = this; -if (e && e.manualTrigger) this.visibleIndex = this.$select.selectedIndex; -this.visibleIndex = BxSelectElement.normalizeIndex.call(this, this.visibleIndex); -let $option = BxSelectElement.getOptionAtIndex.call(this, this.visibleIndex), content = ""; -if ($option) { -let $parent = $option.parentElement, hasLabel = $parent instanceof HTMLOptGroupElement || this.$select.querySelector("optgroup"); -if (content = $option.dataset.label || $option.textContent || "", content && hasLabel) { -let groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : " "; -$label.innerHTML = ""; -let fragment = document.createDocumentFragment(); -fragment.appendChild(CE("span", !1, groupLabel)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment); -} else $label.textContent = content; -} else $label.textContent = content; -if ($label.classList.toggle("bx-line-through", $option && $option.disabled), this.isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); -let disableButtons = optionsList.length <= 1; -$btnPrev?.classList.toggle("bx-gone", disableButtons), $btnNext?.classList.toggle("bx-gone", disableButtons); -for (let i = 0;i < optionsList.length; i++) { -let $option2 = optionsList[i], $indicator = indicatorsList[i]; -if (!$option2 || !$indicator) continue; -if (clearDataSet($indicator), $option2.selected) $indicator.dataset.selected = "true"; -if ($option2.index === this.visibleIndex) $indicator.dataset.highlighted = "true"; -} -} -static normalizeIndex(index) { -return Math.min(Math.max(index, 0), this.optionsList.length - 1); -} -static onPrevNext(e) { -if (!e.target) return; -let { -$btnNext, -$select, -isMultiple, -visibleIndex: currentIndex -} = this, newIndex = e.target.closest("button") === $btnNext ? currentIndex + 1 : currentIndex - 1; -if (newIndex > this.optionsList.length - 1) newIndex = 0; -else if (newIndex < 0) newIndex = this.optionsList.length - 1; -if (newIndex = BxSelectElement.normalizeIndex.call(this, newIndex), this.visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; -if (isMultiple) BxSelectElement.render.call(this); -else BxEvent.dispatch($select, "input"); -} -} -class XboxApi { -static CACHED_TITLES = {}; -static async getProductTitle(xboxTitleId) { -if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId]; -try { -let url = `https://displaycatalog.mp.microsoft.com/v7.0/products/lookup?market=US&languages=en&value=${xboxTitleId}&alternateId=XboxTitleId&fieldsTemplate=browse`, productTitle = (await (await NATIVE_FETCH(url)).json()).Products[0].LocalizedProperties[0].ProductTitle; -return XboxApi.CACHED_TITLES[xboxTitleId] = productTitle, productTitle; -} catch (e) {} -return; -} -} -class SettingsManager { -static instance; -static getInstance = () => SettingsManager.instance ?? (SettingsManager.instance = new SettingsManager); -$streamSettingsSelection; -$tips; -playingGameId = -1; -targetGameId = -1; -SETTINGS = { -"localCoOp.enabled": { -onChange: () => { -BxExposed.toggleLocalCoOp(getStreamPref("localCoOp.enabled")); -} -}, -"deviceVibration.mode": { -onChange: StreamSettings.refreshControllerSettings -}, -"deviceVibration.intensity": { -onChange: StreamSettings.refreshControllerSettings -}, -"controller.pollingRate": { -onChange: StreamSettings.refreshControllerSettings -}, -"controller.settings": { -onChange: StreamSettings.refreshControllerSettings -}, -"nativeMkb.scroll.sensitivityX": { -onChange: () => { -let value = getStreamPref("nativeMkb.scroll.sensitivityX"); -NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100); -} -}, -"nativeMkb.scroll.sensitivityY": { -onChange: () => { -let value = getStreamPref("nativeMkb.scroll.sensitivityY"); -NativeMkbHandler.getInstance()?.setVerticalScrollMultiplier(value / 100); -} -}, -"video.player.type": { -onChange: () => { -if (onChangeVideoPlayerType(), STATES.isPlaying) updateVideoPlayer(); -}, -alwaysTriggerOnChange: !0 -}, -"video.player.powerPreference": { -onChange: () => { -let streamPlayer = STATES.currentStream.streamPlayer; -if (!streamPlayer) return; -streamPlayer.reloadPlayer(), updateVideoPlayer(); -} -}, -"video.processing": { -onChange: updateVideoPlayer -}, -"video.processing.sharpness": { -onChange: updateVideoPlayer -}, -"video.maxFps": { -onChange: () => { -let value = getStreamPref("video.maxFps"); -limitVideoPlayerFps(value); -} -}, -"video.ratio": { -onChange: updateVideoPlayer -}, -"video.brightness": { -onChange: updateVideoPlayer -}, -"video.contrast": { -onChange: updateVideoPlayer -}, -"video.saturation": { -onChange: updateVideoPlayer -}, -"video.position": { -onChange: updateVideoPlayer -}, -"audio.volume": { -onChange: () => { -let value = getStreamPref("audio.volume"); -SoundShortcut.setGainNodeVolume(value); -} -}, -"stats.items": { -onChange: StreamStats.refreshStyles -}, -"stats.quickGlance.enabled": { -onChange: () => { -let value = getStreamPref("stats.quickGlance.enabled"), streamStats = StreamStats.getInstance(); -value ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); -} -}, -"stats.position": { -onChange: StreamStats.refreshStyles -}, -"stats.textSize": { -onChange: StreamStats.refreshStyles -}, -"stats.opacity.all": { -onChange: StreamStats.refreshStyles -}, -"stats.opacity.background": { -onChange: StreamStats.refreshStyles -}, -"stats.colors": { -onChange: StreamStats.refreshStyles -}, -"mkb.p1.preset.mappingId": { -onChange: StreamSettings.refreshMkbSettings -}, -"mkb.p1.slot": { -onChange: () => { -EmulatedMkbHandler.getInstance()?.resetXcloudGamepads(); -} -}, -"keyboardShortcuts.preset.inGameId": { -onChange: StreamSettings.refreshKeyboardShortcuts -} -}; -constructor() { -BxEventBus.Stream.on("setting.changed", (data) => { -if (isStreamPref(data.settingKey)) this.updateStreamElement(data.settingKey); -}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => { -this.switchGameSettings(id); -}), this.renderStreamSettingsSelection(); -} -updateStreamElement(key, onChanges) { -let info = this.SETTINGS[key]; -if (info.onChange && (STATES.isPlaying || info.alwaysTriggerOnChange)) if (onChanges) onChanges.add(info.onChange); -else info.onChange(); -let $elm = info.$element; -if (!$elm) return; -let value = getGamePref(this.targetGameId, key, !0); -if ("setValue" in $elm) $elm.setValue(value); -else $elm.value = value.toString(); -this.updateDataset($elm, key); -} -switchGameSettings(id) { -if (setGameIdPref(id), this.targetGameId === id) return; -let onChanges = new Set, oldGameId = this.targetGameId; -this.targetGameId = id; -let key; -for (key in this.SETTINGS) { -if (!isStreamPref(key)) continue; -let oldValue = getGamePref(oldGameId, key, !0, !0), newValue = getGamePref(this.targetGameId, key, !0, !0); -if (oldValue === newValue) continue; -this.updateStreamElement(key, onChanges); -} -onChanges.forEach((onChange) => { -onChange && onChange(); -}), this.$tips.classList.toggle("bx-gone", id < 0); -} -setElement(pref, $elm) { -if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {}; -this.updateDataset($elm, pref), this.SETTINGS[pref].$element = $elm; -} -getElement(pref, params) { -if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {}; -let $elm = this.SETTINGS[pref].$element; -if (!$elm) $elm = SettingElement.fromPref(pref, null, params), this.SETTINGS[pref].$element = $elm; -return this.updateDataset($elm, pref), $elm; -} -hasElement(pref) { -return !!this.SETTINGS[pref]?.$element; -} -updateDataset($elm, pref) { -if (this.targetGameId === this.playingGameId && hasGamePref(this.playingGameId, pref)) $elm.dataset.override = "true"; -else delete $elm.dataset.override; -} -renderStreamSettingsSelection() { -this.$tips = CE("p", { class: "bx-gone" }, `⇐ Q ⟶: ${t("reset-highlighted-setting")}`); -let $select = BxSelectElement.create(CE("select", !1, CE("optgroup", { label: t("settings-for") }, CE("option", { value: -1 }, t("all-games")))), !0); -$select.addEventListener("input", (e) => { -let id = parseInt($select.value); -BxEventBus.Stream.emit("gameSettings.switched", { id }); -}), this.$streamSettingsSelection = CE("div", { -class: "bx-stream-settings-selection bx-gone", -_nearby: { orientation: "vertical" } -}, CE("div", !1, $select), this.$tips), BxEventBus.Stream.on("xboxTitleId.changed", async ({ id }) => { -this.playingGameId = id, setGameIdPref(id); -let $optGroup = $select.querySelector("optgroup"); -while ($optGroup.childElementCount > 1) -$optGroup.lastElementChild?.remove(); -if (id >= 0) { -let title = id === 0 ? "Xbox" : await XboxApi.getProductTitle(id); -$optGroup.appendChild(CE("option", { -value: id -}, title)), $select.value = id.toString(); -} else $select.value = "-1"; -BxEventBus.Stream.emit("gameSettings.switched", { id }); -}); -} -getStreamSettingsSelection() { -return this.$streamSettingsSelection; -} -getTargetGameId() { -return this.targetGameId; -} -} -function onChangeVideoPlayerType() { -let playerType = getStreamPref("video.player.type"), settingsManager = SettingsManager.getInstance(); -if (!settingsManager.hasElement("video.processing")) return; -let isDisabled = !1, $videoProcessing = settingsManager.getElement("video.processing"), $videoSharpness = settingsManager.getElement("video.processing.sharpness"), $videoPowerPreference = settingsManager.getElement("video.player.powerPreference"), $videoMaxFps = settingsManager.getElement("video.maxFps"), $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); -if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); -else if ($videoProcessing.value = "usm", setStreamPref("video.processing", "usm", "direct"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; -$videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"); -} -function limitVideoPlayerFps(targetFps) { -STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps); -} -function updateVideoPlayer() { -let streamPlayer = STATES.currentStream.streamPlayer; -if (!streamPlayer) return; -limitVideoPlayerFps(getStreamPref("video.maxFps")); -let options = { -processing: getStreamPref("video.processing"), -sharpness: getStreamPref("video.processing.sharpness"), -saturation: getStreamPref("video.saturation"), -contrast: getStreamPref("video.contrast"), -brightness: getStreamPref("video.brightness") -}; -streamPlayer.setPlayerType(getStreamPref("video.player.type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); -} -window.addEventListener("resize", updateVideoPlayer); -class NavigationDialog { -dialogManager; -onMountedCallbacks = []; -constructor() { -this.dialogManager = NavigationDialogManager.getInstance(); -} -isCancellable() { -return !0; -} -isOverlayVisible() { -return !0; -} -show(configs = {}, clearStack = !1) { -if (NavigationDialogManager.getInstance().show(this, configs, clearStack), !this.getFocusedElement()) this.focusIfNeeded(); -} -hide() { -NavigationDialogManager.getInstance().hide(); -} -getFocusedElement() { -let $activeElement = document.activeElement; -if (!$activeElement) return null; -if (this.$container.contains($activeElement)) return $activeElement; -return null; -} -onBeforeMount(configs = {}) {} -onMounted(configs = {}) { -for (let callback of this.onMountedCallbacks) -callback.call(this); -} -onBeforeUnmount() {} -onUnmounted() {} -handleKeyPress(key) { -return !1; -} -handleGamepad(button) { -return !1; -} -} -class NavigationDialogManager { -static instance; -static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager); -LOG_TAG = "NavigationDialogManager"; -static GAMEPAD_POLLING_INTERVAL = 50; -static GAMEPAD_KEYS = [ -0, -1, -2, -3, -12, -15, -13, -14, -4, -5, -6, -7, -10, -11, -8, -9 -]; -static GAMEPAD_DIRECTION_MAP = { -12: 1, -13: 3, -14: 4, -15: 2, -100: 1, -101: 3, -102: 4, -103: 2 -}; -static SIBLING_PROPERTY_MAP = { -horizontal: { -4: "previousElementSibling", -2: "nextElementSibling" -}, -vertical: { -1: "previousElementSibling", -3: "nextElementSibling" -} -}; -gamepadPollingIntervalId = null; -gamepadLastStates = []; -gamepadHoldingIntervalId = null; -$overlay; -$container; -dialog = null; -dialogsStack = []; -constructor() { -BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => { -e.preventDefault(), e.stopPropagation(), this.dialog?.isCancellable() && this.hide(); -}), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), new MutationObserver((mutationList) => { -if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return; -let $dialog = mutationList[0].addedNodes[0]; -if (!$dialog || !($dialog instanceof HTMLElement)) return; -calculateSelectBoxes($dialog); -}).observe(this.$container, { childList: !0 }); -} -updateActiveInput(input) { -document.documentElement.dataset.activeInput = input; -} -handleEvent(event) { -switch (event.type) { -case "keydown": -this.updateActiveInput("keyboard"); -let $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key, handled = this.dialog?.handleKeyPress(keyCode); -if (handled) { -event.preventDefault(), event.stopPropagation(); -return; -} -if (keyCode === "ArrowUp" || keyCode === "ArrowDown") handled = !0, this.focusDirection(keyCode === "ArrowUp" ? 1 : 3); -else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") { -if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2); -} else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") { -if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 })); -} else if (keyCode === "Escape") handled = !0, this.hide(); -if (handled) event.preventDefault(), event.stopPropagation(); -break; -} -} -isShowing() { -return this.$container && !this.$container.classList.contains("bx-gone"); -} -pollGamepad = () => { -let gamepads = window.navigator.getGamepads(); -for (let gamepad of gamepads) { -if (!gamepad || !gamepad.connected) continue; -if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; -let { axes, buttons } = gamepad, releasedButton = null, heldButton = null, lastState = this.gamepadLastStates[gamepad.index], lastTimestamp, lastKey, lastKeyPressed; -if (lastState) [lastTimestamp, lastKey, lastKeyPressed] = lastState; -if (lastTimestamp && lastTimestamp === gamepad.timestamp) continue; -for (let key of NavigationDialogManager.GAMEPAD_KEYS) -if (lastKey === key && !buttons[key].pressed) { -releasedButton = key; -break; -} else if (buttons[key].pressed) { -heldButton = key; -break; -} -if (heldButton === null && releasedButton === null && axes && axes.length >= 2) { -if (lastKey) { -let releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastKey === 102 || lastKey === 103), releasedVertical = Math.abs(axes[1]) < 0.1 && (lastKey === 100 || lastKey === 101); -if (releasedHorizontal || releasedVertical) releasedButton = lastKey; -else heldButton = lastKey; -} else if (axes[0] < -0.5) heldButton = 102; -else if (axes[0] > 0.5) heldButton = 103; -else if (axes[1] < -0.5) heldButton = 100; -else if (axes[1] > 0.5) heldButton = 101; -} -if (heldButton !== null) { -if (this.gamepadLastStates[gamepad.index] = [gamepad.timestamp, heldButton, !1], this.clearGamepadHoldingInterval(), NavigationDialogManager.GAMEPAD_DIRECTION_MAP[heldButton]) this.gamepadHoldingIntervalId = window.setInterval(() => { -let lastState2 = this.gamepadLastStates[gamepad.index]; -if (lastState2) { -if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) { -this.handleGamepad(gamepad, heldButton); -return; -} -} -this.clearGamepadHoldingInterval(); -}, 100); -continue; -} -if (releasedButton === null) { -this.clearGamepadHoldingInterval(); -continue; -} -if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return; -if (this.updateActiveInput("gamepad"), this.handleGamepad(gamepad, releasedButton)) return; -if (releasedButton === 0) { -document.activeElement?.dispatchEvent(new MouseEvent("click", { bubbles: !0 })); -return; -} else if (releasedButton === 1) { -this.hide(); -return; -} -} -}; -handleGamepad(gamepad, key) { -let handled = this.dialog?.handleGamepad(key); -if (handled) return !0; -let direction = NavigationDialogManager.GAMEPAD_DIRECTION_MAP[key]; -if (!direction) return !1; -if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === "range") { -let $range = document.activeElement; -if (direction === 4 || direction === 2) { -let $numberStepper = $range.closest(".bx-number-stepper"); -if ($numberStepper) BxNumberStepper.change.call($numberStepper, direction === 4 ? "dec" : "inc"); -else $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === 4 ? -1 : 1)).toString(), $range.dispatchEvent(new InputEvent("input")); -handled = !0; -} -} -if (!handled) this.focusDirection(direction); -return this.gamepadLastStates[gamepad.index] && (this.gamepadLastStates[gamepad.index][2] = !0), !0; -} -clearGamepadHoldingInterval() { -this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = null; -} -show(dialog, configs = {}, clearStack = !1) { -this.clearGamepadHoldingInterval(), BxEventBus.Script.emit("dialog.shown", {}), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.unmountCurrentDialog(), this.dialogsStack.push(dialog), this.dialog = dialog, dialog.onBeforeMount(configs), this.$container.appendChild(dialog.getContent()), dialog.onMounted(configs), this.$overlay.classList.remove("bx-gone"), this.$overlay.classList.toggle("bx-invisible", !dialog.isOverlayVisible()), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling(); -} -hide() { -if (this.clearGamepadHoldingInterval(), !this.isShowing()) return; -if (document.body.classList.remove("bx-no-scroll"), BxEventBus.Script.emit("dialog.dismissed", {}), this.$overlay.classList.add("bx-gone"), this.$overlay.classList.remove("bx-invisible"), this.$container.classList.add("bx-gone"), this.$container.removeEventListener("keydown", this), this.stopGamepadPolling(), this.dialog) { -let dialogIndex = this.dialogsStack.indexOf(this.dialog); -if (dialogIndex > -1) this.dialogsStack = this.dialogsStack.slice(0, dialogIndex); -} -if (this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1, this.dialogsStack.length) this.dialogsStack[this.dialogsStack.length - 1].show(); -} -focus($elm) { -if (!$elm) return !1; -if ($elm.nearby && $elm.nearby.focus) if ($elm.nearby.focus instanceof HTMLElement) return this.focus($elm.nearby.focus); -else return $elm.nearby.focus(); -return $elm.focus(), $elm === document.activeElement; -} -getOrientation($elm) { -let nearby = $elm.nearby || {}; -if (nearby.selfOrientation) return nearby.selfOrientation; -let orientation, $current = $elm.parentElement; -while ($current !== this.$container) { -let tmp = $current.nearby?.orientation; -if ($current.nearby && tmp) { -orientation = tmp; -break; -} -$current = $current.parentElement; -} -return orientation = orientation || "vertical", setNearby($elm, { -selfOrientation: orientation -}), orientation; -} -findNextTarget($focusing, direction, checkParent = !1, checked = []) { -if (!$focusing || $focusing === this.$container) return null; -if (checked.includes($focusing)) return null; -checked.push($focusing); -let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target); -if (nearby[1] && direction === 1) return nearby[1]; -else if (nearby[3] && direction === 3) return nearby[3]; -else if (nearby[4] && direction === 4) return nearby[4]; -else if (nearby[2] && direction === 2) return nearby[2]; -let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction]; -if (siblingProperty) { -let $sibling = $target; -while ($sibling[siblingProperty]) { -$sibling = $sibling[siblingProperty]; -let $focusable = this.findFocusableElement($sibling, direction); -if ($focusable) return $focusable; -} -} -if (nearby.loop) { -if (nearby.loop(direction)) return null; -} -if (checkParent) return this.findNextTarget($parent, direction, checkParent, checked); -return null; -} -findFocusableElement($elm, direction) { -if (!$elm) return null; -if (!!$elm.disabled) return null; -if (!isElementVisible($elm)) return null; -if ($elm.tabIndex > -1) return $elm; -let focus = $elm.nearby?.focus; -if (focus) { -if (focus instanceof HTMLElement) return this.findFocusableElement(focus, direction); -else if (typeof focus === "function") { -if (focus()) return document.activeElement; -} -} -let children = Array.from($elm.children), orientation = $elm.nearby?.orientation || "vertical"; -if (orientation === "horizontal" || orientation === "vertical" && direction === 1) children.reverse(); -for (let $child of children) { -if (!$child || !($child instanceof HTMLElement)) return null; -let $target = this.findFocusableElement($child, direction); -if ($target) return $target; -} -return null; -} -startGamepadPolling() { -this.stopGamepadPolling(); -} -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"); -} -} -class SuggestionsSetting { -static async renderSuggestions(e) { -let $btnSuggest = e.target.closest("div"); -$btnSuggest.toggleAttribute("bx-open"); -let $content = $btnSuggest.nextElementSibling; -if ($content) { -BxEvent.dispatch($content.querySelector("select"), "input"); -return; -} -let settingTabGroup; -for (settingTabGroup in this.SETTINGS_UI) { -let settingTab = this.SETTINGS_UI[settingTabGroup]; -if (!settingTab || !settingTab.items || typeof settingTab.items === "function") continue; -for (let settingTabContent of settingTab.items) { -if (!settingTabContent || settingTabContent instanceof HTMLElement || !settingTabContent.items) continue; -for (let setting of settingTabContent.items) { -let prefKey; -if (typeof setting === "string") prefKey = setting; -else if (typeof setting === "object") prefKey = setting.pref; -if (prefKey) this.settingLabels[prefKey] = settingTabContent.label; -} -} -} -let recommendedDevice = ""; -if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) { -if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await SuggestionsSetting.getRecommendedSettings.call(this, BX_FLAGS.DeviceInfo.androidInfo); -} -let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType; -if (deviceType === "android-handheld") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"), SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "on"); -else if (deviceType === "android") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "auto"); -else if (deviceType === "android-tv") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"); -SuggestionsSetting.generateDefaultSuggestedSettings.call(this); -let $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", !1, hasRecommendedSettings && CE("option", { value: "recommended" }, t("recommended")), !hasRecommendedSettings && CE("option", { value: "highest" }, t("highest-quality")), CE("option", { value: "default" }, t("default")), CE("option", { value: "lowest" }, t("lowest-quality"))); -$select.addEventListener("input", (e2) => { -let profile = $select.value; -removeChildElements($suggestedSettings); -let fragment = document.createDocumentFragment(), note; -if (profile === "recommended") note = t("recommended-settings-for-device", { device: recommendedDevice }); -else if (profile === "highest") note = "⚠️ " + t("highest-quality-note"); -note && fragment.appendChild(CE("div", { class: "bx-suggest-note" }, note)); -let settings = this.suggestedSettings[profile]; -for (let key in settings) { -let { storage, definition } = getPrefInfo(key), prefKey; -if (storage === STORAGE.Stream) prefKey = key; -else prefKey = key; -let suggestedValue; -if (definition && definition.transformValue) suggestedValue = definition.transformValue.get.call(definition, settings[prefKey]); -else suggestedValue = settings[prefKey]; -let currentValue = storage.getSetting(prefKey, !1), currentValueText = storage.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue, $child, $value; -if (isSameValue) $value = currentValueText; -else { -let suggestedValueText = storage.getValueText(prefKey, suggestedValue); -$value = currentValueText + " ➔ " + suggestedValueText; -} -let $checkbox, breadcrumb = this.settingLabels[prefKey] + " ❯ " + storage.getLabel(prefKey), id = escapeCssSelector(`bx_suggest_${prefKey}`); -if ($child = CE("div", { -class: `bx-suggest-row ${isSameValue ? "bx-suggest-ok" : "bx-suggest-change"}` -}, $checkbox = CE("input", { -type: "checkbox", -tabindex: 0, -checked: !0, -id -}), CE("label", { -for: id -}, CE("div", { -class: "bx-suggest-label" -}, breadcrumb), CE("div", { -class: "bx-suggest-value" -}, $value))), isSameValue) -$checkbox.disabled = !0, $checkbox.checked = !0; -fragment.appendChild($child); -} -$suggestedSettings.appendChild(fragment); -}), BxEvent.dispatch($select, "input"); -let onClickApply = () => { -let profile = $select.value, settings = this.suggestedSettings[profile], prefKey, settingsManager = SettingsManager.getInstance(); -for (prefKey in settings) { -let suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${escapeCssSelector(prefKey)}`); -if (!$checkBox.checked || $checkBox.disabled) continue; -let $control = settingsManager.getElement(prefKey); -if (!$control) { -setPref(prefKey, suggestedValue, "direct"); -continue; -} -let { definition: settingDefinition } = getPrefInfo(prefKey); -if (settingDefinition?.transformValue) suggestedValue = settingDefinition.transformValue.get.call(settingDefinition, suggestedValue); -if ("setValue" in $control) $control.setValue(suggestedValue); -else $control.value = suggestedValue; -BxEvent.dispatch($control, "input", { -manualTrigger: !0 -}); -} -BxEvent.dispatch($select, "input"); -}, $btnApply = createButton({ -label: t("apply"), -style: 128 | 64, -onClick: onClickApply -}); -$content = CE("div", { -class: "bx-sub-content-box bx-suggest-box", -_nearby: { -orientation: "vertical" -} -}, BxSelectElement.create($select), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", { -class: "bx-suggest-link bx-focusable", -href: "https://better-xcloud.github.io/guide/android-webview-tweaks/", -target: "_blank", -tabindex: 0 -}, "🤓 " + t("how-to-improve-app-performance")), BX_FLAGS.DeviceInfo.deviceType.includes("android") && !hasRecommendedSettings && CE("a", { -class: "bx-suggest-link bx-focusable", -href: "https://github.com/redphx/better-xcloud-devices", -target: "_blank", -tabindex: 0 -}, t("suggest-settings-link"))), $btnSuggest.insertAdjacentElement("afterend", $content); -} -static async getRecommendedSettings(androidInfo) { -function normalize(str) { -return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-"); -} -try { -let { brand, board, model } = androidInfo; -brand = normalize(brand), board = normalize(board), model = normalize(model); -let url = GhPagesUtils.getUrl(`devices/${brand}/${board}-${model}.json`), json = await (await NATIVE_FETCH(url)).json(), recommended = {}; -if (json.schema_version !== 2) return null; -let scriptSettings = json.settings.script; -if (scriptSettings._base) { -let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base; -for (let profile of base) -Object.assign(recommended, this.suggestedSettings[profile]); -delete scriptSettings._base; -} -let key; -for (key in scriptSettings) -recommended[key] = scriptSettings[key]; -return BX_FLAGS.DeviceInfo.deviceType = json.device_type, this.suggestedSettings.recommended = recommended, json.device_name; -} catch (e) {} -return null; -} -static addDefaultSuggestedSetting(prefKey, value) { -let key; -for (key in this.suggestedSettings) -if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value; -} -static generateDefaultSuggestedSettings() { -let key; -for (key in this.suggestedSettings) { -if (key === "default") continue; -let prefKey; -for (prefKey in this.suggestedSettings[key]) -if (!(prefKey in this.suggestedSettings.default)) this.suggestedSettings.default[prefKey] = getPrefInfo(prefKey).definition.default; -} -} -} -class 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" -] -}, !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 = getGlobalPref("nativeMkb.mode"); -if (nativeMkbMode !== "default") FeatureGates.EnableMouseAndKeyboard = nativeMkbMode === "on"; -var blockFeatures = getGlobalPref("block.features"); -if (blockFeatures.includes("chat")) FeatureGates.EnableGuideChatTab = !1; -if (blockFeatures.includes("friends")) FeatureGates.EnableFriendsAndFollowers = !1; -if (blockFeatures.includes("byog")) FeatureGates.EnableBYOG = !1, FeatureGates.EnableBYOGPurchase = !1; -if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates); -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: () => {} -}; -function getPreferredServerRegion(shortName = !1) { -let preferredRegion = getGlobalPref("server.region"), serverRegions = STATES.serverRegions; -if (preferredRegion in serverRegions) if (shortName && serverRegions[preferredRegion].shortName) return serverRegions[preferredRegion].shortName; -else return preferredRegion; -for (let regionName in serverRegions) { -let region = serverRegions[regionName]; -if (!region.isDefault) continue; -if (shortName && region.shortName) return region.shortName; -else return regionName; -} -return null; -} -class HeaderSection { -static instance; -static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection); -LOG_TAG = "HeaderSection"; -$btnRemotePlay; -$btnSettings; -$buttonsWrapper; -observer; -timeoutId; -constructor() { -BxLogger.info(this.LOG_TAG, "constructor()"), this.$btnRemotePlay = 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), 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 $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()); -}); -} -} -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(), 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"); -} -function clearDbLogs(dbName, table) { -let request = window.indexedDB.open(dbName); -request.onsuccess = (e) => { -let db = e.target.result; -try { -let objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear(); -objectStoreRequest.onsuccess = () => BxLogger.info("clearDbLogs", `Cleared ${dbName}.${table}`); -} catch (ex) {} -}; -} -function clearAllLogs() { -clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs"); -} -function updateIceCandidates(candidates, options) { -let pattern = new RegExp(/a=candidate:(?\d+) (?\d+) UDP (?\d+) (?[^\s]+) (?\d+) (?.*)/), lst = []; -for (let 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: 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 (!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 } -} -} -}; -} -function getOsNameFromResolution(resolution) { -let osName; -switch (resolution) { -case "1080p-hq": -osName = "tizen"; -break; -case "1080p": -osName = "windows"; -break; -default: -osName = "android"; -break; -} -return osName; -} -function addCss() { -let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-warning-button-color:#c16e04;--bx-warning-button-rgb:193,110,4;--bx-warning-button-hover-color:#fa9005;--bx-warning-button-hover-rgb:250,144,5;--bx-warning-button-active-color:#965603;--bx-warning-button-active-rgb:150,86,3;--bx-warning-button-disabled-color:#a2816c;--bx-warning-button-disabled-rgb:162,129,108;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#bd8282;--bx-danger-button-disabled-rgb:189,130,130;--bx-fullscreen-text-z-index:9999;--bx-toast-z-index:6000;--bx-key-binding-dialog-z-index:5010;--bx-key-binding-dialog-overlay-z-index:5000;--bx-stats-bar-z-index:4010;--bx-navigation-dialog-z-index:3010;--bx-navigation-dialog-overlay-z-index:3000;--bx-mkb-pointer-lock-msg-z-index:2000;--bx-game-bar-z-index:1000;--bx-screenshot-animation-z-index:200;--bx-wait-time-box-z-index:100}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf");unicode-range:U+2196-E011,U+27F6,U+FF31}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-auto-height{height:auto !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font) !important}.bx-monospaced{font-family:var(--bx-monospaced-font) !important}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}.bx-frosted{backdrop-filter:blur(4px) brightness(1.5)}select[multiple],select[multiple]:focus{overflow:auto;border:none}select[multiple] option,select[multiple]:focus option{padding:4px 6px}select[multiple] option:checked,select[multiple]:focus option:checked{background:#1a7bc0 linear-gradient(0deg,#1a7bc0 0%,#1a7bc0 100%)}select[multiple] option:checked::before,select[multiple]:focus option:checked::before{content:\'☑️\';font-size:12px;display:inline-block;margin-right:6px;height:100%;line-height:100%;vertical-align:middle}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.5);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-game-tile-wait-time[data-duration=short]{background-color:rgba(0,133,133,0.75)}.bx-game-tile-wait-time[data-duration=medium]{background-color:rgba(213,133,0,0.75)}.bx-game-tile-wait-time[data-duration=long]{background-color:rgba(150,0,0,0.75)}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-blink-me{animation:bx-blinker 1s linear infinite}.bx-horizontal-shaking{animation:bx-horizontal-shaking .4s ease-in-out 2}@-moz-keyframes bx-blinker{100%{opacity:0}}@-webkit-keyframes bx-blinker{100%{opacity:0}}@-o-keyframes bx-blinker{100%{opacity:0}}@keyframes bx-blinker{100%{opacity:0}}@-moz-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-webkit-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-o-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb));opacity:.5}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-warning{--button-rgb:var(--bx-warning-button-rgb)}.bx-button.bx-warning:not([disabled]):active{--button-active-rgb:var(--bx-warning-button-active-rgb)}.bx-button.bx-warning:not([disabled]):not(:active):hover,.bx-button.bx-warning:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-warning-button-hover-rgb)}.bx-button.bx-warning:disabled{--button-disabled-rgb:var(--bx-warning-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha))}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);width:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-inline-start:8px}.bx-button.bx-button-multi-lines{height:auto;text-align:left;padding:10px}.bx-button.bx-button-multi-lines span{line-height:unset;display:block}.bx-button.bx-button-multi-lines span:last-of-type{text-transform:none;font-weight:normal;font-family:"Segoe Sans Variable Text";font-size:12px;margin-top:4px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-key-binding-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-key-binding-dialog-overlay-z-index);background:#000;opacity:50%}.bx-key-binding-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:16px;border-radius:8px;z-index:var(--bx-key-binding-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-key-binding-dialog *:focus{outline:none !important}.bx-key-binding-dialog h2{margin-bottom:12px;color:#fff;display:block;font-family:var(--bx-title-font);font-size:32px;font-weight:400;line-height:var(--bx-button-height)}.bx-key-binding-dialog > div{overflow:auto;padding:2px 0}.bx-key-binding-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-key-binding-dialog > button:hover{background-color:#515863}}.bx-key-binding-dialog > button:focus{background-color:#515863}.bx-key-binding-dialog ul{margin-bottom:1rem}.bx-key-binding-dialog ul li{display:none}.bx-key-binding-dialog ul[data-flags*="[1]"] > li[data-flag="1"],.bx-key-binding-dialog ul[data-flags*="[2]"] > li[data-flag="2"],.bx-key-binding-dialog ul[data-flags*="[4]"] > li[data-flag="4"],.bx-key-binding-dialog ul[data-flags*="[8]"] > li[data-flag="8"]{display:list-item}@media screen and (max-width:450px){.bx-key-binding-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog select:disabled{-webkit-appearance:none;text-align-last:right;text-align:right;color:#fff;background:#131416;border:none;border-radius:4px;padding:0 5px}.bx-navigation-dialog .bx-focusable::after{border-radius:4px}.bx-navigation-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-centered-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;min-width:min(calc(100vw - 20px), 500px);max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px;max-height:95vh;flex-direction:column;overflow:hidden;display:flex;flex-direction:column}.bx-centered-dialog .bx-dialog-title{display:flex;flex-direction:row;align-items:center;margin-bottom:10px}.bx-centered-dialog .bx-dialog-title p{padding:0;margin:0;flex:1;font-size:1.3rem;font-weight:bold}.bx-centered-dialog .bx-dialog-title button{flex-shrink:0}.bx-centered-dialog .bx-dialog-content{flex:1;padding:6px;overflow:auto;overflow-x:hidden}.bx-centered-dialog .bx-dialog-preset-tools{display:flex;margin-bottom:12px;gap:6px}.bx-centered-dialog .bx-dialog-preset-tools button{align-self:center;min-height:50px}.bx-centered-dialog .bx-default-preset-note{font-size:12px;font-style:italic;text-align:center;margin-bottom:10px}.bx-centered-dialog input,.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-centered-dialog input:focus,.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-centered-dialog select:disabled,.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-centered-dialog select option:disabled,.bx-settings-dialog select option:disabled{display:none}.bx-centered-dialog input[type=checkbox]:focus,.bx-settings-dialog input[type=checkbox]:focus,.bx-centered-dialog select:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-centered-dialog a,.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-centered-dialog a:hover,.bx-settings-dialog a:hover,.bx-centered-dialog a:focus,.bx-settings-dialog a:focus{color:#5dc21e}.bx-centered-dialog label,.bx-settings-dialog label{margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-note{margin-top:10px;font-size:14px;text-align:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt{flex-shrink:0;font-size:32px;margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt::first-letter{letter-spacing:6px}.bx-controller-shortcuts-manager-container select:disabled{text-align:left;text-align-last:left}.bx-keyboard-shortcuts-manager-container{display:flex;flex-direction:column;gap:16px}.bx-keyboard-shortcuts-manager-container fieldset{background:#2a2a2a;border:1px solid #2a2a2a;border-radius:4px;padding:4px}.bx-keyboard-shortcuts-manager-container legend{width:auto;padding:4px 8px;margin:0 4px 4px;background:#004f87;box-shadow:0 2px 0 #071e3d;border-radius:4px;font-size:14px;font-weight:bold;text-transform:uppercase}.bx-keyboard-shortcuts-manager-container .bx-settings-row{background:none;padding:10px}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;margin-left:48px;width:450px;background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;min-height:var(--bx-button-height);align-content:center}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label svg{width:20px;height:20px;margin-inline-end:8px}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-row[data-multi-lines="true"]{flex-direction:column}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label{align-self:start}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label + *{margin:unset}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;margin-bottom:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-content{padding:10px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:6px;border-top-right-radius:6px}.bx-settings-tab-content > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:6px;border-bottom-right-radius:6px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:6px}.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row[data-override=true],.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row:has(*[data-override=true]){border-left:4px solid #ffa500 !important;border-top-left-radius:0 !important;border-bottom-left-radius:0 !important}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861;height:45px;align-items:center}.bx-suggest-toggler label{flex:1;align-content:center;padding:0 10px;background:#004f87;height:100%}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:45px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-sub-content-box{background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-settings-row .bx-sub-content-box{background:#202020;padding:12px;box-shadow:0 0 4px #000 inset;border-radius:6px}.bx-controller-extra-settings[data-has-gamepad=true] > :first-child{display:none}.bx-controller-extra-settings[data-has-gamepad=true] > :last-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :first-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :last-child{display:none}.bx-controller-extra-settings .bx-controller-extra-wrapper{flex:1;min-width:1px}.bx-controller-extra-settings .bx-sub-content-box{flex:1;text-align:left;display:flex;flex-direction:column;margin-top:10px}.bx-controller-extra-settings .bx-sub-content-box > label{font-size:14px}.bx-preset-row{display:flex;gap:8px}.bx-preset-row .bx-select{flex:1}.bx-stream-settings-selection{margin-bottom:8px;position:sticky;z-index:1000;top:0}.bx-stream-settings-selection > div{display:flex;gap:8px;background:#222;padding:10px;border-bottom:4px solid #353638;box-shadow:0 0 6px #000;position:relative;z-index:1}.bx-stream-settings-selection > div .bx-select{flex:1}.bx-stream-settings-selection > div .bx-select label{font-weight:bold;font-size:1.1rem;line-height:initial}.bx-stream-settings-selection > div .bx-select label span{line-height:initial}.bx-stream-settings-selection > div .bx-select .bx-select-indicators{display:none}.bx-stream-settings-selection p{font-family:var(--bx-promptfont-font),var(--bx-normal-font);margin:0;font-size:13px;background:rgba(80,80,80,0.949);height:25px;line-height:23px;position:absolute;bottom:-25px;left:0;right:0;text-shadow:0 1px #000}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1;font-size:14px}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;align-self:center;padding:4px 0}.bx-remote-play-device-name{font-size:14px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:8px;background:#004c87;color:#fff;display:inline-block;border-radius:8px;padding:2px 6px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}select.bx-select{min-height:30px}div.bx-select{display:flex;align-items:stretch;flex:0 1 auto;gap:8px}div.bx-select select:disabled ~ button{display:none}div.bx-select select:disabled ~ div{background:#131416;color:#fff;pointer-events:none}div.bx-select select:disabled ~ div .bx-select-indicators{visibility:hidden}div.bx-select > div,div.bx-select button.bx-select-value{min-width:120px;text-align:left;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;display:flex;flex:1;flex-direction:column}div.bx-select > div{min-height:24px}div.bx-select > div input{display:inline-block;margin-right:8px}div.bx-select > div label{margin-bottom:0;font-size:14px;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-height:15px}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:20px;white-space:pre;min-height:15px;align-content:center}div.bx-select button.bx-select-value{border:none;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}div.bx-select button.bx-select-value > div{display:flex;width:100%}div.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}div.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}div.bx-select button.bx-select-value:hover input,div.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}div.bx-select button.bx-select-value:hover::after,div.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}div.bx-select button.bx-button{border:none;width:24px;height:auto;padding:0;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}div.bx-select button.bx-button span{line-height:unset}div.bx-select[data-controller-friendly=true] > div{box-sizing:content-box}div.bx-select[data-controller-friendly=true] select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}div.bx-select[data-controller-friendly=false]{position:relative}div.bx-select[data-controller-friendly=false] > div{box-sizing:border-box}div.bx-select[data-controller-friendly=false] > div label{margin-right:24px}div.bx-select[data-controller-friendly=false] select:disabled{display:none}div.bx-select[data-controller-friendly=false] select:not(:disabled){cursor:pointer;position:absolute;top:0;right:0;bottom:0;display:block;opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}div.bx-select[data-controller-friendly=false] select:not(:disabled):hover + div{background:#f0f0f0}div.bx-select[data-controller-friendly=false] select:not(:disabled) + div label::after{content:\'▾\';font-size:14px;position:absolute;right:8px;pointer-events:none}.bx-select-indicators{display:flex;height:4px;gap:2px;margin-bottom:2px}.bx-select-indicators span{content:\' \';display:inline-block;flex:1;background:#cfcfcf;border-radius:4px;min-width:1px}.bx-select-indicators span[data-highlighted]{background:#9c9c9c;min-width:6px}.bx-select-indicators span[data-selected]{background:#aacfe7}.bx-select-indicators span[data-highlighted][data-selected]{background:#5fa3d0}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}body[data-bx-media-type=tv] .bx-guide-home-achievements-progress{flex-direction:column}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress{flex-direction:row}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}body[data-bx-media-type=tv] .bx-guide-home-buttons > div{flex-direction:column}body[data-bx-media-type=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}body:not([data-bx-media-type=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state=\'normal\']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state=\'playing\']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container][data-position=center]{display:flex}div[data-testid=media-container][data-position=top] video,div[data-testid=media-container][data-position=top] canvas{top:0}div[data-testid=media-container][data-position=bottom] video,div[data-testid=media-container][data-position=bottom] canvas{bottom:0}#game-stream video{margin:auto;align-self:center;background:#000;position:absolute;left:0;right:0}#game-stream canvas{align-self:center;margin:auto;position:absolute;left:0;right:0}#game-stream.bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:\' \';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper > div{display:flex;align-items:center}.bx-number-stepper > div span{flex:1;display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);white-space:pre;font-size:13px;margin:0 4px}.bx-number-stepper > div button{flex-shrink:0;border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper > div button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper > div button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper > div button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type=range]{display:block;margin:8px 0 2px auto;min-width:180px;width:100%;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button,.bx-number-stepper[disabled=true] button{display:none}.bx-dual-number-stepper > span{display:block;font-family:var(--bx-monospaced-font);font-size:13px;white-space:pre;margin:0 4px;text-align:center}.bx-dual-number-stepper > div input[type=range]{display:block;width:100%;min-width:180px;background:transparent;color:#959595 !important;appearance:none;padding:8px 0}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-track,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-thumb{background:#fb3232}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-thumb{background:#fb3232}.bx-dual-number-stepper[data-disabled=true] input[type=range],.bx-dual-number-stepper[disabled=true] input[type=range]{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-shadow=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{display:inline-block;text-align:right;vertical-align:middle;white-space:pre}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;bottom:40px;transform:translateX(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:300px;opacity:.9;display:flex;flex-direction:column;gap:10px}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > p{margin:0;width:100%;font-size:22px;margin-bottom:4px;font-weight:bold;text-align:left}.bx-mkb-pointer-lock-msg > div{width:100%;display:flex;flex-direction:row;gap:10px}.bx-mkb-pointer-lock-msg > div button:first-of-type{flex-shrink:1}.bx-mkb-pointer-lock-msg > div button:last-of-type{flex-grow:1}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center;gap:20px}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:0 0 10px;font-size:12px;text-align:center}button.bx-binding-button{flex:1;min-height:38px;border:none;border-radius:4px;font-size:14px;color:#fff;display:flex;align-items:center;align-self:center;padding:0 6px}button.bx-binding-button:disabled{background:#131416;padding:0 8px}button.bx-binding-button:not(:disabled){border:2px solid transparent;border-top:none;border-bottom:4px solid #252525;background:#3b3b3b;cursor:pointer}button.bx-binding-button:not(:disabled):hover,button.bx-binding-button:not(:disabled).bx-focusable:focus{background:#20b217;border-bottom-color:#186c13}button.bx-binding-button:not(:disabled):active{background:#16900f;border-bottom:3px solid #0c4e08;border-left-width:2px;border-right-width:2px}button.bx-binding-button:not(:disabled).bx-focusable:focus::after{top:-6px;left:-8px;right:-8px;bottom:-10px}.bx-settings-row .bx-binding-button-wrapper button.bx-binding-button{min-width:60px}.bx-controller-customizations-container .bx-btn-detect{display:block;margin-bottom:20px}.bx-controller-customizations-container .bx-btn-detect.bx-monospaced{background:none;font-weight:bold;font-size:12px}.bx-controller-customizations-container .bx-buttons-grid{display:grid;grid-template-columns:auto auto;column-gap:20px;row-gap:10px;margin-bottom:20px}.bx-controller-key-row{display:flex;align-items:stretch}.bx-controller-key-row > label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center;min-width:50px;flex-shrink:0;display:flex;align-self:center}.bx-controller-key-row > label::after{content:\'❯\';margin:0 12px;font-size:16px;align-self:center}.bx-controller-key-row .bx-select{width:100% !important}.bx-controller-key-row .bx-select > div{min-width:50px}.bx-controller-key-row .bx-select label{font-family:var(--bx-promptfont-font),var(--bx-normal-font);font-size:32px;text-align:center;margin-bottom:6px;height:40px;line-height:40px}.bx-controller-key-row:hover > label{color:#ffe64b}.bx-controller-key-row:hover > label::after{color:#fff}.bx-controller-customization-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}.bx-controller-customization-summary span{font-family:var(--bx-promptfont);font-size:24px;border-radius:6px;background:#131313;color:#fff;display:inline-block;padding:2px;text-align:center}.bx-product-details-icons{padding:8px;border-radius:4px}.bx-product-details-icons svg{margin-right:8px}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getGlobalPref("ui.hideSections"), selectorToHide = []; -if (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); -} -function patchHistoryMethod(type) { -let orig = window.history[type]; -return function(...args) { -return BxEvent.dispatch(window, BxEvent.POPSTATE, { -arguments: args -}), orig.apply(this, arguments); -}; -} -function onHistoryChanged(e) { -if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return; -NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEventBus.Stream.emit("state.stopped", {}); -} -function setCodecPreferences(sdp, preferredCodec) { -let h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || []; -for (let match of matches) { -let id = match[1]; -if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id); -} -if (!preferredCodecIds.length) return sdp; -let lines = sdp.split(`\r -`); -for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) { -let line = lines[lineIndex]; -if (!line.startsWith("m=video")) continue; -let tmp = line.trim().split(" "), ids = tmp.slice(3); -ids = ids.filter((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 -`), mediaSet = new Set; -!!video && mediaSet.add("video"), !!audio && mediaSet.add("audio"); -let bitrate = { -video, -audio -}; -for (let lineNumber = 0;lineNumber < lines.length; lineNumber++) { -let media = "", line = lines[lineNumber]; -if (!line.startsWith("m=")) continue; -for (let m of mediaSet) -if (line.startsWith(`m=${m}`)) { -media = m, mediaSet.delete(media); -break; -} -if (!media) continue; -let bLine = `b=AS:${bitrate[media]}`; -while (lineNumber++, lineNumber < lines.length) { -if (line = lines[lineNumber], line.startsWith("i=") || line.startsWith("c=")) continue; -if (line.startsWith("b=AS:")) { -lines[lineNumber] = bLine; -break; -} -if (line.startsWith("m=")) { -lines.splice(lineNumber, 0, bLine); -break; -} -} -} -return lines.join(`\r -`); -} -var clarity_boost_default = `#version 300 es -in vec4 position; -void main() { -gl_Position = position; -}`; -var clarity_boost_default2 = `#version 300 es -precision mediump float; -uniform sampler2D data; -uniform vec2 iResolution; -const int FILTER_UNSHARP_MASKING = 1; -const float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0; -const vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722); -uniform int filterId; -uniform float sharpenFactor; -uniform float brightness; -uniform float contrast; -uniform float saturation; -out vec4 fragColor; -vec3 clarityBoost(sampler2D tex, vec2 coord, vec3 e) { -vec2 texelSize = 1.0 / iResolution.xy; -vec3 a = texture(tex, coord + texelSize * vec2(-1, 1)).rgb; -vec3 b = texture(tex, coord + texelSize * vec2(0, 1)).rgb; -vec3 c = texture(tex, coord + texelSize * vec2(1, 1)).rgb; -vec3 d = texture(tex, coord + texelSize * vec2(-1, 0)).rgb; -vec3 f = texture(tex, coord + texelSize * vec2(1, 0)).rgb; -vec3 g = texture(tex, coord + texelSize * vec2(-1, -1)).rgb; -vec3 h = texture(tex, coord + texelSize * vec2(0, -1)).rgb; -vec3 i = texture(tex, coord + texelSize * vec2(1, -1)).rgb; -if (filterId == FILTER_UNSHARP_MASKING) { -vec3 gaussianBlur = (a + c + g + i) * 1.0 + (b + d + f + h) * 2.0 + e * 4.0; -gaussianBlur /= 16.0; -return e + (e - gaussianBlur) * sharpenFactor / 3.0; -} -vec3 minRgb = min(min(min(d, e), min(f, b)), h); -minRgb += min(min(a, c), min(g, i)); -vec3 maxRgb = max(max(max(d, e), max(f, b)), h); -maxRgb += max(max(a, c), max(g, i)); -vec3 reciprocalMaxRgb = 1.0 / maxRgb; -vec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0); -amplifyRgb = inversesqrt(amplifyRgb); -vec3 weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK)); -vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0); -vec3 window = b + d + f + h; -vec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0); -return mix(e, outColor, sharpenFactor / 2.0); -} -void main() { -vec2 uv = gl_FragCoord.xy / iResolution.xy; -vec3 color = texture(data, uv).rgb; -color = sharpenFactor > 0.0 ? clarityBoost(data, uv, color) : color; -color = saturation != 1.0 ? mix(vec3(dot(color, LUMINOSITY_FACTOR)), color, saturation) : color; -color = contrast * (color - 0.5) + 0.5; -color = brightness * color; -fragColor = vec4(color, 1.0); -}`; -class WebGL2Player { -LOG_TAG = "WebGL2Player"; -$video; -$canvas; -gl = null; -resources = []; -program = null; -stopped = !1; -options = { -filterId: 1, -sharpenFactor: 0, -brightness: 0, -contrast: 0, -saturation: 0 -}; -targetFps = 60; -frameInterval = 0; -lastFrameTime = 0; -animFrameId = null; -constructor($video) { -BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video; -let $canvas = document.createElement("canvas"); -$canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas); -} -setFilter(filterId, update = !0) { -this.options.filterId = filterId, update && this.updateCanvas(); -} -setSharpness(sharpness, update = !0) { -this.options.sharpenFactor = sharpness, update && this.updateCanvas(); -} -setBrightness(brightness, update = !0) { -this.options.brightness = 1 + (brightness - 100) / 100, update && this.updateCanvas(); -} -setContrast(contrast, update = !0) { -this.options.contrast = 1 + (contrast - 100) / 100, update && this.updateCanvas(); -} -setSaturation(saturation, update = !0) { -this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas(); -} -setTargetFps(target) { -this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0; -} -getCanvas() { -return this.$canvas; -} -updateCanvas() { -let gl = this.gl, program = this.program; -gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation); -} -forceDrawFrame() { -let gl = this.gl; -gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6); -} -setupRendering() { -let frameCallback; -if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) { -let $video = this.$video; -frameCallback = $video.requestVideoFrameCallback.bind($video); -} else frameCallback = requestAnimationFrame; -let animate = () => { -if (this.stopped) return; -this.animFrameId = frameCallback(animate); -let draw = !0; -if (this.targetFps === 0) draw = !1; -else if (this.targetFps < 60) { -let currentTime = performance.now(); -if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1; -else this.lastFrameTime = currentTime; -} -if (draw) { -let gl = this.gl; -gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6); -} -}; -this.animFrameId = frameCallback(animate); -} -setupShaders() { -BxLogger.info(this.LOG_TAG, "Setting up", getStreamPref("video.player.powerPreference")); -let gl = this.$canvas.getContext("webgl2", { -isBx: !0, -antialias: !0, -alpha: !1, -powerPreference: getStreamPref("video.player.powerPreference") -}); -this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); -let vShader = gl.createShader(gl.VERTEX_SHADER); -gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader); -let fShader = gl.createShader(gl.FRAGMENT_SHADER); -gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader); -let program = gl.createProgram(); -if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`); -this.updateCanvas(); -let buffer = gl.createBuffer(); -this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0); -let texture = gl.createTexture(); -this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0); -} -resume() { -this.stop(), this.stopped = !1, BxLogger.info(this.LOG_TAG, "Resume"), this.$canvas.classList.remove("bx-gone"), this.setupRendering(); -} -stop() { -if (BxLogger.info(this.LOG_TAG, "Stop"), this.$canvas.classList.add("bx-gone"), this.stopped = !0, this.animFrameId) { -if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId); -else cancelAnimationFrame(this.animFrameId); -this.animFrameId = null; -} -} -destroy() { -BxLogger.info(this.LOG_TAG, "Destroy"), this.stop(); -let gl = this.gl; -if (gl) { -gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null); -for (let resource of this.resources) -if (resource instanceof WebGLProgram) gl.deleteProgram(resource); -else if (resource instanceof WebGLShader) gl.deleteShader(resource); -else if (resource instanceof WebGLTexture) gl.deleteTexture(resource); -else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource); -this.gl = null; -} -if (this.$canvas.isConnected) this.$canvas.parentElement?.removeChild(this.$canvas); -this.$canvas.width = 1, this.$canvas.height = 1; -} -} -class StreamPlayer { -$video; -playerType = "default"; -options = {}; -webGL2Player = null; -$videoCss = null; -$usmMatrix = null; -constructor($video, type, options) { -this.setupVideoElements(), this.$video = $video, this.options = options || {}, this.setPlayerType(type); -} -setupVideoElements() { -if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) return; -let $fragment = document.createDocumentFragment(); -this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss); -let $svg = CE("svg", { -id: "bx-video-filters", -xmlns: "http://www.w3.org/2000/svg", -class: "bx-gone" -}, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", { -id: "bx-filter-usm", -xmlns: "http://www.w3.org/2000/svg" -}, this.$usmMatrix = CE("feConvolveMatrix", { -id: "bx-filter-usm-matrix", -order: "3", -xmlns: "http://www.w3.org/2000/svg" -})))); -$fragment.appendChild($svg), document.documentElement.appendChild($fragment); -} -getVideoPlayerFilterStyle() { -let filters = [], sharpness = this.options.sharpness || 0; -if (this.options.processing === "usm" && sharpness != 0) { -let matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`; -this.$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)"); -} -let saturation = this.options.saturation || 100; -if (saturation != 100) filters.push(`saturate(${saturation}%)`); -let contrast = this.options.contrast || 100; -if (contrast != 100) filters.push(`contrast(${contrast}%)`); -let brightness = this.options.brightness || 100; -if (brightness != 100) filters.push(`brightness(${brightness}%)`); -return filters.join(" "); -} -resizePlayer() { -let PREF_RATIO = getStreamPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas; -if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas(); -let targetWidth, targetHeight, targetObjectFit; -if (PREF_RATIO.includes(":")) { -let tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]), width = 0, height = 0, parentRect = $video.parentElement.getBoundingClientRect(); -if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio; -else width = parentRect.width, height = width / videoRatio; -width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString(); -let $parent = $video.parentElement, position = getStreamPref("video.position"); -if ($parent.style.removeProperty("padding-top"), $parent.dataset.position = position, position === "top-half" || position === "bottom-half") { -let padding = Math.floor((window.innerHeight - height) / 4); -if (padding > 0) { -if (position === "bottom-half") padding *= 3; -$parent.style.paddingTop = padding + "px"; -} -} -targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill"; -} else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString(); -if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit, $video.dispatchEvent(new Event("resize")); -if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions(); -} -setPlayerType(type, refreshPlayer = !1) { -if (this.playerType !== type) { -let videoClass = BX_FLAGS.DeviceInfo.deviceType === "android-tv" ? "bx-pixel" : "bx-gone"; -if (type === "webgl2") { -if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video); -else this.webGL2Player.resume(); -this.$videoCss.textContent = "", this.$video.classList.add(videoClass); -} else this.webGL2Player?.stop(), this.$video.classList.remove(videoClass); -} -this.playerType = type, refreshPlayer && this.refreshPlayer(); -} -setOptions(options, refreshPlayer = !1) { -this.options = options, refreshPlayer && this.refreshPlayer(); -} -updateOptions(options, refreshPlayer = !1) { -this.options = Object.assign(this.options, options), refreshPlayer && this.refreshPlayer(); -} -getPlayerElement(playerType) { -if (typeof playerType === "undefined") playerType = this.playerType; -if (playerType === "webgl2") return this.webGL2Player?.getCanvas(); -return this.$video; -} -getWebGL2Player() { -return this.webGL2Player; -} -refreshPlayer() { -if (this.playerType === "webgl2") { -let options = this.options, webGL2Player = this.webGL2Player; -if (options.processing === "usm") webGL2Player.setFilter(1); -else webGL2Player.setFilter(2); -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 = getGlobalPref("ui.splashVideo.skip"), showFunc = function() { -if (this.style.visibility = "visible", !this.videoWidth) return; -let playerOptions = { -processing: getStreamPref("video.processing"), -sharpness: getStreamPref("video.processing.sharpness"), -saturation: getStreamPref("video.saturation"), -contrast: getStreamPref("video.contrast"), -brightness: getStreamPref("video.brightness") -}; -STATES.currentStream.streamPlayer = new StreamPlayer(this, getStreamPref("video.player.type"), playerOptions), BxEventBus.Stream.emit("state.playing", { -$video: this -}); -}, nativePlay = HTMLMediaElement.prototype.play; -HTMLMediaElement.prototype.nativePlay = nativePlay, HTMLMediaElement.prototype.play = function() { -if (this.className && this.className.startsWith("XboxSplashVideo")) { -if (PREF_SKIP_SPLASH_VIDEO) return this.volume = 0, this.style.display = "none", this.dispatchEvent(new Event("ended")), new Promise(() => {}); -return nativePlay.apply(this); -} -let $parent = this.parentElement; -if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 }); -return nativePlay.apply(this); -}; -} -function patchRtcCodecs() { -if (getGlobalPref("stream.video.codecProfile") === "default") return; -if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1; -} -function patchRtcPeerConnection() { -let nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; -RTCPeerConnection.prototype.createDataChannel = function() { -let dataChannel = nativeCreateDataChannel.apply(this, arguments); -return BxEventBus.Stream.emit("dataChannelCreated", { dataChannel }), dataChannel; -}; -let maxVideoBitrateDef = getGlobalPrefDefinition("stream.video.maxBitrate"), maxVideoBitrate = getGlobalPref("stream.video.maxBitrate"), codec = getGlobalPref("stream.video.codecProfile"); -if (codec !== "default" || maxVideoBitrate < maxVideoBitrateDef.max) { -let nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; -RTCPeerConnection.prototype.setLocalDescription = function(description) { -if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec); -try { -if (maxVideoBitrate < maxVideoBitrateDef.max && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); -} catch (e) { -BxLogger.error("setLocalDescription", e); -} -return nativeSetLocalDescription.apply(this, arguments); -}; -} -let OrgRTCPeerConnection = window.RTCPeerConnection; -window.RTCPeerConnection = function() { -let conn = new OrgRTCPeerConnection; -return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => { -BxLogger.info("connectionstatechange", conn.connectionState); -}), conn; -}; -} -function patchAudioContext() { -let OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain; -window.AudioContext = function(options) { -if (options && options.latencyHint) options.latencyHint = 0; -let ctx = new OrgAudioContext(options); -return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() { -let gainNode = nativeCreateGain.apply(this); -return gainNode.gain.value = getStreamPref("audio.volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; -}, STATES.currentStream.audioContext = ctx, ctx; -}; -} -function patchMeControl() { -let overrideConfigs = { -enableAADTelemetry: !1, -enableTelemetry: !1, -telEvs: "", -oneDSUrl: "" -}, MSA = { -MeControl: { -API: { -setDisplayMode: () => {}, -setMobileState: () => {}, -addEventListener: () => {}, -removeEventListener: () => {} -} -} -}, MeControl = {}, MsaHandler = { -get(target, prop, receiver) { -return target[prop]; -}, -set(obj, prop, value) { -if (prop === "MeControl" && value.Config) value.Config = Object.assign(value.Config, overrideConfigs); -return obj[prop] = value, !0; -} -}, MeControlHandler = { -get(target, prop, receiver) { -return target[prop]; -}, -set(obj, prop, value) { -if (prop === "Config") value = Object.assign(value, overrideConfigs); -return obj[prop] = value, !0; -} -}; -window.MSA = new Proxy(MSA, MsaHandler), window.MeControl = new Proxy(MeControl, MeControlHandler); -} -function disableAdobeAudienceManager() { -Object.defineProperty(window, "adobe", { -get() { -return Object.freeze({}); -} -}); -} -function patchCanvasContext() { -let nativeGetContext = HTMLCanvasElement.prototype.getContext; -HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) { -if (contextType.includes("webgl")) { -if (contextAttributes = contextAttributes || {}, !contextAttributes.isBx) { -if (contextAttributes.antialias = !1, contextAttributes.powerPreference === "high-performance") contextAttributes.powerPreference = "low-power"; -} -} -return nativeGetContext.apply(this, [contextType, contextAttributes]); -}; -} -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; -} -} -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 }); -} -} -SettingsManager.getInstance(); -if (window.location.pathname.includes("/auth/msa")) { -let nativePushState = window.history.pushState; -throw window.history.pushState = function(...args) { -let url = args[2]; -if (url && (url.startsWith("/play") || url.substring(6).startsWith("/play"))) { -console.log("Redirecting to xbox.com/play"), window.stop(), window.location.href = "https://www.xbox.com" + url; -return; -} -return nativePushState.apply(this, arguments); -}, new Error("[Better xCloud] Refreshing the page after logging in"); -} -BxLogger.info("readyState", document.readyState); -window.addEventListener("load", (e) => { -window.setTimeout(() => { -if (document.body.classList.contains("legacyBackground")) window.stop(), window.location.reload(!0); -}, 3000); -}); -document.addEventListener("readystatechange", (e) => { -if (document.readyState !== "interactive") return; -if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) ; -else window.setTimeout(HeaderSection.watchHeader, 2000); -if (getGlobalPref("ui.hideSections").includes("friends") || getGlobalPref("block.features").includes("friends")) { -let $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]"); -$parent && ($parent.style.display = "none"); -} -preloadFonts(); -}); -window.BX_EXPOSED = BxExposed; -window.addEventListener(BxEvent.POPSTATE, onHistoryChanged); -window.addEventListener("popstate", onHistoryChanged); -window.history.pushState = patchHistoryMethod("pushState"); -window.history.replaceState = patchHistoryMethod("replaceState"); -BxEventBus.Script.once("xcloud.server.unavailable", () => { -if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show(); -}); -BxEventBus.Script.on("xcloud.server.ready", () => { -STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000); -}); -BxEventBus.Stream.on("state.loading", () => { -if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title); -else STATES.currentStream.titleSlug = "remote-play"; -}); -getGlobalPref("loadingScreen.gameArt.show") && BxEventBus.Script.on("titleInfo.ready", LoadingScreen.setup); -BxEventBus.Stream.on("state.starting", () => { -LoadingScreen.hide(); -}); -BxEventBus.Stream.on("state.playing", (payload) => { -STATES.isPlaying = !0, StreamUiHandler.observe(), updateVideoPlayer(); -}); -BxEventBus.Stream.on("state.error", () => { -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 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(); -} -BxEventBus.Stream.on("state.stopped", unload); -window.addEventListener("pagehide", (e) => { -BxEventBus.Stream.emit("state.stopped", {}); -}); -function main() { -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();