diff --git a/dist/better-xcloud.lite.user.js b/dist/better-xcloud.lite.user.js new file mode 100644 index 0000000..479a822 --- /dev/null +++ b/dist/better-xcloud.lite.user.js @@ -0,0 +1,6944 @@ +// ==UserScript== +// @name Better xCloud (Lite) +// @namespace https://github.com/redphx +// @version 5.7.8-beta +// @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 #PREFIX = "[BxC]"; + static info(tag, ...args) { + BxLogger.#log("#008746", tag, ...args); + } + static warning(tag, ...args) { + BxLogger.#log("#c1a404", tag, ...args); + } + static error(tag, ...args) { + BxLogger.#log("#c10404", tag, ...args); + } + static #log(color, tag, ...args) { + console.log(`%c${BxLogger.#PREFIX}`, `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 SMART_TV_UNIQUE_ID = "FC4A1DA2-711C-4E9C-BC7F-047AF8A672EA", CHROMIUM_VERSION = "123.0.0.0"; +if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) { + const match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/); + if (match) CHROMIUM_VERSION = match[1]; +} +class UserAgent { + static STORAGE_KEY = "better_xcloud_user_agent"; + static #config; + static #isMobile = null; + static #isSafari = null; + static #isSafariMobile = null; + static #USER_AGENTS = { + "windows-edge": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`, + "macos-safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1", + "smarttv-generic": `${window.navigator.userAgent} SmartTV`, + "smarttv-tizen": `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`, + "vr-oculus": window.navigator.userAgent + " OculusBrowser VR" + }; + static init() { + if (UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}"), !UserAgent.#config.profile) UserAgent.#config.profile = BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default"; + if (!UserAgent.#config.custom) UserAgent.#config.custom = ""; + UserAgent.spoof(); + } + static updateStorage(profile, custom) { + const config = UserAgent.#config; + if (config.profile = profile, profile === "custom" && typeof custom !== "undefined") config.custom = custom; + window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(config)); + } + static getDefault() { + return window.navigator.orgUserAgent || window.navigator.userAgent; + } + static get(profile) { + const defaultUserAgent = window.navigator.userAgent; + switch (profile) { + case "default": + return defaultUserAgent; + case "custom": + return UserAgent.#config.custom || defaultUserAgent; + default: + return UserAgent.#USER_AGENTS[profile] || defaultUserAgent; + } + } + static isSafari() { + if (this.#isSafari !== null) return this.#isSafari; + const userAgent = UserAgent.getDefault().toLowerCase(); + let result = userAgent.includes("safari") && !userAgent.includes("chrom"); + return this.#isSafari = result, result; + } + static isSafariMobile() { + if (this.#isSafariMobile !== null) return this.#isSafariMobile; + const userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile"); + return this.#isSafariMobile = result, result; + } + static isMobile() { + if (this.#isMobile !== null) return this.#isMobile; + const userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent); + return this.#isMobile = result, result; + } + static spoof() { + const profile = UserAgent.#config.profile; + if (profile === "default") return; + let newUserAgent = UserAgent.get(profile); + if ("userAgentData" in window.navigator) window.navigator.orgUserAgentData = window.navigator.userAgentData, Object.defineProperty(window.navigator, "userAgentData", {}); + window.navigator.orgUserAgent = window.navigator.userAgent, Object.defineProperty(window.navigator, "userAgent", { + value: newUserAgent + }); + } +} +function deepClone(obj) { + if ("structuredClone" in window) return structuredClone(obj); + if (!obj) return {}; + return JSON.parse(JSON.stringify(obj)); +} +var SCRIPT_VERSION = "5.7.8-beta", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface; +UserAgent.init(); +var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = { + supportedRegion: !0, + serverRegions: {}, + selectedRegion: {}, + gsToken: "", + isSignedIn: !1, + isPlaying: !1, + appContext: {}, + browser: { + capabilities: { + touch: browserHasTouchSupport, + batteryApi: "getBattery" in window.navigator + } + }, + userAgent: { + isTv, + capabilities: { + touch: userAgentHasTouchSupport + } + }, + currentStream: {}, + remotePlay: {}, + pointerServerPort: 9269 +}, STORAGE = {}; +var BxEvent; +((BxEvent) => { + BxEvent.JUMP_BACK_IN_READY = "bx-jump-back-in-ready", BxEvent.POPSTATE = "bx-popstate", BxEvent.TITLE_INFO_READY = "bx-title-info-ready", BxEvent.SETTINGS_CHANGED = "bx-settings-changed", BxEvent.STREAM_LOADING = "bx-stream-loading", BxEvent.STREAM_STARTING = "bx-stream-starting", BxEvent.STREAM_STARTED = "bx-stream-started", BxEvent.STREAM_PLAYING = "bx-stream-playing", BxEvent.STREAM_STOPPED = "bx-stream-stopped", BxEvent.STREAM_ERROR_PAGE = "bx-stream-error-page", BxEvent.STREAM_WEBRTC_CONNECTED = "bx-stream-webrtc-connected", BxEvent.STREAM_WEBRTC_DISCONNECTED = "bx-stream-webrtc-disconnected", BxEvent.STREAM_SESSION_READY = "bx-stream-session-ready", BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED = "bx-custom-touch-layouts-loaded", BxEvent.TOUCH_LAYOUT_MANAGER_READY = "bx-touch-layout-manager-ready", BxEvent.REMOTE_PLAY_READY = "bx-remote-play-ready", BxEvent.REMOTE_PLAY_FAILED = "bx-remote-play-failed", BxEvent.XCLOUD_SERVERS_READY = "bx-servers-ready", BxEvent.XCLOUD_SERVERS_UNAVAILABLE = "bx-servers-unavailable", BxEvent.DATA_CHANNEL_CREATED = "bx-data-channel-created", BxEvent.GAME_BAR_ACTION_ACTIVATED = "bx-game-bar-action-activated", BxEvent.MICROPHONE_STATE_CHANGED = "bx-microphone-state-changed", BxEvent.SPEAKER_STATE_CHANGED = "bx-speaker-state-changed", BxEvent.CAPTURE_SCREENSHOT = "bx-capture-screenshot", BxEvent.POINTER_LOCK_REQUESTED = "bx-pointer-lock-requested", BxEvent.POINTER_LOCK_EXITED = "bx-pointer-lock-exited", BxEvent.NAVIGATION_FOCUS_CHANGED = "bx-nav-focus-changed", BxEvent.XCLOUD_DIALOG_SHOWN = "bx-xcloud-dialog-shown", BxEvent.XCLOUD_DIALOG_DISMISSED = "bx-xcloud-dialog-dismissed", BxEvent.XCLOUD_GUIDE_MENU_SHOWN = "bx-xcloud-guide-menu-shown", BxEvent.XCLOUD_POLLING_MODE_CHANGED = "bx-xcloud-polling-mode-changed", BxEvent.XCLOUD_RENDERING_COMPONENT = "bx-xcloud-rendering-component", BxEvent.XCLOUD_ROUTER_HISTORY_READY = "bx-xcloud-router-history-ready"; + function dispatch(target, eventName, data) { + if (!target) return; + if (!eventName) { + alert("BxEvent.dispatch(): eventName is null"); + return; + } + const event = new Event(eventName); + if (data) for (let key in data) + event[key] = data[key]; + target.dispatchEvent(event), AppInterface && AppInterface.onEvent(eventName), BX_FLAGS.Debug && BxLogger.warning("BxEvent", "dispatch", eventName, data); + } + BxEvent.dispatch = dispatch; +})(BxEvent ||= {}); +window.BxEvent = BxEvent; +class NavigationUtils { + static setNearby($elm, nearby) { + $elm.nearby = $elm.nearby || {}; + let key; + for (key in nearby) + $elm.nearby[key] = nearby[key]; + } +} +var setNearby = NavigationUtils.setNearby; +function createElement(elmName, props = {}, ..._) { + let $elm; + const hasNs = "xmlns" in props; + if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns; + else $elm = document.createElement(elmName); + if (props._nearby) setNearby($elm, props._nearby), delete props._nearby; + for (let key in props) { + if ($elm.hasOwnProperty(key)) continue; + if (hasNs) $elm.setAttributeNS(null, key, props[key]); + else if (key === "on") for (let eventName in props[key]) + $elm.addEventListener(eventName, props[key][eventName]); + else $elm.setAttribute(key, props[key]); + } + for (let i = 2, size = arguments.length;i < size; i++) { + const arg = arguments[i]; + if (arg instanceof Node) $elm.appendChild(arg); + else if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.appendChild(document.createTextNode(arg)); + } + return $elm; +} +function escapeHtml(html) { + const text = document.createTextNode(html), $span = document.createElement("span"); + return $span.appendChild(text), $span.innerHTML; +} +function isElementVisible($elm) { + const rect = $elm.getBoundingClientRect(); + return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height; +} +function removeChildElements($parent) { + while ($parent.firstElementChild) + $parent.firstElementChild.remove(); +} +var ButtonStyleClass = { + 1: "bx-primary", + 2: "bx-danger", + 4: "bx-ghost", + 8: "bx-frosted", + 16: "bx-drop-shadow", + 32: "bx-focusable", + 64: "bx-full-width", + 128: "bx-full-height", + 256: "bx-tall", + 512: "bx-circular", + 1024: "bx-normal-case", + 2048: "bx-normal-link" +}, CE = createElement, svgParser = (svg) => new DOMParser().parseFromString(svg, "image/svg+xml").documentElement, createSvgIcon = (icon) => { + return svgParser(icon.toString()); +}, ButtonStyleIndices = Object.keys(ButtonStyleClass).map((i) => parseInt(i)), createButton = (options) => { + let $btn; + if (options.url) $btn = CE("a", { class: "bx-button" }), $btn.href = options.url, $btn.target = "_blank"; + else $btn = CE("button", { class: "bx-button", type: "button" }); + const style = options.style || 0; + style && ButtonStyleIndices.forEach((index) => { + style & index && $btn.classList.add(ButtonStyleClass[index]); + }), options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", {}, options.label)), options.title && $btn.setAttribute("title", options.title), options.disabled && ($btn.disabled = !0), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0; + for (let key in options.attributes) + if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]); + return $btn; +}, CTN = document.createTextNode.bind(document); +window.BX_CE = createElement; +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 = { + activate: "Activate", + activated: "Activated", + active: "Active", + advanced: "Advanced", + "always-off": "Always off", + "always-on": "Always on", + "amd-fidelity-cas": "AMD FidelityFX CAS", + "app-settings": "App settings", + apply: "Apply", + "aspect-ratio": "Aspect ratio", + "aspect-ratio-note": "Don't use with native touch games", + audio: "Audio", + auto: "Auto", + "back-to-home": "Back to home", + "back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?", + "badge-audio": "Audio", + "badge-battery": "Battery", + "badge-in": "In", + "badge-out": "Out", + "badge-playtime": "Playtime", + "badge-server": "Server", + "badge-video": "Video", + "battery-saving": "Battery saving", + "better-xcloud": "Better xCloud", + "bitrate-audio-maximum": "Maximum audio bitrate", + "bitrate-video-maximum": "Maximum video bitrate", + "bottom-left": "Bottom-left", + "bottom-right": "Bottom-right", + brazil: "Brazil", + brightness: "Brightness", + "browser-unsupported-feature": "Your browser doesn't support this feature", + "bypass-region-restriction": "Bypass region restriction", + "can-stream-xbox-360-games": "Can stream Xbox 360 games", + cancel: "Cancel", + "cant-stream-xbox-360-games": "Can't stream Xbox 360 games", + "clarity-boost": "Clarity boost", + "clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON", + clear: "Clear", + close: "Close", + "close-app": "Close app", + "combine-audio-video-streams": "Combine audio & video streams", + "combine-audio-video-streams-summary": "May fix the laggy audio problem", + "conditional-formatting": "Conditional formatting text color", + "confirm-delete-preset": "Do you want to delete this preset?", + "confirm-reload-stream": "Do you want to refresh the stream?", + connected: "Connected", + "console-connect": "Connect", + contrast: "Contrast", + controller: "Controller", + "controller-friendly-ui": "Controller-friendly UI", + "controller-shortcuts": "Controller shortcuts", + "controller-shortcuts-connect-note": "Connect a controller to use this feature", + "controller-shortcuts-xbox-note": "Button to open the Guide menu", + "controller-vibration": "Controller vibration", + copy: "Copy", + "create-shortcut": "Shortcut", + custom: "Custom", + "deadzone-counterweight": "Deadzone counterweight", + decrease: "Decrease", + default: "Default", + delete: "Delete", + device: "Device", + "device-unsupported-touch": "Your device doesn't have touch support", + "device-vibration": "Device vibration", + "device-vibration-not-using-gamepad": "On when not using gamepad", + disable: "Disable", + "disable-home-context-menu": "Disable context menu in Home page", + "disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog", + "disable-social-features": "Disable social features", + "disable-xcloud-analytics": "Disable xCloud analytics", + disabled: "Disabled", + disconnected: "Disconnected", + 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 if the game doesn't require a different profile", + "enable-mic-on-startup": "Enable microphone on game launch", + "enable-mkb": "Emulate controller with Mouse & Keyboard", + "enable-quick-glance-mode": "Enable \"Quick Glance\" mode", + "enable-remote-play-feature": "Enable the \"Remote Play\" feature", + "enable-volume-control": "Enable volume control feature", + enabled: "Enabled", + experimental: "Experimental", + export: "Export", + fast: "Fast", + "fortnite-allow-stw-mode": "Allows playing STW mode on mobile", + "fortnite-force-console-version": "Fortnite: force console version", + "game-bar": "Game Bar", + "getting-consoles-list": "Getting the list of consoles...", + guide: "Guide", + help: "Help", + hide: "Hide", + "hide-idle-cursor": "Hide mouse cursor on idle", + "hide-scrollbar": "Hide web page's scrollbar", + "hide-sections": "Hide sections", + "hide-system-menu-icon": "Hide System menu's icon", + "hide-touch-controller": "Hide touch controller", + "high-performance": "High performance", + "highest-quality": "Highest quality", + "highest-quality-note": "Your device may not be powerful enough to use these settings", + "horizontal-scroll-sensitivity": "Horizontal scroll sensitivity", + "horizontal-sensitivity": "Horizontal sensitivity", + "how-to-fix": "How to fix", + "how-to-improve-app-performance": "How to improve app's performance", + ignore: "Ignore", + import: "Import", + increase: "Increase", + "install-android": "Better xCloud app for Android", + japan: "Japan", + "keyboard-shortcuts": "Keyboard shortcuts", + korea: "Korea", + language: "Language", + large: "Large", + layout: "Layout", + "left-stick": "Left stick", + "load-failed-message": "Failed to run Better xCloud", + "loading-screen": "Loading screen", + "local-co-op": "Local co-op", + "lowest-quality": "Lowest quality", + "map-mouse-to": "Map mouse to", + "may-not-work-properly": "May not work properly!", + menu: "Menu", + microphone: "Microphone", + "mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings", + "mkb-click-to-activate": "Click to activate", + "mkb-disclaimer": "Using this feature when playing online could be viewed as cheating", + "mouse-and-keyboard": "Mouse & Keyboard", + "mouse-wheel": "Mouse wheel", + muted: "Muted", + name: "Name", + "native-mkb": "Native Mouse & Keyboard", + new: "New", + "new-version-available": [ + e => `Version ${e.version} available`, + , + , + e => `Version ${e.version} verfügbar`, + , + e => `Versión ${e.version} disponible`, + e => `Version ${e.version} disponible`, + e => `Disponibile la versione ${e.version}`, + e => `Ver ${e.version} が利用可能です`, + e => `${e.version} 버전 사용가능`, + e => `Dostępna jest nowa wersja ${e.version}`, + e => `Versão ${e.version} disponível`, + , + e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`, + , + e => `Доступна версія ${e.version}`, + e => `Đã có phiên bản ${e.version}`, + e => `版本 ${e.version} 可供更新`, + e => `已可更新為 ${e.version} 版` + ], + "no-consoles-found": "No consoles found", + normal: "Normal", + off: "Off", + on: "On", + "only-supports-some-games": "Only supports some games", + opacity: "Opacity", + other: "Other", + playing: "Playing", + poland: "Poland", + position: "Position", + "powered-off": "Powered off", + "powered-on": "Powered on", + "prefer-ipv6-server": "Prefer IPv6 server", + "preferred-game-language": "Preferred game's language", + preset: "Preset", + "press-esc-to-cancel": "Press Esc to cancel", + "press-key-to-toggle-mkb": [ + e => `Press ${e.key} to toggle this feature`, + e => `Premeu ${e.key} per alternar aquesta funció`, + e => `Tryk på ${e.key} for at slå denne funktion til`, + e => `${e.key}: Funktion an-/ausschalten`, + e => `Tekan ${e.key} untuk mengaktifkan fitur ini`, + e => `Pulsa ${e.key} para alternar esta función`, + e => `Appuyez sur ${e.key} pour activer cette fonctionnalité`, + e => `Premi ${e.key} per attivare questa funzionalità`, + e => `${e.key} でこの機能を切替`, + e => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`, + e => `Naciśnij ${e.key} aby przełączyć tę funkcję`, + e => `Pressione ${e.key} para alternar este recurso`, + e => `Нажмите ${e.key} для переключения этой функции`, + e => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`, + e => `Etkinleştirmek için ${e.key} tuşuna basın`, + e => `Натисніть ${e.key} щоб перемкнути цю функцію`, + e => `Nhấn ${e.key} để bật/tắt tính năng này`, + e => `按下 ${e.key} 来切换此功能`, + e => `按下 ${e.key} 來啟用此功能` + ], + "press-to-bind": "Press a key or do a mouse click to bind...", + "prompt-preset-name": "Preset's name:", + recommended: "Recommended", + "recommended-settings-for-device": [ + e => `Recommended settings for ${e.device}`, + e => `Configuració recomanada per a ${e.device}`, + , + e => `Empfohlene Einstellungen für ${e.device}`, + , + e => `Ajustes recomendados para ${e.device}`, + e => `Paramètres recommandés pour ${e.device}`, + e => `Configurazioni consigliate per ${e.device}`, + e => `${e.device} の推奨設定`, + e => `다음 기기에서 권장되는 설정: ${e.device}`, + e => `Zalecane ustawienia dla ${e.device}`, + e => `Configurações recomendadas para ${e.device}`, + e => `Рекомендуемые настройки для ${e.device}`, + e => `การตั้งค่าที่แนะนำสำหรับ ${e.device}`, + e => `${e.device} için önerilen ayarlar`, + e => `Рекомендовані налаштування для ${e.device}`, + e => `Cấu hình được đề xuất cho ${e.device}`, + e => `${e.device} 的推荐设置`, + e => `${e.device} 推薦的設定` + ], + "reduce-animations": "Reduce UI animations", + region: "Region", + "reload-page": "Reload page", + "remote-play": "Remote Play", + rename: "Rename", + renderer: "Renderer", + "renderer-configuration": "Renderer configuration", + "right-click-to-unbind": "Right-click on a key to unbind it", + "right-stick": "Right stick", + "rocket-always-hide": "Always hide", + "rocket-always-show": "Always show", + "rocket-animation": "Rocket animation", + "rocket-hide-queue": "Hide when queuing", + saturation: "Saturation", + save: "Save", + screen: "Screen", + "screenshot-apply-filters": "Apply video filters to screenshots", + "section-all-games": "All games", + "section-most-popular": "Most popular", + "section-native-mkb": "Play with mouse & keyboard", + "section-news": "News", + "section-play-with-friends": "Play with friends", + "section-touch": "Play with touch", + "separate-touch-controller": "Separate Touch controller & Controller #1", + "separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2", + server: "Server", + settings: "Settings", + "settings-reload": "Reload page to reflect changes", + "settings-reload-note": "Settings in this tab only go into effect on the next page load", + "settings-reloading": "Reloading...", + sharpness: "Sharpness", + "shortcut-keys": "Shortcut keys", + show: "Show", + "show-controller-connection-status": "Show controller connection status", + "show-game-art": "Show game art", + "show-hide": "Show/hide", + "show-stats-on-startup": "Show stats when starting the game", + "show-touch-controller": "Show touch controller", + "show-wait-time": "Show the estimated wait time", + "show-wait-time-in-game-card": "Show wait time in game card", + "simplify-stream-menu": "Simplify Stream's menu", + "skip-splash-video": "Skip Xbox splash video", + slow: "Slow", + small: "Small", + "smart-tv": "Smart TV", + sound: "Sound", + standby: "Standby", + "stat-bitrate": "Bitrate", + "stat-decode-time": "Decode time", + "stat-fps": "FPS", + "stat-frames-lost": "Frames lost", + "stat-packets-lost": "Packets lost", + "stat-ping": "Ping", + stats: "Stats", + "stick-decay-minimum": "Stick decay minimum", + "stick-decay-strength": "Stick decay strength", + stream: "Stream", + "stream-settings": "Stream settings", + "stream-stats": "Stream stats", + stretch: "Stretch", + "suggest-settings": "Suggest settings", + "suggest-settings-link": "Suggest recommended settings for this device", + "support-better-xcloud": "Support Better xCloud", + "swap-buttons": "Swap buttons", + "take-screenshot": "Take screenshot", + "target-resolution": "Target resolution", + "tc-all-games": "All games", + "tc-all-white": "All white", + "tc-auto-off": "Off when controller found", + "tc-availability": "Availability", + "tc-custom-layout-style": "Custom layout's button style", + "tc-default-opacity": "Default opacity", + "tc-muted-colors": "Muted colors", + "tc-standard-layout-style": "Standard layout's button style", + "text-size": "Text size", + toggle: "Toggle", + "top-center": "Top-center", + "top-left": "Top-left", + "top-right": "Top-right", + "touch-control-layout": "Touch control layout", + "touch-control-layout-by": [ + e => `Touch control layout by ${e.name}`, + e => `Format del control tàctil per ${e.name}`, + e => `Touch-kontrol layout af ${e.name}`, + e => `Touch-Steuerungslayout von ${e.name}`, + e => `Tata letak Sentuhan layar oleh ${e.name}`, + e => `Disposición del control táctil por ${e.nombre}`, + e => `Disposition du contrôleur tactile par ${e.name}`, + e => `Configurazione dei comandi su schermo creata da ${e.name}`, + e => `タッチ操作レイアウト作成者: ${e.name}`, + e => `${e.name} 제작, 터치 컨트롤 레이아웃`, + e => `Układ sterowania dotykowego stworzony przez ${e.name}`, + e => `Disposição de controle por toque feito por ${e.name}`, + e => `Сенсорная раскладка по ${e.name}`, + e => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`, + e => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`, + e => `Розташування сенсорного керування від ${e.name}`, + e => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`, + e => `由 ${e.name} 提供的虚拟按键样式`, + e => `觸控遊玩佈局由 ${e.name} 提供` + ], + "touch-controller": "Touch controller", + "transparent-background": "Transparent background", + "true-achievements": "TrueAchievements", + ui: "UI", + "unexpected-behavior": "May cause unexpected behavior", + "united-states": "United States", + unknown: "Unknown", + unlimited: "Unlimited", + unmuted: "Unmuted", + "unsharp-masking": "Unsharp masking", + "use-mouse-absolute-position": "Use mouse's absolute position", + "use-this-at-your-own-risk": "Use this at your own risk", + "user-agent-profile": "User-Agent profile", + "vertical-scroll-sensitivity": "Vertical scroll sensitivity", + "vertical-sensitivity": "Vertical sensitivity", + "vibration-intensity": "Vibration intensity", + "vibration-status": "Vibration", + video: "Video", + "virtual-controller": "Virtual controller", + "visual-quality": "Visual quality", + "visual-quality-high": "High", + "visual-quality-low": "Low", + "visual-quality-normal": "Normal", + volume: "Volume", + "wait-time-countdown": "Countdown", + "wait-time-estimated": "Estimated finish time", + wallpaper: "Wallpaper", + webgl2: "WebGL2" +}; +class Translations { + static #EN_US = "en-US"; + static #KEY_LOCALE = "better_xcloud_locale"; + static #KEY_TRANSLATIONS = "better_xcloud_translations"; + static #enUsIndex = -1; + static #selectedLocaleIndex = -1; + static #selectedLocale = "en-US"; + static #supportedLocales = Object.keys(SUPPORTED_LANGUAGES); + static #foreignTranslations = {}; + static async init() { + Translations.#enUsIndex = Translations.#supportedLocales.indexOf(Translations.#EN_US), Translations.refreshLocale(), await Translations.#loadTranslations(); + } + static refreshLocale(newLocale) { + let locale; + if (newLocale) localStorage.setItem(Translations.#KEY_LOCALE, newLocale), locale = newLocale; + else locale = localStorage.getItem(Translations.#KEY_LOCALE); + const supportedLocales = Translations.#supportedLocales; + if (!locale) { + if (locale = window.navigator.language || Translations.#EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.#EN_US; + localStorage.setItem(Translations.#KEY_LOCALE, locale); + } + Translations.#selectedLocale = locale, Translations.#selectedLocaleIndex = supportedLocales.indexOf(locale); + } + static get(key, values) { + let text = null; + if (Translations.#foreignTranslations && Translations.#selectedLocale !== Translations.#EN_US) text = Translations.#foreignTranslations[key]; + if (!text) text = Texts[key] || alert(`Missing translation key: ${key}`); + let translation; + if (Array.isArray(text)) return translation = text[Translations.#selectedLocaleIndex] || text[Translations.#enUsIndex], translation(values); + return translation = text, translation; + } + static async#loadTranslations() { + if (Translations.#selectedLocale === Translations.#EN_US) return; + try { + Translations.#foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.#KEY_TRANSLATIONS)); + } catch (e) {} + if (!Translations.#foreignTranslations) await this.downloadTranslations(Translations.#selectedLocale); + } + static async updateTranslations(async = !1) { + if (Translations.#selectedLocale === Translations.#EN_US) { + localStorage.removeItem(Translations.#KEY_TRANSLATIONS); + return; + } + if (async) Translations.downloadTranslationsAsync(Translations.#selectedLocale); + else await Translations.downloadTranslations(Translations.#selectedLocale); + } + static async downloadTranslations(locale) { + try { + const translations = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/translations/${locale}.json`)).json(); + if (localStorage.getItem(Translations.#KEY_LOCALE) === locale) window.localStorage.setItem(Translations.#KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.#foreignTranslations = translations; + return !0; + } catch (e) { + debugger; + } + return !1; + } + static downloadTranslationsAsync(locale) { + NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/translations/${locale}.json`).then((resp) => resp.json()).then((translations) => { + window.localStorage.setItem(Translations.#KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.#foreignTranslations = translations; + }); + } + static switchLocale(locale) { + localStorage.setItem(Translations.#KEY_LOCALE, locale); + } +} +var t = Translations.get; +Translations.init(); +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 StreamStats { + static instance; + static getInstance() { + if (!StreamStats.instance) StreamStats.instance = new StreamStats; + return StreamStats.instance; + } + #timeoutId; + #updateInterval = 1000; + #$container; + #$fps; + #$ping; + #$dt; + #$pl; + #$fl; + #$br; + #lastVideoStat; + #quickGlanceObserver; + constructor() { + this.#render(); + } + start(glancing = !1) { + if (!this.isHidden() || glancing && this.isGlancing()) return; + if (this.#$container) this.#$container.classList.remove("bx-gone"), this.#$container.dataset.display = glancing ? "glancing" : "fixed"; + this.#timeoutId = window.setTimeout(this.#update.bind(this), this.#updateInterval); + } + stop(glancing = !1) { + if (glancing && !this.isGlancing()) return; + if (this.#timeoutId && clearTimeout(this.#timeoutId), this.#timeoutId = null, this.#lastVideoStat = null, this.#$container) this.#$container.removeAttribute("data-display"), this.#$container.classList.add("bx-gone"); + } + toggle() { + if (this.isGlancing()) this.#$container && (this.#$container.dataset.display = "fixed"); + else this.isHidden() ? this.start() : this.stop(); + } + onStoppedPlaying() { + this.stop(), this.quickGlanceStop(), this.hideSettingsUi(); + } + isHidden = () => this.#$container && this.#$container.classList.contains("bx-gone"); + isGlancing = () => this.#$container && this.#$container.dataset.display === "glancing"; + quickGlanceSetup() { + if (!STATES.isPlaying || this.#quickGlanceObserver) return; + const $uiContainer = document.querySelector("div[data-testid=ui-container]"); + if (!$uiContainer) return; + this.#quickGlanceObserver = new MutationObserver((mutationList, observer) => { + for (let record of mutationList) + if (record.attributeName && record.attributeName === "aria-expanded") if (record.target.ariaExpanded === "true") this.isHidden() && this.start(!0); + else this.stop(!0); + }), this.#quickGlanceObserver.observe($uiContainer, { + attributes: !0, + attributeFilter: ["aria-expanded"], + subtree: !0 + }); + } + quickGlanceStop() { + this.#quickGlanceObserver && this.#quickGlanceObserver.disconnect(), this.#quickGlanceObserver = null; + } + async#update() { + if (this.isHidden() || !STATES.currentStream.peerConnection) { + this.onStoppedPlaying(); + return; + } + this.#timeoutId = null; + const startTime = performance.now(), PREF_STATS_CONDITIONAL_FORMATTING = getPref("stats_conditional_formatting"), stats = await STATES.currentStream.peerConnection.getStats(); + let grade = ""; + stats.forEach((stat) => { + if (stat.type === "inbound-rtp" && stat.kind === "video") { + this.#$fps.textContent = stat.framesPerSecond || 0; + const packetsLost = Math.max(0, stat.packetsLost), packetsReceived = stat.packetsReceived, packetsLostPercentage = (packetsLost * 100 / (packetsLost + packetsReceived || 1)).toFixed(2); + this.#$pl.textContent = packetsLostPercentage === "0.00" ? packetsLost.toString() : `${packetsLost} (${packetsLostPercentage}%)`; + const { framesDropped, framesReceived } = stat, framesDroppedPercentage = (framesDropped * 100 / (framesDropped + framesReceived || 1)).toFixed(2); + if (this.#$fl.textContent = framesDroppedPercentage === "0.00" ? framesDropped : `${framesDropped} (${framesDroppedPercentage}%)`, !this.#lastVideoStat) { + this.#lastVideoStat = stat; + return; + } + const lastStat = this.#lastVideoStat, timeDiff = stat.timestamp - lastStat.timestamp, bitrate = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000; + this.#$br.textContent = `${bitrate.toFixed(2)} Mbps`; + const totalDecodeTimeDiff = stat.totalDecodeTime - lastStat.totalDecodeTime, framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded, currentDecodeTime = totalDecodeTimeDiff / framesDecodedDiff * 1000; + if (isNaN(currentDecodeTime)) this.#$dt.textContent = "??ms"; + else this.#$dt.textContent = `${currentDecodeTime.toFixed(2)}ms`; + if (PREF_STATS_CONDITIONAL_FORMATTING) grade = currentDecodeTime > 12 ? "bad" : currentDecodeTime > 9 ? "ok" : currentDecodeTime > 6 ? "good" : "", this.#$dt.dataset.grade = grade; + this.#lastVideoStat = stat; + } else if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") { + const roundTripTime = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1; + if (this.#$ping.textContent = roundTripTime === -1 ? "???" : roundTripTime.toString(), PREF_STATS_CONDITIONAL_FORMATTING) grade = roundTripTime > 100 ? "bad" : roundTripTime > 75 ? "ok" : roundTripTime > 40 ? "good" : "", this.#$ping.dataset.grade = grade; + } + }); + const lapsedTime = performance.now() - startTime; + this.#timeoutId = window.setTimeout(this.#update.bind(this), this.#updateInterval - lapsedTime); + } + refreshStyles() { + const PREF_ITEMS = getPref("stats_items"), PREF_POSITION = getPref("stats_position"), PREF_TRANSPARENT = getPref("stats_transparent"), PREF_OPACITY = getPref("stats_opacity"), PREF_TEXT_SIZE = getPref("stats_text_size"), $container = this.#$container; + $container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = PREF_POSITION, $container.dataset.transparent = PREF_TRANSPARENT, $container.style.opacity = PREF_OPACITY + "%", $container.style.fontSize = PREF_TEXT_SIZE; + } + hideSettingsUi() { + if (this.isGlancing() && !getPref("stats_quick_glance")) this.stop(); + } + #render() { + const stats = { + ping: [t("stat-ping"), this.#$ping = CE("span", {}, "0")], + fps: [t("stat-fps"), this.#$fps = CE("span", {}, "0")], + btr: [t("stat-bitrate"), this.#$br = CE("span", {}, "0 Mbps")], + dt: [t("stat-decode-time"), this.#$dt = CE("span", {}, "0ms")], + pl: [t("stat-packets-lost"), this.#$pl = CE("span", {}, "0")], + fl: [t("stat-frames-lost"), this.#$fl = CE("span", {}, "0")] + }, $barFragment = document.createDocumentFragment(); + let statKey; + for (statKey in stats) { + const $div = CE("div", { + class: `bx-stat-${statKey}`, + title: stats[statKey][0] + }, CE("label", {}, statKey.toUpperCase()), stats[statKey][1]); + $barFragment.appendChild($div); + } + this.#$container = CE("div", { class: "bx-stats-bar bx-gone" }, $barFragment), this.refreshStyles(), document.documentElement.appendChild(this.#$container); + } + static setupEvents() { + window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { + const PREF_STATS_QUICK_GLANCE = getPref("stats_quick_glance"), PREF_STATS_SHOW_WHEN_PLAYING = getPref("stats_show_when_playing"), streamStats = StreamStats.getInstance(); + if (PREF_STATS_SHOW_WHEN_PLAYING) streamStats.start(); + else if (PREF_STATS_QUICK_GLANCE) streamStats.quickGlanceSetup(), !PREF_STATS_SHOW_WHEN_PLAYING && streamStats.start(!0); + }); + } + static refreshStyles() { + StreamStats.getInstance().refreshStyles(); + } +} +class SettingElement { + static #renderOptions(key, setting, currentValue, onChange) { + const $control = CE("select", { + tabindex: 0 + }); + let $parent; + if (setting.optionsGroup) $parent = CE("optgroup", { + label: setting.optionsGroup + }), $control.appendChild($parent); + else $parent = $control; + for (let value in setting.options) { + const label = setting.options[value], $option = CE("option", { value }, label); + $parent.appendChild($option); + } + return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { + const target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value; + !e.ignoreOnChange && onChange(e, value); + }), $control.setValue = (value) => { + $control.value = value; + }, $control; + } + static #renderMultipleOptions(key, setting, currentValue, onChange, params = {}) { + const $control = CE("select", { + multiple: !0, + tabindex: 0 + }); + if (params && params.size) $control.setAttribute("size", params.size.toString()); + for (let value in setting.multipleOptions) { + const label = setting.multipleOptions[value], $option = CE("option", { value }, label); + $option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) { + e.preventDefault(); + const target = e.target; + target.selected = !target.selected; + const $parent = target.parentElement; + $parent.focus(), BxEvent.dispatch($parent, "input"); + }), $control.appendChild($option); + } + return $control.addEventListener("mousedown", function(e) { + const self = this, orgScrollTop = self.scrollTop; + window.setTimeout(() => self.scrollTop = orgScrollTop, 0); + }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => { + const target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value); + !e.ignoreOnChange && onChange(e, values); + }), $control; + } + static #renderNumber(key, setting, currentValue, onChange) { + const $control = CE("input", { + tabindex: 0, + type: "number", + min: setting.min, + max: setting.max + }); + return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { + const target = e.target, value = Math.max(setting.min, Math.min(setting.max, parseInt(target.value))); + target.value = value.toString(), !e.ignoreOnChange && onChange(e, value); + }), $control; + } + static #renderCheckbox(key, setting, currentValue, onChange) { + const $control = CE("input", { type: "checkbox", tabindex: 0 }); + return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => { + !e.ignoreOnChange && onChange(e, e.target.checked); + }), $control.setValue = (value) => { + $control.checked = !!value; + }, $control; + } + static #renderNumberStepper(key, setting, value, onChange, options = {}) { + options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider; + let $text, $btnDec, $btnInc, $range = null, controlValue = value; + const { min: MIN, max: MAX } = setting, STEPS = Math.max(setting.steps || 1, 1), renderTextValue = (value2) => { + value2 = parseInt(value2); + let textContent = null; + if (options.customTextValue) textContent = options.customTextValue(value2); + if (textContent === null) textContent = value2.toString() + options.suffix; + return textContent; + }, updateButtonsVisibility = () => { + $btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX); + }, $wrapper = CE("div", { class: "bx-number-stepper", id: `bx_setting_${key}` }, $btnDec = CE("button", { + "data-type": "dec", + type: "button", + class: options.hideSlider ? "bx-focusable" : "", + tabindex: options.hideSlider ? 0 : -1 + }, "-"), $text = CE("span", {}, renderTextValue(value)), $btnInc = CE("button", { + "data-type": "inc", + type: "button", + class: options.hideSlider ? "bx-focusable" : "", + tabindex: options.hideSlider ? 0 : -1 + }, "+")); + if (options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), $wrapper.disabled = !0, $wrapper; + if ($range = CE("input", { + id: `bx_setting_${key}`, + type: "range", + min: MIN, + max: MAX, + value, + step: STEPS, + tabindex: 0 + }), options.hideSlider && $range.classList.add("bx-gone"), $range.addEventListener("input", (e) => { + if (value = parseInt(e.target.value), controlValue === value) return; + controlValue = value, updateButtonsVisibility(), $text.textContent = renderTextValue(value), !e.ignoreOnChange && onChange && onChange(e, value); + }), $wrapper.addEventListener("input", (e) => { + BxEvent.dispatch($range, "input"); + }), $wrapper.appendChild($range), options.ticks || options.exactTicks) { + const markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId }); + if ($range.setAttribute("list", markersId), options.exactTicks) { + let start = Math.max(Math.floor(MIN / options.exactTicks), 1) * options.exactTicks; + if (start === MIN) start += options.exactTicks; + for (let i = start;i < MAX; i += options.exactTicks) + $markers.appendChild(CE("option", { value: i })); + } else for (let i = MIN + options.ticks;i < MAX; i += options.ticks) + $markers.appendChild(CE("option", { value: i })); + $wrapper.appendChild($markers); + } + updateButtonsVisibility(); + let interval, isHolding = !1; + const onClick = (e) => { + if (isHolding) { + e.preventDefault(), isHolding = !1; + return; + } + const $btn = e.target; + let value2 = parseInt(controlValue); + if ($btn.dataset.type === "dec") value2 = Math.max(MIN, value2 - STEPS); + else value2 = Math.min(MAX, value2 + STEPS); + controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), isHolding = !1, !e.ignoreOnChange && onChange && onChange(e, value2); + }, onMouseDown = (e) => { + e.preventDefault(), isHolding = !0; + const args = arguments; + interval && clearInterval(interval), interval = window.setInterval(() => { + e.target && BxEvent.dispatch(e.target, "click", { + arguments: args + }); + }, 200); + }, onMouseUp = (e) => { + e.preventDefault(), interval && clearInterval(interval), isHolding = !1; + }, onContextMenu = (e) => e.preventDefault(); + return $wrapper.setValue = (value2) => { + $text.textContent = renderTextValue(value2), $range.value = value2; + }, $btnDec.addEventListener("click", onClick), $btnDec.addEventListener("pointerdown", onMouseDown), $btnDec.addEventListener("pointerup", onMouseUp), $btnDec.addEventListener("contextmenu", onContextMenu), $btnInc.addEventListener("click", onClick), $btnInc.addEventListener("pointerdown", onMouseDown), $btnInc.addEventListener("pointerup", onMouseUp), $btnInc.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, { + focus: $range || $btnInc + }), $wrapper; + } + static #METHOD_MAP = { + options: SettingElement.#renderOptions, + "multiple-options": SettingElement.#renderMultipleOptions, + number: SettingElement.#renderNumber, + "number-stepper": SettingElement.#renderNumberStepper, + checkbox: SettingElement.#renderCheckbox + }; + static render(type, key, setting, currentValue, onChange, options) { + const method = SettingElement.#METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1)); + if (type !== "number-stepper") $control.id = `bx_setting_${key}`; + if (type === "options" || type === "multiple-options") $control.name = $control.id; + return $control; + } + static fromPref(key, storage, onChange, overrideParams = {}) { + const definition = storage.getDefinition(key); + let currentValue = storage.getSetting(key), type; + if ("type" in definition) type = definition.type; + else if ("options" in definition) type = "options"; + else if ("multipleOptions" in definition) type = "multiple-options"; + else if (typeof definition.default === "number") type = "number"; + else type = "checkbox"; + let params = {}; + if ("params" in definition) params = Object.assign(overrideParams, definition.params || {}); + if (params.disabled) currentValue = definition.default; + return SettingElement.render(type, key, definition, currentValue, (e, value) => { + storage.setSetting(key, value), onChange && onChange(e, value); + }, params); + } +} +class BaseSettingsStore { + storage; + storageKey; + _settings; + definitions; + constructor(storageKey, definitions) { + this.storage = window.localStorage, this.storageKey = storageKey; + let settingId; + for (settingId in definitions) { + const setting = definitions[settingId]; + if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants]; + setting.ready && setting.ready.call(this, setting); + } + this.definitions = definitions, this._settings = null; + } + get settings() { + if (this._settings) return this._settings; + const settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}"); + return this._settings = settings, settings; + } + getDefinition(key) { + if (!this.definitions[key]) { + const error = "Request invalid definition: " + key; + throw alert(error), Error(error); + } + return this.definitions[key]; + } + getSetting(key, checkUnsupported = !0) { + if (typeof key === "undefined") { + debugger; + return; + } + const definition = this.definitions[key]; + if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return definition.default; + if (checkUnsupported && definition.unsupported) return definition.default; + if (!(key in this.settings)) this.settings[key] = this.validateValue(key, null); + return this.settings[key]; + } + setSetting(key, value, emitEvent = !1) { + return value = this.validateValue(key, value), this.settings[key] = value, this.saveSettings(), emitEvent && BxEvent.dispatch(window, BxEvent.SETTINGS_CHANGED, { + storageKey: this.storageKey, + settingKey: key, + settingValue: value + }), value; + } + saveSettings() { + this.storage.setItem(this.storageKey, JSON.stringify(this.settings)); + } + validateValue(key, value) { + const def = this.definitions[key]; + if (!def) return value; + if (typeof value === "undefined" || value === null) value = def.default; + if ("min" in def) value = Math.max(def.min, value); + if ("max" in def) value = Math.min(def.max, value); + if ("options" in def && !(value in def.options)) value = def.default; + else if ("multipleOptions" in def) { + if (value.length) { + const validOptions = Object.keys(def.multipleOptions); + value.forEach((item2, idx) => { + validOptions.indexOf(item2) === -1 && value.splice(idx, 1); + }); + } + if (!value.length) value = def.default; + } + return value; + } + getLabel(key) { + return this.definitions[key].label || key; + } + getValueText(key, value) { + const definition = this.definitions[key]; + if (definition.type === "number-stepper") { + const params = definition.params; + if (params.customTextValue) { + const text = params.customTextValue(value); + if (text) return text; + } + return value.toString(); + } else if ("options" in definition) { + const options = definition.options; + if (value in options) return options[value]; + } else if (typeof value === "boolean") return value ? t("on") : t("off"); + return value.toString(); + } +} +function getSupportedCodecProfiles() { + const options = { + default: t("default") + }; + if (!("getCapabilities" in RTCRtpReceiver)) return options; + let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1; + const codecs = RTCRtpReceiver.getCapabilities("video").codecs; + for (let codec of codecs) { + if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue; + const fmtp = codec.sdpFmtpLine.toLowerCase(); + if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0; + else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0; + else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0; + } + if (hasLowCodec) if (!hasNormalCodec && !hasHighCodec) options.default = `${t("visual-quality-low")} (${t("default")})`; + else options.low = t("visual-quality-low"); + if (hasNormalCodec) if (!hasLowCodec && !hasHighCodec) options.default = `${t("visual-quality-normal")} (${t("default")})`; + else options.normal = t("visual-quality-normal"); + if (hasHighCodec) if (!hasLowCodec && !hasNormalCodec) options.default = `${t("visual-quality-high")} (${t("default")})`; + else options.high = t("visual-quality-high"); + return options; +} +class GlobalSettingsStorage extends BaseSettingsStore { + static DEFINITIONS = { + version_last_check: { + default: 0 + }, + version_latest: { + default: "" + }, + version_current: { + default: "" + }, + bx_locale: { + label: t("language"), + default: localStorage.getItem("better_xcloud_locale") || "en-US", + options: SUPPORTED_LANGUAGES + }, + server_region: { + label: t("region"), + default: "default" + }, + server_bypass_restriction: { + label: t("bypass-region-restriction"), + note: "⚠️ " + t("use-this-at-your-own-risk"), + default: "off", + optionsGroup: t("region"), + options: Object.assign({ + off: t("off") + }, BypassServers) + }, + stream_preferred_locale: { + label: t("preferred-game-language"), + default: "default", + options: { + default: t("default"), + "ar-SA": "العربية", + "cs-CZ": "čeština", + "da-DK": "dansk", + "de-DE": "Deutsch", + "el-GR": "Ελληνικά", + "en-GB": "English (UK)", + "en-US": "English (US)", + "es-ES": "español (España)", + "es-MX": "español (Latinoamérica)", + "fi-FI": "suomi", + "fr-FR": "français", + "he-IL": "עברית", + "hu-HU": "magyar", + "it-IT": "italiano", + "ja-JP": "日本語", + "ko-KR": "한국어", + "nb-NO": "norsk bokmål", + "nl-NL": "Nederlands", + "pl-PL": "polski", + "pt-BR": "português (Brasil)", + "pt-PT": "português (Portugal)", + "ru-RU": "русский", + "sk-SK": "slovenčina", + "sv-SE": "svenska", + "tr-TR": "Türkçe", + "zh-CN": "中文(简体)", + "zh-TW": "中文 (繁體)" + } + }, + stream_target_resolution: { + label: t("target-resolution"), + default: "auto", + options: { + auto: t("default"), + "720p": "720p", + "1080p": "1080p" + }, + suggest: { + lowest: "720p", + highest: "1080p" + } + }, + stream_codec_profile: { + label: t("visual-quality"), + default: "default", + options: getSupportedCodecProfiles(), + ready: (setting) => { + const options = setting.options, keys = Object.keys(options); + if (keys.length <= 1) setting.unsupported = !0, setting.note = "⚠️ " + t("browser-unsupported-feature"); + setting.suggest = { + lowest: keys.length === 1 ? keys[0] : keys[1], + highest: keys[keys.length - 1] + }; + } + }, + prefer_ipv6_server: { + label: t("prefer-ipv6-server"), + default: !1 + }, + screenshot_apply_filters: { + requiredVariants: "full", + label: t("screenshot-apply-filters"), + default: !1 + }, + skip_splash_video: { + label: t("skip-splash-video"), + default: !1 + }, + hide_dots_icon: { + label: t("hide-system-menu-icon"), + default: !1 + }, + stream_combine_sources: { + requiredVariants: "full", + label: t("combine-audio-video-streams"), + default: !1, + experimental: !0, + note: t("combine-audio-video-streams-summary") + }, + stream_touch_controller: { + requiredVariants: "full", + label: t("tc-availability"), + default: "all", + options: { + default: t("default"), + all: t("tc-all-games"), + off: t("off") + }, + unsupported: !STATES.userAgent.capabilities.touch, + ready: (setting) => { + if (setting.unsupported) setting.default = "default"; + } + }, + stream_touch_controller_auto_off: { + requiredVariants: "full", + label: t("tc-auto-off"), + default: !1, + unsupported: !STATES.userAgent.capabilities.touch + }, + stream_touch_controller_default_opacity: { + requiredVariants: "full", + type: "number-stepper", + label: t("tc-default-opacity"), + default: 100, + min: 10, + max: 100, + steps: 10, + params: { + suffix: "%", + ticks: 10, + hideSlider: !0 + }, + unsupported: !STATES.userAgent.capabilities.touch + }, + stream_touch_controller_style_standard: { + requiredVariants: "full", + label: t("tc-standard-layout-style"), + default: "default", + options: { + default: t("default"), + white: t("tc-all-white"), + muted: t("tc-muted-colors") + }, + unsupported: !STATES.userAgent.capabilities.touch + }, + stream_touch_controller_style_custom: { + requiredVariants: "full", + label: t("tc-custom-layout-style"), + default: "default", + options: { + default: t("default"), + muted: t("tc-muted-colors") + }, + unsupported: !STATES.userAgent.capabilities.touch + }, + stream_simplify_menu: { + label: t("simplify-stream-menu"), + default: !1 + }, + mkb_hide_idle_cursor: { + requiredVariants: "full", + label: t("hide-idle-cursor"), + default: !1 + }, + stream_disable_feedback_dialog: { + requiredVariants: "full", + label: t("disable-post-stream-feedback-dialog"), + default: !1 + }, + bitrate_video_max: { + requiredVariants: "full", + type: "number-stepper", + label: t("bitrate-video-maximum"), + note: "⚠️ " + t("unexpected-behavior"), + default: 0, + min: 0, + max: 14336000, + steps: 102400, + params: { + exactTicks: 5120000, + customTextValue: (value) => { + if (value = parseInt(value), value === 0) return t("unlimited"); + else return (value / 1024000).toFixed(1) + " Mb/s"; + } + }, + suggest: { + highest: 0 + } + }, + game_bar_position: { + requiredVariants: "full", + label: t("position"), + default: "bottom-left", + options: { + "bottom-left": t("bottom-left"), + "bottom-right": t("bottom-right"), + off: t("off") + } + }, + local_co_op_enabled: { + requiredVariants: "full", + label: t("enable-local-co-op-support"), + default: !1, + note: CE("a", { + href: "https://github.com/redphx/better-xcloud/discussions/275", + target: "_blank" + }, t("enable-local-co-op-support-note")) + }, + controller_show_connection_status: { + label: t("show-controller-connection-status"), + default: !0 + }, + controller_enable_shortcuts: { + requiredVariants: "full", + default: !1 + }, + controller_enable_vibration: { + requiredVariants: "full", + label: t("controller-vibration"), + default: !0 + }, + controller_device_vibration: { + requiredVariants: "full", + label: t("device-vibration"), + default: "off", + options: { + on: t("on"), + auto: t("device-vibration-not-using-gamepad"), + off: t("off") + } + }, + controller_vibration_intensity: { + requiredVariants: "full", + label: t("vibration-intensity"), + type: "number-stepper", + default: 100, + min: 0, + max: 100, + steps: 10, + params: { + suffix: "%", + ticks: 10 + } + }, + mkb_enabled: { + requiredVariants: "full", + label: t("enable-mkb"), + default: !1, + unsupported: (() => { + const userAgent2 = (window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase(); + return !AppInterface && userAgent2.match(/(android|iphone|ipad)/) ? t("browser-unsupported-feature") : !1; + })(), + 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.note = CE("a", { + href: url, + target: "_blank" + }, "⚠️ " + note); + } + }, + native_mkb_enabled: { + requiredVariants: "full", + label: t("native-mkb"), + default: "default", + options: { + default: t("default"), + on: t("on"), + off: t("off") + }, + ready: (setting) => { + if (AppInterface) ; + else if (UserAgent.isMobile()) setting.unsupported = !0, setting.default = "off", delete setting.options.default, delete setting.options.on; + else delete setting.options.on; + } + }, + native_mkb_scroll_x_sensitivity: { + requiredVariants: "full", + label: t("horizontal-scroll-sensitivity"), + type: "number-stepper", + default: 0, + min: 0, + max: 1e4, + steps: 10, + params: { + exactTicks: 2000, + customTextValue: (value) => { + if (!value) return t("default"); + return (value / 100).toFixed(1) + "x"; + } + } + }, + native_mkb_scroll_y_sensitivity: { + requiredVariants: "full", + label: t("vertical-scroll-sensitivity"), + type: "number-stepper", + default: 0, + min: 0, + max: 1e4, + steps: 10, + params: { + exactTicks: 2000, + customTextValue: (value) => { + if (!value) return t("default"); + return (value / 100).toFixed(1) + "x"; + } + } + }, + mkb_default_preset_id: { + requiredVariants: "full", + default: 0 + }, + mkb_absolute_mouse: { + requiredVariants: "full", + default: !1 + }, + reduce_animations: { + label: t("reduce-animations"), + default: !1 + }, + ui_loading_screen_game_art: { + requiredVariants: "full", + label: t("show-game-art"), + default: !0 + }, + ui_loading_screen_wait_time: { + label: t("show-wait-time"), + default: !0 + }, + ui_loading_screen_rocket: { + label: t("rocket-animation"), + default: "show", + options: { + show: t("rocket-always-show"), + "hide-queue": t("rocket-hide-queue"), + hide: t("rocket-always-hide") + } + }, + ui_controller_friendly: { + label: t("controller-friendly-ui"), + default: BX_FLAGS.DeviceInfo.deviceType !== "unknown" + }, + ui_layout: { + requiredVariants: "full", + label: t("layout"), + default: "default", + options: { + default: t("default"), + normal: t("normal"), + tv: t("smart-tv") + } + }, + ui_scrollbar_hide: { + label: t("hide-scrollbar"), + default: !1 + }, + ui_home_context_menu_disabled: { + requiredVariants: "full", + label: t("disable-home-context-menu"), + default: STATES.browser.capabilities.touch + }, + ui_hide_sections: { + requiredVariants: "full", + label: t("hide-sections"), + default: [], + multipleOptions: { + news: t("section-news"), + friends: t("section-play-with-friends"), + "native-mkb": t("section-native-mkb"), + touch: t("section-touch"), + "most-popular": t("section-most-popular"), + "all-games": t("section-all-games") + }, + params: { + size: 6 + } + }, + ui_game_card_show_wait_time: { + requiredVariants: "full", + label: t("show-wait-time-in-game-card"), + default: !1 + }, + block_social_features: { + label: t("disable-social-features"), + default: !1 + }, + block_tracking: { + label: t("disable-xcloud-analytics"), + default: !1 + }, + user_agent_profile: { + label: t("user-agent-profile"), + note: "⚠️ " + t("unexpected-behavior"), + default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default", + options: { + default: t("default"), + "windows-edge": "Edge + Windows", + "macos-safari": "Safari + macOS", + "vr-oculus": "Android TV", + "smarttv-generic": "Smart TV", + "smarttv-tizen": "Samsung Smart TV", + custom: t("custom") + } + }, + video_player_type: { + label: t("renderer"), + default: "default", + options: { + default: t("default"), + webgl2: t("webgl2") + }, + suggest: { + lowest: "default", + highest: "webgl2" + } + }, + video_processing: { + label: t("clarity-boost"), + default: "usm", + options: { + usm: t("unsharp-masking"), + cas: t("amd-fidelity-cas") + }, + suggest: { + lowest: "usm", + highest: "cas" + } + }, + video_power_preference: { + label: t("renderer-configuration"), + default: "default", + options: { + default: t("default"), + "low-power": t("battery-saving"), + "high-performance": t("high-performance") + }, + suggest: { + highest: "low-power" + } + }, + video_sharpness: { + label: t("sharpness"), + type: "number-stepper", + default: 0, + min: 0, + max: 10, + params: { + exactTicks: 2, + customTextValue: (value) => { + return value = parseInt(value), value === 0 ? t("off") : value.toString(); + } + }, + suggest: { + lowest: 0, + highest: 4 + } + }, + video_ratio: { + label: t("aspect-ratio"), + note: t("aspect-ratio-note"), + default: "16:9", + options: { + "16:9": "16:9", + "18:9": "18:9", + "21:9": "21:9", + "16:10": "16:10", + "4:3": "4:3", + fill: t("stretch") + } + }, + video_saturation: { + label: t("saturation"), + type: "number-stepper", + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + video_contrast: { + label: t("contrast"), + type: "number-stepper", + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + video_brightness: { + label: t("brightness"), + type: "number-stepper", + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + audio_mic_on_playing: { + label: t("enable-mic-on-startup"), + default: !1 + }, + audio_enable_volume_control: { + requiredVariants: "full", + label: t("enable-volume-control"), + default: !1 + }, + audio_volume: { + label: t("volume"), + type: "number-stepper", + default: 100, + min: 0, + max: 600, + steps: 10, + params: { + suffix: "%", + ticks: 100 + } + }, + stats_items: { + label: t("stats"), + default: ["ping", "fps", "btr", "dt", "pl", "fl"], + multipleOptions: { + ping: `${"ping".toUpperCase()}: ${t("stat-ping")}`, + fps: `${"fps".toUpperCase()}: ${t("stat-fps")}`, + btr: `${"btr".toUpperCase()}: ${t("stat-bitrate")}`, + dt: `${"dt".toUpperCase()}: ${t("stat-decode-time")}`, + pl: `${"pl".toUpperCase()}: ${t("stat-packets-lost")}`, + fl: `${"fl".toUpperCase()}: ${t("stat-frames-lost")}` + }, + params: { + size: 6 + } + }, + stats_show_when_playing: { + label: t("show-stats-on-startup"), + default: !1 + }, + stats_quick_glance: { + label: "👀 " + t("enable-quick-glance-mode"), + default: !0 + }, + stats_position: { + label: t("position"), + default: "top-right", + options: { + "top-left": t("top-left"), + "top-center": t("top-center"), + "top-right": t("top-right") + } + }, + stats_text_size: { + label: t("text-size"), + default: "0.9rem", + options: { + "0.9rem": t("small"), + "1.0rem": t("normal"), + "1.1rem": t("large") + } + }, + stats_transparent: { + label: t("transparent-background"), + default: !1 + }, + stats_opacity: { + label: t("opacity"), + type: "number-stepper", + default: 80, + min: 50, + max: 100, + steps: 10, + params: { + suffix: "%", + ticks: 10 + } + }, + stats_conditional_formatting: { + label: t("conditional-formatting"), + default: !1 + }, + xhome_enabled: { + requiredVariants: "full", + label: t("enable-remote-play-feature"), + default: !1 + }, + xhome_resolution: { + requiredVariants: "full", + default: "1080p", + options: { + "1080p": "1080p", + "720p": "720p" + } + }, + game_fortnite_force_console: { + requiredVariants: "full", + label: "🎮 " + t("fortnite-force-console-version"), + default: !1, + note: t("fortnite-allow-stw-mode") + } + }; + constructor() { + super("better_xcloud", GlobalSettingsStorage.DEFINITIONS); + } +} +var globalSettings = new GlobalSettingsStorage, getPrefDefinition = globalSettings.getDefinition.bind(globalSettings), getPref = globalSettings.getSetting.bind(globalSettings), setPref = globalSettings.setSetting.bind(globalSettings); +STORAGE.Global = globalSettings; +var GamepadKeyName = { + 0: ["A", "⇓"], + 1: ["B", "⇒"], + 2: ["X", "⇐"], + 3: ["Y", "⇑"], + 4: ["LB", "↘"], + 5: ["RB", "↙"], + 6: ["LT", "↖"], + 7: ["RT", "↗"], + 8: ["Select", "⇺"], + 9: ["Start", "⇻"], + 16: ["Home", ""], + 12: ["D-Pad Up", "≻"], + 13: ["D-Pad Down", "≽"], + 14: ["D-Pad Left", "≺"], + 15: ["D-Pad Right", "≼"], + 10: ["L3", "↺"], + 100: ["Left Stick Up", "↾"], + 101: ["Left Stick Down", "⇂"], + 102: ["Left Stick Left", "↼"], + 103: ["Left Stick Right", "⇀"], + 11: ["R3", "↻"], + 200: ["Right Stick Up", "↿"], + 201: ["Right Stick Down", "⇃"], + 202: ["Right Stick Left", "↽"], + 203: ["Right Stick Right", "⇁"] +}; +var MouseMapTo; +((MouseMapTo2) => { + MouseMapTo2[MouseMapTo2.OFF = 0] = "OFF"; + MouseMapTo2[MouseMapTo2.LS = 1] = "LS"; + MouseMapTo2[MouseMapTo2.RS = 2] = "RS"; +})(MouseMapTo ||= {}); +class MkbPreset { + static MOUSE_SETTINGS = { + map_to: { + label: t("map-mouse-to"), + type: "options", + default: MouseMapTo[2], + options: { + [MouseMapTo[2]]: t("right-stick"), + [MouseMapTo[1]]: t("left-stick"), + [MouseMapTo[0]]: t("off") + } + }, + sensitivity_y: { + label: t("horizontal-sensitivity"), + type: "number-stepper", + default: 50, + min: 1, + max: 300, + params: { + suffix: "%", + exactTicks: 50 + } + }, + sensitivity_x: { + label: t("vertical-sensitivity"), + type: "number-stepper", + default: 50, + min: 1, + max: 300, + params: { + suffix: "%", + exactTicks: 50 + } + }, + deadzone_counterweight: { + label: t("deadzone-counterweight"), + type: "number-stepper", + default: 20, + min: 1, + max: 50, + params: { + suffix: "%", + exactTicks: 10 + } + } + }; + static DEFAULT_PRESET = { + mapping: { + 12: ["ArrowUp"], + 13: ["ArrowDown"], + 14: ["ArrowLeft"], + 15: ["ArrowRight"], + 100: ["KeyW"], + 101: ["KeyS"], + 102: ["KeyA"], + 103: ["KeyD"], + 200: ["KeyI"], + 201: ["KeyK"], + 202: ["KeyJ"], + 203: ["KeyL"], + 0: ["Space", "KeyE"], + 2: ["KeyR"], + 1: ["ControlLeft", "Backspace"], + 3: ["KeyV"], + 9: ["Enter"], + 8: ["Tab"], + 4: ["KeyC", "KeyG"], + 5: ["KeyQ"], + 16: ["Backquote"], + 7: ["Mouse0"], + 6: ["Mouse2"], + 10: ["ShiftLeft"], + 11: ["KeyF"] + }, + mouse: { + map_to: MouseMapTo[2], + sensitivity_x: 100, + sensitivity_y: 100, + deadzone_counterweight: 20 + } + }; + static convert(preset) { + const obj = { + mapping: {}, + mouse: Object.assign({}, preset.mouse) + }; + for (let buttonIndex in preset.mapping) + for (let keyName of preset.mapping[parseInt(buttonIndex)]) + obj.mapping[keyName] = parseInt(buttonIndex); + const mouse = obj.mouse; + mouse["sensitivity_x"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["sensitivity_y"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["deadzone_counterweight"] *= EmulatedMkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT; + const mouseMapTo = MouseMapTo[mouse["map_to"]]; + if (typeof mouseMapTo !== "undefined") mouse["map_to"] = mouseMapTo; + else mouse["map_to"] = MkbPreset.MOUSE_SETTINGS["map_to"].default; + return console.log(obj), obj; + } +} +class Toast { + static #$wrapper; + static #$msg; + static #$status; + static #stack = []; + static #isShowing = !1; + static #timeout; + static #DURATION = 3000; + static show(msg, status, options = {}) { + options = options || {}; + const args = Array.from(arguments); + if (options.instant) Toast.#stack = [args], Toast.#showNext(); + else Toast.#stack.push(args), !Toast.#isShowing && Toast.#showNext(); + } + static #showNext() { + if (!Toast.#stack.length) { + Toast.#isShowing = !1; + return; + } + Toast.#isShowing = !0, Toast.#timeout && clearTimeout(Toast.#timeout), Toast.#timeout = window.setTimeout(Toast.#hide, Toast.#DURATION); + const [msg, status, options] = Toast.#stack.shift(); + if (options && options.html) Toast.#$msg.innerHTML = msg; + else Toast.#$msg.textContent = msg; + if (status) Toast.#$status.classList.remove("bx-gone"), Toast.#$status.textContent = status; + else Toast.#$status.classList.add("bx-gone"); + const classList = Toast.#$wrapper.classList; + classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); + } + static #hide() { + Toast.#timeout = null; + const classList = Toast.#$wrapper.classList; + classList.remove("bx-show"), classList.add("bx-hide"); + } + static setup() { + Toast.#$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, Toast.#$msg = CE("span", { class: "bx-toast-msg" }), Toast.#$status = CE("span", { class: "bx-toast-status" })), Toast.#$wrapper.addEventListener("transitionend", (e) => { + const classList = Toast.#$wrapper.classList; + if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), Toast.#showNext(); + }), document.documentElement.appendChild(Toast.#$wrapper); + } +} +class LocalDb { + static #instance; + static get INSTANCE() { + if (!LocalDb.#instance) LocalDb.#instance = new LocalDb; + return LocalDb.#instance; + } + static DB_NAME = "BetterXcloud"; + static DB_VERSION = 1; + static TABLE_PRESETS = "mkb_presets"; + #DB; + #open() { + return new Promise((resolve, reject) => { + if (this.#DB) { + resolve(); + return; + } + const request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION); + request.onupgradeneeded = (e) => { + const db = e.target.result; + switch (e.oldVersion) { + case 0: { + db.createObjectStore(LocalDb.TABLE_PRESETS, { keyPath: "id", autoIncrement: !0 }).createIndex("name_idx", "name"); + break; + } + } + }, request.onerror = (e) => { + console.log(e), alert(e.target.error.message), reject && reject(); + }, request.onsuccess = (e) => { + this.#DB = e.target.result, resolve(); + }; + }); + } + #table(name, type) { + const table = this.#DB.transaction(name, type || "readonly").objectStore(name); + return new Promise((resolve) => resolve(table)); + } + #call(method) { + const table = arguments[1]; + return new Promise((resolve) => { + const request = method.call(table, ...Array.from(arguments).slice(2)); + request.onsuccess = (e) => { + resolve([table, e.target.result]); + }; + }); + } + #count(table) { + return this.#call(table.count, ...arguments); + } + #add(table, data) { + return this.#call(table.add, ...arguments); + } + #put(table, data) { + return this.#call(table.put, ...arguments); + } + #delete(table, data) { + return this.#call(table.delete, ...arguments); + } + #get(table, id2) { + return this.#call(table.get, ...arguments); + } + #getAll(table) { + return this.#call(table.getAll, ...arguments); + } + newPreset(name, data) { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#add(table, { name, data })).then(([table, id2]) => new Promise((resolve) => resolve(id2))); + } + updatePreset(preset) { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#put(table, preset)).then(([table, id2]) => new Promise((resolve) => resolve(id2))); + } + deletePreset(id2) { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#delete(table, id2)).then(([table, id3]) => new Promise((resolve) => resolve(id3))); + } + getPreset(id2) { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#get(table, id2)).then(([table, preset]) => new Promise((resolve) => resolve(preset))); + } + getPresets() { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#count(table)).then(([table, count]) => { + if (count > 0) return new Promise((resolve) => { + this.#getAll(table).then(([table2, items]) => { + const presets = {}; + items.forEach((item2) => presets[item2.id] = item2), resolve(presets); + }); + }); + const preset = { + name: t("default"), + data: MkbPreset.DEFAULT_PRESET + }; + return new Promise((resolve) => { + this.#add(table, preset).then(([table2, id2]) => { + preset.id = id2, setPref("mkb_default_preset_id", id2), resolve({ [id2]: preset }); + }); + }); + }); + } +} +class KeyHelper { + static #NON_PRINTABLE_KEYS = { + Backquote: "`", + Mouse0: "Left Click", + Mouse2: "Right Click", + Mouse1: "Middle Click", + ScrollUp: "Scroll Up", + ScrollDown: "Scroll Down", + ScrollLeft: "Scroll Left", + ScrollRight: "Scroll Right" + }; + static getKeyFromEvent(e) { + let code, name; + if (e instanceof KeyboardEvent) code = e.code || e.key; + else if (e instanceof WheelEvent) { + if (e.deltaY < 0) code = "ScrollUp"; + else if (e.deltaY > 0) code = "ScrollDown"; + else if (e.deltaX < 0) code = "ScrollLeft"; + else if (e.deltaX > 0) code = "ScrollRight"; + } else if (e instanceof MouseEvent) code = "Mouse" + e.button; + if (code) name = KeyHelper.codeToKeyName(code); + return code ? { code, name } : null; + } + static codeToKeyName(code) { + return KeyHelper.#NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code; + } +} +var LOG_TAG = "PointerClient"; +class PointerClient { + static instance; + static getInstance() { + if (!PointerClient.instance) PointerClient.instance = new PointerClient; + return PointerClient.instance; + } + #socket; + #mkbHandler; + start(port, mkbHandler) { + if (!port) throw new Error("PointerServer port is 0"); + this.#mkbHandler = mkbHandler, this.#socket = new WebSocket(`ws://localhost:${port}`), this.#socket.binaryType = "arraybuffer", this.#socket.addEventListener("open", (event) => { + BxLogger.info(LOG_TAG, "connected"); + }), this.#socket.addEventListener("error", (event) => { + BxLogger.error(LOG_TAG, event), Toast.show("Cannot setup mouse: " + event); + }), this.#socket.addEventListener("close", (event) => { + this.#socket = null; + }), this.#socket.addEventListener("message", (event) => { + const dataView = new DataView(event.data); + let messageType = dataView.getInt8(0), offset = Int8Array.BYTES_PER_ELEMENT; + switch (messageType) { + case 1: + this.onMove(dataView, offset); + break; + case 2: + case 3: + this.onPress(messageType, dataView, offset); + break; + case 4: + this.onScroll(dataView, offset); + break; + case 5: + this.onPointerCaptureChanged(dataView, offset); + } + }); + } + onMove(dataView, offset) { + const x = dataView.getInt16(offset); + offset += Int16Array.BYTES_PER_ELEMENT; + const y = dataView.getInt16(offset); + this.#mkbHandler?.handleMouseMove({ + movementX: x, + movementY: y + }); + } + onPress(messageType, dataView, offset) { + const button = dataView.getUint8(offset); + this.#mkbHandler?.handleMouseClick({ + pointerButton: button, + pressed: messageType === 2 + }); + } + onScroll(dataView, offset) { + const vScroll = dataView.getInt16(offset); + offset += Int16Array.BYTES_PER_ELEMENT; + const hScroll = dataView.getInt16(offset); + this.#mkbHandler?.handleMouseWheel({ + vertical: vScroll, + horizontal: hScroll + }); + } + onPointerCaptureChanged(dataView, offset) { + dataView.getInt8(offset) !== 1 && this.#mkbHandler?.stop(); + } + stop() { + try { + this.#socket?.close(); + } catch (e) {} + this.#socket = null; + } +} +class MouseDataProvider { + mkbHandler; + constructor(handler) { + this.mkbHandler = handler; + } +} +class MkbHandler {} +class NativeMkbHandler extends MkbHandler { + static instance; + #pointerClient; + #enabled = !1; + #mouseButtonsPressed = 0; + #mouseWheelX = 0; + #mouseWheelY = 0; + #mouseVerticalMultiply = 0; + #mouseHorizontalMultiply = 0; + #inputSink; + #$message; + static getInstance() { + if (!NativeMkbHandler.instance) NativeMkbHandler.instance = new NativeMkbHandler; + return NativeMkbHandler.instance; + } + #onKeyboardEvent(e) { + if (e.type === "keyup" && e.code === "F8") { + e.preventDefault(), this.toggle(); + return; + } + } + #onPointerLockRequested(e) { + AppInterface.requestPointerCapture(), this.start(); + } + #onPointerLockExited(e) { + AppInterface.releasePointerCapture(), this.stop(); + } + #onPollingModeChanged = (e) => { + if (!this.#$message) return; + if (e.mode === "none") this.#$message.classList.remove("bx-offscreen"); + else this.#$message.classList.add("bx-offscreen"); + }; + #onDialogShown = () => { + document.pointerLockElement && document.exitPointerLock(); + }; + #initMessage() { + if (!this.#$message) this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg" }, CE("div", {}, CE("p", {}, t("native-mkb")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "native" }, createButton({ + style: 1 | 64 | 256, + label: t("activate"), + onClick: ((e) => { + e.preventDefault(), e.stopPropagation(), this.toggle(!0); + }).bind(this) + }), createButton({ + style: 4 | 64, + label: t("ignore"), + onClick: (e) => { + e.preventDefault(), e.stopPropagation(), this.#$message?.classList.add("bx-gone"); + } + }))); + if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); + } + handleEvent(event) { + switch (event.type) { + case "keyup": + this.#onKeyboardEvent(event); + break; + case BxEvent.XCLOUD_DIALOG_SHOWN: + this.#onDialogShown(); + break; + case BxEvent.POINTER_LOCK_REQUESTED: + this.#onPointerLockRequested(event); + break; + case BxEvent.POINTER_LOCK_EXITED: + this.#onPointerLockExited(event); + break; + case BxEvent.XCLOUD_POLLING_MODE_CHANGED: + this.#onPollingModeChanged(event); + break; + } + } + init() { + this.#pointerClient = PointerClient.getInstance(), this.#inputSink = window.BX_EXPOSED.inputSink, this.#updateInputConfigurationAsync(!1); + try { + this.#pointerClient.start(STATES.pointerServerPort, this); + } catch (e) { + Toast.show("Cannot enable Mouse & Keyboard feature"); + } + if (this.#mouseVerticalMultiply = getPref("native_mkb_scroll_y_sensitivity"), this.#mouseHorizontalMultiply = getPref("native_mkb_scroll_x_sensitivity"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), this.#initMessage(), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("native-mkb"), { html: !0 }), this.#$message?.classList.add("bx-gone"); + else this.#$message?.classList.remove("bx-gone"); + } + toggle(force) { + let setEnable; + if (typeof force !== "undefined") setEnable = force; + else setEnable = !this.#enabled; + if (setEnable) document.documentElement.requestPointerLock(); + else document.exitPointerLock(); + } + #updateInputConfigurationAsync(enabled) { + window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({ + enableKeyboardInput: enabled, + enableMouseInput: enabled, + enableAbsoluteMouse: !1, + enableTouchInput: !1 + }); + } + start() { + this.#resetMouseInput(), this.#enabled = !0, this.#updateInputConfigurationAsync(!0), window.BX_EXPOSED.stopTakRendering = !0, this.#$message?.classList.add("bx-gone"), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 }); + } + stop() { + this.#resetMouseInput(), this.#enabled = !1, this.#updateInputConfigurationAsync(!1), this.#$message?.classList.remove("bx-gone"); + } + destroy() { + this.#pointerClient?.stop(), window.removeEventListener("keyup", this), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this), window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), this.#$message?.classList.add("bx-gone"); + } + handleMouseMove(data) { + this.#sendMouseInput({ + X: data.movementX, + Y: data.movementY, + Buttons: this.#mouseButtonsPressed, + WheelX: this.#mouseWheelX, + WheelY: this.#mouseWheelY + }); + } + handleMouseClick(data) { + const { pointerButton, pressed } = data; + if (pressed) this.#mouseButtonsPressed |= pointerButton; + else this.#mouseButtonsPressed ^= pointerButton; + this.#mouseButtonsPressed = Math.max(0, this.#mouseButtonsPressed), this.#sendMouseInput({ + X: 0, + Y: 0, + Buttons: this.#mouseButtonsPressed, + WheelX: this.#mouseWheelX, + WheelY: this.#mouseWheelY + }); + } + handleMouseWheel(data) { + const { vertical, horizontal } = data; + if (this.#mouseWheelX = horizontal, this.#mouseHorizontalMultiply && this.#mouseHorizontalMultiply !== 1) this.#mouseWheelX *= this.#mouseHorizontalMultiply; + if (this.#mouseWheelY = vertical, this.#mouseVerticalMultiply && this.#mouseVerticalMultiply !== 1) this.#mouseWheelY *= this.#mouseVerticalMultiply; + return this.#sendMouseInput({ + X: 0, + Y: 0, + Buttons: this.#mouseButtonsPressed, + WheelX: this.#mouseWheelX, + WheelY: this.#mouseWheelY + }), !0; + } + setVerticalScrollMultiplier(vertical) { + this.#mouseVerticalMultiply = vertical; + } + setHorizontalScrollMultiplier(horizontal) { + this.#mouseHorizontalMultiply = horizontal; + } + waitForMouseData(enabled) {} + isEnabled() { + return this.#enabled; + } + #sendMouseInput(data) { + data.Type = 0, this.#inputSink?.onMouseInput(data); + } + #resetMouseInput() { + this.#mouseButtonsPressed = 0, this.#mouseWheelX = 0, this.#mouseWheelY = 0, this.#sendMouseInput({ + X: 0, + Y: 0, + Buttons: 0, + WheelX: 0, + WheelY: 0 + }); + } +} +function onChangeVideoPlayerType() { + const playerType = getPref("video_player_type"), $videoProcessing = document.getElementById("bx_setting_video_processing"), $videoSharpness = document.getElementById("bx_setting_video_sharpness"), $videoPowerPreference = document.getElementById("bx_setting_video_power_preference"); + if (!$videoProcessing) return; + let isDisabled = !1; + const $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); + if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); + else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; + $videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), updateVideoPlayer(); +} +function updateVideoPlayer() { + const streamPlayer = STATES.currentStream.streamPlayer; + if (!streamPlayer) return; + const options = { + processing: getPref("video_processing"), + sharpness: getPref("video_sharpness"), + saturation: getPref("video_saturation"), + contrast: getPref("video_contrast"), + brightness: getPref("video_brightness") + }; + streamPlayer.setPlayerType(getPref("video_player_type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); +} +window.addEventListener("resize", updateVideoPlayer); +class NavigationDialog { + dialogManager; + constructor() { + this.dialogManager = NavigationDialogManager.getInstance(); + } + show() { + if (NavigationDialogManager.getInstance().show(this), !this.getFocusedElement()) this.focusIfNeeded(); + } + hide() { + NavigationDialogManager.getInstance().hide(); + } + getFocusedElement() { + const $activeElement = document.activeElement; + if (!$activeElement) return null; + if (this.$container.contains($activeElement)) return $activeElement; + return null; + } + onBeforeMount() {} + onMounted() {} + onBeforeUnmount() {} + onUnmounted() {} + handleKeyPress(key) { + return !1; + } + handleGamepad(button) { + return !1; + } +} +class NavigationDialogManager { + static instance; + static getInstance() { + if (!NavigationDialogManager.instance) NavigationDialogManager.instance = new NavigationDialogManager; + return NavigationDialogManager.instance; + } + static GAMEPAD_POLLING_INTERVAL = 50; + static GAMEPAD_KEYS = [ + 12, + 13, + 14, + 15, + 0, + 1, + 4, + 5, + 6, + 7 + ]; + static GAMEPAD_DIRECTION_MAP = { + 12: 1, + 13: 3, + 14: 4, + 15: 2, + 100: 1, + 101: 3, + 102: 4, + 103: 2 + }; + static SIBLING_PROPERTY_MAP = { + horizontal: { + 4: "previousElementSibling", + 2: "nextElementSibling" + }, + vertical: { + 1: "previousElementSibling", + 3: "nextElementSibling" + } + }; + gamepadPollingIntervalId = null; + gamepadLastStates = []; + gamepadHoldingIntervalId = null; + $overlay; + $container; + dialog = null; + constructor() { + if (this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => { + e.preventDefault(), e.stopPropagation(), this.hide(); + }), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), getPref("ui_controller_friendly")) + new MutationObserver((mutationList) => { + if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return; + const $dialog = mutationList[0].addedNodes[0]; + if (!$dialog || !($dialog instanceof HTMLElement)) return; + this.calculateSelectBoxes($dialog); + }).observe(this.$container, { childList: !0 }); + } + calculateSelectBoxes($root) { + $root.querySelectorAll(".bx-select:not([data-calculated]) select").forEach(($select) => { + const $parent = $select.parentElement; + if ($parent.classList.contains("bx-full-width")) { + $parent.dataset.calculated = "true"; + return; + } + const rect = $select.getBoundingClientRect(); + let $label, width = Math.ceil(rect.width); + if (!width) return; + if ($select.multiple) $label = $parent.querySelector(".bx-select-value"), width += 20; + else $label = $parent.querySelector("div"); + $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; + }); + } + handleEvent(event) { + switch (event.type) { + case "keydown": + const $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key; + let handled = this.dialog?.handleKeyPress(keyCode); + if (handled) { + event.preventDefault(), event.stopPropagation(); + return; + } + if (keyCode === "ArrowUp" || keyCode === "ArrowDown") handled = !0, this.focusDirection(keyCode === "ArrowUp" ? 1 : 3); + else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") { + if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2); + } else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") { + if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click")); + } else if (keyCode === "Escape") handled = !0, this.hide(); + if (handled) event.preventDefault(), event.stopPropagation(); + break; + } + } + isShowing() { + return this.$container && !this.$container.classList.contains("bx-gone"); + } + pollGamepad() { + const gamepads = window.navigator.getGamepads(); + for (let gamepad of gamepads) { + if (!gamepad || !gamepad.connected) continue; + if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) continue; + const { axes, buttons } = gamepad; + let releasedButton = null, heldButton = null, lastState = this.gamepadLastStates[gamepad.index], lastTimestamp, lastKey, lastKeyPressed; + if (lastState) [lastTimestamp, lastKey, lastKeyPressed] = lastState; + if (lastTimestamp && lastTimestamp === gamepad.timestamp) continue; + for (let key of NavigationDialogManager.GAMEPAD_KEYS) + if (lastKey === key && !buttons[key].pressed) { + releasedButton = key; + break; + } else if (buttons[key].pressed) { + heldButton = key; + break; + } + if (heldButton === null && releasedButton === null && axes && axes.length >= 2) { + if (lastKey) { + const releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastKey === 102 || lastKey === 103), releasedVertical = Math.abs(axes[1]) < 0.1 && (lastKey === 100 || lastKey === 101); + if (releasedHorizontal || releasedVertical) releasedButton = lastKey; + else heldButton = lastKey; + } else if (axes[0] < -0.5) heldButton = 102; + else if (axes[0] > 0.5) heldButton = 103; + else if (axes[1] < -0.5) heldButton = 100; + else if (axes[1] > 0.5) heldButton = 101; + } + if (heldButton !== null) { + if (this.gamepadLastStates[gamepad.index] = [gamepad.timestamp, heldButton, !1], this.clearGamepadHoldingInterval(), NavigationDialogManager.GAMEPAD_DIRECTION_MAP[heldButton]) this.gamepadHoldingIntervalId = window.setInterval(() => { + const lastState2 = this.gamepadLastStates[gamepad.index]; + if (lastState2) { + if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) { + this.handleGamepad(gamepad, heldButton); + return; + } + } + this.clearGamepadHoldingInterval(); + }, 200); + continue; + } + if (releasedButton === null) { + this.clearGamepadHoldingInterval(); + continue; + } + if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return; + if (releasedButton === 0) { + document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click")); + return; + } else if (releasedButton === 1) { + this.hide(); + return; + } + if (this.handleGamepad(gamepad, releasedButton)) return; + } + } + handleGamepad(gamepad, key) { + let handled = this.dialog?.handleGamepad(key); + if (handled) return !0; + let direction = NavigationDialogManager.GAMEPAD_DIRECTION_MAP[key]; + if (!direction) return !1; + if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === "range") { + const $range = document.activeElement; + if (direction === 4 || direction === 2) $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === 4 ? -1 : 1)).toString(), $range.dispatchEvent(new InputEvent("input")), handled = !0; + } + if (!handled) this.focusDirection(direction); + return this.gamepadLastStates[gamepad.index] && (this.gamepadLastStates[gamepad.index][2] = !0), !0; + } + clearGamepadHoldingInterval() { + this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = null; + } + show(dialog) { + if (this.clearGamepadHoldingInterval(), BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.$overlay.classList.remove("bx-gone"), STATES.isPlaying) this.$overlay.classList.add("bx-invisible"); + this.unmountCurrentDialog(), this.dialog = dialog, dialog.onBeforeMount(), this.$container.appendChild(dialog.getContent()), dialog.onMounted(), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling(); + } + hide() { + this.clearGamepadHoldingInterval(), document.body.classList.remove("bx-no-scroll"), BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_DISMISSED), this.$overlay.classList.add("bx-gone"), this.$overlay.classList.remove("bx-invisible"), this.$container.classList.add("bx-gone"), this.$container.removeEventListener("keydown", this), this.stopGamepadPolling(), this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1; + } + focus($elm) { + if (!$elm) return !1; + if ($elm.nearby && $elm.nearby.focus) if ($elm.nearby.focus instanceof HTMLElement) return this.focus($elm.nearby.focus); + else return $elm.nearby.focus(); + return $elm.focus(), $elm === document.activeElement; + } + getOrientation($elm) { + const nearby = $elm.nearby || {}; + if (nearby.selfOrientation) return nearby.selfOrientation; + let orientation, $current = $elm.parentElement; + while ($current !== this.$container) { + const tmp = $current.nearby?.orientation; + if ($current.nearby && tmp) { + orientation = tmp; + break; + } + $current = $current.parentElement; + } + return orientation = orientation || "vertical", setNearby($elm, { + selfOrientation: orientation + }), orientation; + } + findNextTarget($focusing, direction, checkParent = !1, checked = []) { + if (!$focusing || $focusing === this.$container) return null; + if (checked.includes($focusing)) return null; + checked.push($focusing); + let $target = $focusing; + const $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target); + let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction]; + if (siblingProperty) { + let $sibling = $target; + while ($sibling[siblingProperty]) { + $sibling = $sibling[siblingProperty]; + const $focusable = this.findFocusableElement($sibling, direction); + if ($focusable) return $focusable; + } + } + if (nearby.loop) { + if (nearby.loop(direction)) return null; + } + if (checkParent) return this.findNextTarget($parent, direction, checkParent, checked); + return null; + } + findFocusableElement($elm, direction) { + if (!$elm) return null; + if (!!$elm.disabled) return null; + if (!isElementVisible($elm)) return null; + if ($elm.tabIndex > -1) return $elm; + const focus = $elm.nearby?.focus; + if (focus) { + if (focus instanceof HTMLElement) return this.findFocusableElement(focus, direction); + else if (typeof focus === "function") { + if (focus()) return document.activeElement; + } + } + const children = Array.from($elm.children), orientation = $elm.nearby?.orientation || "vertical"; + if (orientation === "horizontal" || orientation === "vertical" && direction === 1) children.reverse(); + for (let $child of children) { + if (!$child || !($child instanceof HTMLElement)) return null; + const $target = this.findFocusableElement($child, direction); + if ($target) return $target; + } + return null; + } + startGamepadPolling() { + this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad.bind(this), NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); + } + stopGamepadPolling() { + this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null; + } + focusDirection(direction) { + const dialog = this.dialog; + if (!dialog) return; + const $focusing = dialog.getFocusedElement(); + if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null; + const $target = this.findNextTarget($focusing, direction, !0); + this.focus($target); + } + unmountCurrentDialog() { + const dialog = this.dialog; + dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null; + } +} +var BxIcon = { + BETTER_XCLOUD: "", + TRUE_ACHIEVEMENTS: "", + STREAM_SETTINGS: "", + STREAM_STATS: "", + CLOSE: "", + COMMAND: "", + CONTROLLER: "", + CREATE_SHORTCUT: "", + DISPLAY: "", + HOME: "", + NATIVE_MKB: "", + NEW: "", + COPY: "", + TRASH: "", + CURSOR_TEXT: "", + POWER: "", + QUESTION: "", + REFRESH: "", + VIRTUAL_CONTROLLER: "", + REMOTE_PLAY: "", + CARET_LEFT: "", + CARET_RIGHT: "", + SCREENSHOT: "", + SPEAKER_MUTED: "", + TOUCH_CONTROL_ENABLE: "", + TOUCH_CONTROL_DISABLE: "", + MICROPHONE: "", + MICROPHONE_MUTED: "", + BATTERY: "", + PLAYTIME: "", + SERVER: "", + DOWNLOAD: "", + UPLOAD: "", + AUDIO: "" +}; +class Dialog { + $dialog; + $title; + $content; + $overlay; + onClose; + constructor(options) { + const { + title, + className, + content, + hideCloseButton, + onClose, + helpUrl + } = options, $overlay = document.querySelector(".bx-dialog-overlay"); + if (!$overlay) this.$overlay = CE("div", { class: "bx-dialog-overlay bx-gone" }), this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$overlay); + else this.$overlay = $overlay; + let $close; + this.onClose = onClose, this.$dialog = CE("div", { class: `bx-dialog ${className || ""} bx-gone` }, this.$title = CE("h2", {}, CE("b", {}, title), helpUrl && createButton({ + icon: BxIcon.QUESTION, + style: 4, + title: t("help"), + url: helpUrl + })), this.$content = CE("div", { class: "bx-dialog-content" }, content), !hideCloseButton && ($close = CE("button", { type: "button" }, t("close")))), $close && $close.addEventListener("click", (e) => { + this.hide(e); + }), !title && this.$title.classList.add("bx-gone"), !content && this.$content.classList.add("bx-gone"), this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$dialog); + } + show(newOptions) { + if (document.activeElement && document.activeElement.blur(), newOptions && newOptions.title) this.$title.querySelector("b").textContent = newOptions.title, this.$title.classList.remove("bx-gone"); + this.$dialog.classList.remove("bx-gone"), this.$overlay.classList.remove("bx-gone"), document.body.classList.add("bx-no-scroll"); + } + hide(e) { + this.$dialog.classList.add("bx-gone"), this.$overlay.classList.add("bx-gone"), document.body.classList.remove("bx-no-scroll"), this.onClose && this.onClose(e); + } + toggle() { + this.$dialog.classList.toggle("bx-gone"), this.$overlay.classList.toggle("bx-gone"); + } +} +class MkbRemapper { + #BUTTON_ORDERS = [ + 12, + 13, + 14, + 15, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 16, + 10, + 100, + 101, + 102, + 103, + 11, + 200, + 201, + 202, + 203 + ]; + static #instance; + static get INSTANCE() { + if (!MkbRemapper.#instance) MkbRemapper.#instance = new MkbRemapper; + return MkbRemapper.#instance; + } + #STATE = { + currentPresetId: 0, + presets: {}, + editingPresetData: null, + isEditing: !1 + }; + #$ = { + wrapper: null, + presetsSelect: null, + activateButton: null, + currentBindingKey: null, + allKeyElements: [], + allMouseElements: {} + }; + bindingDialog; + constructor() { + this.#STATE.currentPresetId = getPref("mkb_default_preset_id"), this.bindingDialog = new Dialog({ + className: "bx-binding-dialog", + content: CE("div", {}, CE("p", {}, t("press-to-bind")), CE("i", {}, t("press-esc-to-cancel"))), + hideCloseButton: !0 + }); + } + #clearEventListeners = () => { + window.removeEventListener("keydown", this.#onKeyDown), window.removeEventListener("mousedown", this.#onMouseDown), window.removeEventListener("wheel", this.#onWheel); + }; + #bindKey = ($elm, key) => { + const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")); + if ($elm.getAttribute("data-key-code") === key.code) return; + for (let $otherElm of this.#$.allKeyElements) + if ($otherElm.getAttribute("data-key-code") === key.code) this.#unbindKey($otherElm); + this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.setAttribute("data-key-code", key.code); + }; + #unbindKey = ($elm) => { + const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")); + this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", $elm.removeAttribute("data-key-code"); + }; + #onWheel = (e) => { + e.preventDefault(), this.#clearEventListeners(), this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200); + }; + #onMouseDown = (e) => { + e.preventDefault(), this.#clearEventListeners(), this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200); + }; + #onKeyDown = (e) => { + if (e.preventDefault(), e.stopPropagation(), this.#clearEventListeners(), e.code !== "Escape") this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)); + window.setTimeout(() => this.bindingDialog.hide(), 200); + }; + #onBindingKey = (e) => { + if (!this.#STATE.isEditing || e.button !== 0) return; + console.log(e), this.#$.currentBindingKey = e.target, window.addEventListener("keydown", this.#onKeyDown), window.addEventListener("mousedown", this.#onMouseDown), window.addEventListener("wheel", this.#onWheel), this.bindingDialog.show({ title: this.#$.currentBindingKey.getAttribute("data-prompt") }); + }; + #onContextMenu = (e) => { + if (e.preventDefault(), !this.#STATE.isEditing) return; + this.#unbindKey(e.target); + }; + #getPreset = (presetId) => { + return this.#STATE.presets[presetId]; + }; + #getCurrentPreset = () => { + return this.#getPreset(this.#STATE.currentPresetId); + }; + #switchPreset = (presetId) => { + this.#STATE.currentPresetId = presetId; + const presetData = this.#getCurrentPreset().data; + for (let $elm of this.#$.allKeyElements) { + const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")), buttonKeys = presetData.mapping[buttonIndex]; + if (buttonKeys && buttonKeys[keySlot]) $elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]), $elm.setAttribute("data-key-code", buttonKeys[keySlot]); + else $elm.textContent = "", $elm.removeAttribute("data-key-code"); + } + let key; + for (key in this.#$.allMouseElements) { + const $elm = this.#$.allMouseElements[key]; + let value = presetData.mouse[key]; + if (typeof value === "undefined") value = MkbPreset.MOUSE_SETTINGS[key].default; + "setValue" in $elm && $elm.setValue(value); + } + const activated = getPref("mkb_default_preset_id") === this.#STATE.currentPresetId; + this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"); + }; + #refresh() { + while (this.#$.presetsSelect.firstChild) + this.#$.presetsSelect.removeChild(this.#$.presetsSelect.firstChild); + LocalDb.INSTANCE.getPresets().then((presets) => { + this.#STATE.presets = presets; + const $fragment = document.createDocumentFragment(); + let defaultPresetId; + if (this.#STATE.currentPresetId === 0) this.#STATE.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.#STATE.currentPresetId, setPref("mkb_default_preset_id", defaultPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(); + else defaultPresetId = getPref("mkb_default_preset_id"); + for (let id2 in presets) { + let name = presets[id2].name; + if (id2 === defaultPresetId) name = "🎮 " + name; + const $options = CE("option", { value: id2 }, name); + $options.selected = parseInt(id2) === this.#STATE.currentPresetId, $fragment.appendChild($options); + } + this.#$.presetsSelect.appendChild($fragment); + const activated = defaultPresetId === this.#STATE.currentPresetId; + this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.#STATE.isEditing && this.#switchPreset(this.#STATE.currentPresetId); + }); + } + #toggleEditing = (force) => { + if (this.#STATE.isEditing = typeof force !== "undefined" ? force : !this.#STATE.isEditing, this.#$.wrapper.classList.toggle("bx-editing", this.#STATE.isEditing), this.#STATE.isEditing) this.#STATE.editingPresetData = deepClone(this.#getCurrentPreset().data); + else this.#STATE.editingPresetData = null; + const childElements = this.#$.wrapper.querySelectorAll("select, button, input"); + for (let $elm of Array.from(childElements)) { + if ($elm.parentElement.parentElement.classList.contains("bx-mkb-action-buttons")) continue; + let disable = !this.#STATE.isEditing; + if ($elm.parentElement.classList.contains("bx-mkb-preset-tools")) disable = !disable; + $elm.disabled = disable; + } + }; + render() { + this.#$.wrapper = CE("div", { class: "bx-mkb-settings" }), this.#$.presetsSelect = CE("select", { tabindex: -1 }), this.#$.presetsSelect.addEventListener("change", (e) => { + this.#switchPreset(parseInt(e.target.value)); + }); + const promptNewName = (value) => { + let newName = ""; + while (!newName) { + if (newName = prompt(t("prompt-preset-name"), value), newName === null) return !1; + newName = newName.trim(); + } + return newName ? newName : !1; + }, $header = CE("div", { class: "bx-mkb-preset-tools" }, this.#$.presetsSelect, createButton({ + title: t("rename"), + icon: BxIcon.CURSOR_TEXT, + tabIndex: -1, + onClick: (e) => { + const preset = this.#getCurrentPreset(); + let newName = promptNewName(preset.name); + if (!newName || newName === preset.name) return; + preset.name = newName, LocalDb.INSTANCE.updatePreset(preset).then((id2) => this.#refresh()); + } + }), createButton({ + icon: BxIcon.NEW, + title: t("new"), + tabIndex: -1, + onClick: (e) => { + let newName = promptNewName(""); + if (!newName) return; + LocalDb.INSTANCE.newPreset(newName, MkbPreset.DEFAULT_PRESET).then((id2) => { + this.#STATE.currentPresetId = id2, this.#refresh(); + }); + } + }), createButton({ + icon: BxIcon.COPY, + title: t("copy"), + tabIndex: -1, + onClick: (e) => { + const preset = this.#getCurrentPreset(); + let newName = promptNewName(`${preset.name} (2)`); + if (!newName) return; + LocalDb.INSTANCE.newPreset(newName, preset.data).then((id2) => { + this.#STATE.currentPresetId = id2, this.#refresh(); + }); + } + }), createButton({ + icon: BxIcon.TRASH, + style: 2, + title: t("delete"), + tabIndex: -1, + onClick: (e) => { + if (!confirm(t("confirm-delete-preset"))) return; + LocalDb.INSTANCE.deletePreset(this.#STATE.currentPresetId).then((id2) => { + this.#STATE.currentPresetId = 0, this.#refresh(); + }); + } + })); + this.#$.wrapper.appendChild($header); + const $rows = CE("div", { class: "bx-mkb-settings-rows" }, CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind"))), keysPerButton = 2; + for (let buttonIndex of this.#BUTTON_ORDERS) { + const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex]; + let $elm; + const $fragment = document.createDocumentFragment(); + for (let i = 0;i < keysPerButton; i++) + $elm = CE("button", { + type: "button", + "data-prompt": buttonPrompt, + "data-button-index": buttonIndex, + "data-key-slot": i + }, " "), $elm.addEventListener("mouseup", this.#onBindingKey), $elm.addEventListener("contextmenu", this.#onContextMenu), $fragment.appendChild($elm), this.#$.allKeyElements.push($elm); + const $keyRow = CE("div", { class: "bx-mkb-key-row" }, CE("label", { title: buttonName }, buttonPrompt), $fragment); + $rows.appendChild($keyRow); + } + $rows.appendChild(CE("i", { class: "bx-mkb-note" }, t("mkb-adjust-ingame-settings"))); + const $mouseSettings = document.createDocumentFragment(); + for (let key in MkbPreset.MOUSE_SETTINGS) { + const setting = MkbPreset.MOUSE_SETTINGS[key], value = setting.default; + let $elm; + const onChange = (e, value2) => { + this.#STATE.editingPresetData.mouse[key] = value2; + }, $row = CE("label", { + class: "bx-settings-row", + for: `bx_setting_${key}` + }, CE("span", { class: "bx-settings-label" }, setting.label), $elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params)); + $mouseSettings.appendChild($row), this.#$.allMouseElements[key] = $elm; + } + $rows.appendChild($mouseSettings), this.#$.wrapper.appendChild($rows); + const $actionButtons = CE("div", { class: "bx-mkb-action-buttons" }, CE("div", {}, createButton({ + label: t("edit"), + tabIndex: -1, + onClick: (e) => this.#toggleEditing(!0) + }), this.#$.activateButton = createButton({ + label: t("activate"), + style: 1, + tabIndex: -1, + onClick: (e) => { + setPref("mkb_default_preset_id", this.#STATE.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.#refresh(); + } + })), CE("div", {}, createButton({ + label: t("cancel"), + style: 4, + tabIndex: -1, + onClick: (e) => { + this.#switchPreset(this.#STATE.currentPresetId), this.#toggleEditing(!1); + } + }), createButton({ + label: t("save"), + style: 1, + tabIndex: -1, + onClick: (e) => { + const updatedPreset = deepClone(this.#getCurrentPreset()); + updatedPreset.data = this.#STATE.editingPresetData, LocalDb.INSTANCE.updatePreset(updatedPreset).then((id2) => { + if (id2 === getPref("mkb_default_preset_id")) EmulatedMkbHandler.getInstance().refreshPresetData(); + this.#toggleEditing(!1), this.#refresh(); + }); + } + }))); + return this.#$.wrapper.appendChild($actionButtons), this.#toggleEditing(!1), this.#refresh(), this.#$.wrapper; + } +} +function hashCode(str) { + let hash = 0; + for (let i = 0, len = str.length;i < len; i++) { + const chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr, hash |= 0; + } + return hash; +} +function renderString(str, obj) { + return str.replace(/\$\{.+?\}/g, (match) => { + const key = match.substring(2, match.length - 1); + if (key in obj) return obj[key]; + return match; + }); +} +function ceilToNearest(value, interval) { + return Math.ceil(value / interval) * interval; +} +function floorToNearest(value, interval) { + return Math.floor(value / interval) * interval; +} +async function copyToClipboard(text, showToast = !0) { + try { + return await navigator.clipboard.writeText(text), showToast && Toast.show("Copied to clipboard", "", { instant: !0 }), !0; + } catch (err) { + console.error("Failed to copy: ", err), showToast && Toast.show("Failed to copy", "", { instant: !0 }); + } + return !1; +} +function productTitleToSlug(title) { + return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase(); +} +class SoundShortcut { + static adjustGainNodeVolume(amount) { + if (!getPref("audio_enable_volume_control")) return 0; + const currentValue = getPref("audio_volume"); + let nearestValue; + if (amount > 0) nearestValue = ceilToNearest(currentValue, amount); + else nearestValue = floorToNearest(currentValue, -1 * amount); + let newValue; + if (currentValue !== nearestValue) newValue = nearestValue; + else newValue = currentValue + amount; + return newValue = setPref("audio_volume", newValue, !0), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue; + } + static setGainNodeVolume(value) { + STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); + } + static muteUnmute() { + if (getPref("audio_enable_volume_control") && STATES.currentStream.audioGainNode) { + const gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio_volume"); + let targetValue; + if (settingValue === 0) targetValue = 100, setPref("audio_volume", targetValue, !0); + else if (gainValue === 0) targetValue = settingValue; + else targetValue = 0; + let status; + if (targetValue === 0) status = t("muted"); + else status = targetValue + "%"; + SoundShortcut.setGainNodeVolume(targetValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { + speakerState: targetValue === 0 ? 1 : 0 + }); + return; + } + let $media; + if ($media = document.querySelector("div[data-testid=media-container] audio"), !$media) $media = document.querySelector("div[data-testid=media-container] video"); + if ($media) { + $media.muted = !$media.muted; + const status = $media.muted ? t("muted") : t("unmuted"); + Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { + speakerState: $media.muted ? 1 : 0 + }); + } + } +} +var LOG_TAG2 = "TouchController"; +class TouchController { + static #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent("message", { + data: JSON.stringify({ + content: '{"layoutId":""}', + target: "/streaming/touchcontrols/showlayoutv2", + type: "Message" + }), + origin: "better-xcloud" + }); + static #$style; + static #enabled = !1; + static #dataChannel; + static #customLayouts = {}; + static #baseCustomLayouts = {}; + static #currentLayoutId; + static #customList; + static #xboxTitleId = null; + static setXboxTitleId(xboxTitleId) { + TouchController.#xboxTitleId = xboxTitleId; + } + static getCustomLayouts() { + const xboxTitleId = TouchController.#xboxTitleId; + if (!xboxTitleId) return null; + return TouchController.#customLayouts[xboxTitleId]; + } + static enable() { + TouchController.#enabled = !0; + } + static disable() { + TouchController.#enabled = !1; + } + static isEnabled() { + return TouchController.#enabled; + } + static #showDefault() { + TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER); + } + static #show() { + document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.remove("bx-offscreen"); + } + static #hide() { + document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.add("bx-offscreen"); + } + static toggleVisibility(status) { + if (!TouchController.#dataChannel) return; + status ? TouchController.#hide() : TouchController.#show(); + } + static reset() { + TouchController.#enabled = !1, TouchController.#dataChannel = null, TouchController.#xboxTitleId = null, TouchController.#$style && (TouchController.#$style.textContent = ""); + } + static #dispatchMessage(msg) { + TouchController.#dataChannel && window.setTimeout(() => { + TouchController.#dataChannel.dispatchEvent(msg); + }, 10); + } + static #dispatchLayouts(data) { + TouchController.applyCustomLayout(null, 1000), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); + } + static async requestCustomLayouts(retries = 1) { + const xboxTitleId = TouchController.#xboxTitleId; + if (!xboxTitleId) return; + if (xboxTitleId in TouchController.#customLayouts) { + TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]); + return; + } + if (retries = retries || 1, retries > 2) { + TouchController.#customLayouts[xboxTitleId] = null, window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000); + return; + } + const baseUrl = "https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts", url = `${baseUrl}/${xboxTitleId}.json`; + try { + const json = await (await NATIVE_FETCH(url)).json(), layouts = {}; + json.layouts.forEach(async (layoutName) => { + let baseLayouts = {}; + if (layoutName in TouchController.#baseCustomLayouts) baseLayouts = TouchController.#baseCustomLayouts[layoutName]; + else try { + const layoutUrl = `${baseUrl}/layouts/${layoutName}.json`; + baseLayouts = (await (await NATIVE_FETCH(layoutUrl)).json()).layouts, TouchController.#baseCustomLayouts[layoutName] = baseLayouts; + } catch (e) {} + Object.assign(layouts, baseLayouts); + }), json.layouts = layouts, TouchController.#customLayouts[xboxTitleId] = json, window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000); + } catch (e) { + TouchController.requestCustomLayouts(retries + 1); + } + } + static applyCustomLayout(layoutId, delay = 0) { + if (!window.BX_EXPOSED.touchLayoutManager) { + const listener = (e) => { + if (window.removeEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener), TouchController.#enabled) TouchController.applyCustomLayout(layoutId, 0); + }; + window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener); + return; + } + const xboxTitleId = TouchController.#xboxTitleId; + if (!xboxTitleId) { + BxLogger.error(LOG_TAG2, "Invalid xboxTitleId"); + return; + } + if (!layoutId) layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null; + if (!layoutId) { + BxLogger.error(LOG_TAG2, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault(); + return; + } + const layoutChanged = TouchController.#currentLayoutId !== layoutId; + TouchController.#currentLayoutId = layoutId; + const layoutData = TouchController.#customLayouts[xboxTitleId]; + if (!xboxTitleId || !layoutId || !layoutData) { + TouchController.#enabled && TouchController.#showDefault(); + return; + } + const layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout]; + if (!layout) return; + let msg, html = !1; + if (layout.author) { + const author = `${escapeHtml(layout.author)}`; + msg = t("touch-control-layout-by", { name: author }), html = !0; + } else msg = t("touch-control-layout"); + layoutChanged && Toast.show(msg, layout.name, { html }), window.setTimeout(() => { + window.BX_EXPOSED.shouldShowSensorControls = JSON.stringify(layout).includes("gyroscope"), window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({ + type: "showLayout", + scope: xboxTitleId, + subscope: "base", + layout: { + id: "System.Standard", + displayName: "System", + layoutFile: layout + } + }); + }, delay); + } + static updateCustomList() { + TouchController.#customList = JSON.parse(window.localStorage.getItem("better_xcloud_custom_touch_layouts") || "[]"), NATIVE_FETCH("https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/ids.json").then((response) => response.json()).then((json) => { + TouchController.#customList = json, window.localStorage.setItem("better_xcloud_custom_touch_layouts", JSON.stringify(json)); + }); + } + static getCustomList() { + return TouchController.#customList; + } + static setup() { + window.testTouchLayout = (layout) => { + const { touchLayoutManager } = window.BX_EXPOSED; + touchLayoutManager && touchLayoutManager.changeLayoutForScope({ + type: "showLayout", + scope: "" + TouchController.#xboxTitleId, + subscope: "base", + layout: { + id: "System.Standard", + displayName: "Custom", + layoutFile: layout + } + }); + }; + const $style = document.createElement("style"); + document.documentElement.appendChild($style), TouchController.#$style = $style; + const PREF_STYLE_STANDARD = getPref("stream_touch_controller_style_standard"), PREF_STYLE_CUSTOM = getPref("stream_touch_controller_style_custom"); + window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { + const dataChannel = e.dataChannel; + if (!dataChannel || dataChannel.label !== "message") return; + let filter = ""; + if (TouchController.#enabled) { + if (PREF_STYLE_STANDARD === "white") filter = "grayscale(1) brightness(2)"; + else if (PREF_STYLE_STANDARD === "muted") filter = "sepia(0.5)"; + } else if (PREF_STYLE_CUSTOM === "muted") filter = "sepia(0.5)"; + if (filter) $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`; + else $style.textContent = ""; + TouchController.#dataChannel = dataChannel, dataChannel.addEventListener("open", () => { + window.setTimeout(TouchController.#show, 1000); + }); + let focused = !1; + dataChannel.addEventListener("message", (msg) => { + if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return; + if (msg.data.includes("touchcontrols/showtitledefault")) { + if (TouchController.#enabled) if (focused) TouchController.requestCustomLayouts(); + else TouchController.#showDefault(); + return; + } + try { + if (msg.data.includes("/titleinfo")) { + const json = JSON.parse(JSON.parse(msg.data).content); + if (focused = json.focused, !json.focused) TouchController.#show(); + TouchController.setXboxTitleId(parseInt(json.titleid, 16).toString()); + } + } catch (e2) { + BxLogger.error(LOG_TAG2, "Load custom layout", e2); + } + }); + }); + } +} +var VIBRATION_DATA_MAP = { + gamepadIndex: 8, + leftMotorPercent: 8, + rightMotorPercent: 8, + leftTriggerMotorPercent: 8, + rightTriggerMotorPercent: 8, + durationMs: 16 +}; +class VibrationManager { + static #playDeviceVibration(data) { + if (AppInterface) { + AppInterface.vibrate(JSON.stringify(data), window.BX_VIBRATION_INTENSITY); + return; + } + const intensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * window.BX_VIBRATION_INTENSITY; + if (intensity === 0 || intensity === 100) { + window.navigator.vibrate(intensity ? data.durationMs : 0); + return; + } + const pulseDuration = 200, onDuration = Math.floor(pulseDuration * intensity / 100), offDuration = pulseDuration - onDuration, repeats = Math.ceil(data.durationMs / pulseDuration), pulses = Array(repeats).fill([onDuration, offDuration]).flat(); + window.navigator.vibrate(pulses); + } + static supportControllerVibration() { + return Gamepad.prototype.hasOwnProperty("vibrationActuator"); + } + static supportDeviceVibration() { + return !!window.navigator.vibrate; + } + static updateGlobalVars(stopVibration = !0) { + if (window.BX_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? getPref("controller_enable_vibration") : !1, window.BX_VIBRATION_INTENSITY = getPref("controller_vibration_intensity") / 100, !VibrationManager.supportDeviceVibration()) { + window.BX_ENABLE_DEVICE_VIBRATION = !1; + return; + } + stopVibration && window.navigator.vibrate(0); + const value = getPref("controller_device_vibration"); + let enabled; + if (value === "on") enabled = !0; + else if (value === "auto") { + enabled = !0; + const gamepads = window.navigator.getGamepads(); + for (let gamepad of gamepads) + if (gamepad) { + enabled = !1; + break; + } + } else enabled = !1; + window.BX_ENABLE_DEVICE_VIBRATION = enabled; + } + static #onMessage(e) { + if (!window.BX_ENABLE_DEVICE_VIBRATION) return; + if (typeof e !== "object" || !(e.data instanceof ArrayBuffer)) return; + const dataView = new DataView(e.data); + let offset = 0, messageType; + if (dataView.byteLength === 13) messageType = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT; + else messageType = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT; + if (!(messageType & 128)) return; + const vibrationType = dataView.getUint8(offset); + if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0) return; + const data = {}; + let key; + for (key in VIBRATION_DATA_MAP) + if (VIBRATION_DATA_MAP[key] === 16) data[key] = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT; + else data[key] = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT; + VibrationManager.#playDeviceVibration(data); + } + static initialSetup() { + window.addEventListener("gamepadconnected", (e) => VibrationManager.updateGlobalVars()), window.addEventListener("gamepaddisconnected", (e) => VibrationManager.updateGlobalVars()), VibrationManager.updateGlobalVars(!1), window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { + const dataChannel = e.dataChannel; + if (!dataChannel || dataChannel.label !== "input") return; + dataChannel.addEventListener("message", VibrationManager.#onMessage); + }); + } +} +class BxSelectElement { + static wrap($select) { + $select.removeAttribute("tabindex"); + const $btnPrev = createButton({ + label: "<", + style: 32 + }), $btnNext = createButton({ + label: ">", + style: 32 + }), isMultiple = $select.multiple; + let $checkBox, $label, visibleIndex = $select.selectedIndex, $content; + if (isMultiple) $content = CE("button", { + class: "bx-select-value bx-focusable", + tabindex: 0 + }, $checkBox = CE("input", { type: "checkbox" }), $label = CE("span", {}, "")), $content.addEventListener("click", (e) => { + $checkBox.click(); + }), $checkBox.addEventListener("input", (e) => { + const $option = getOptionAtIndex(visibleIndex); + $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); + }); + else $content = CE("div", {}, $label = CE("label", { for: $select.id + "_checkbox" }, "")); + const getOptionAtIndex = (index) => { + return Array.from($select.querySelectorAll("option"))[index]; + }, render = (e) => { + if (e && e.manualTrigger) visibleIndex = $select.selectedIndex; + visibleIndex = normalizeIndex(visibleIndex); + const $option = getOptionAtIndex(visibleIndex); + let content = ""; + if ($option) if (content = $option.textContent || "", content && $option.parentElement.tagName === "OPTGROUP") { + $label.innerHTML = ""; + const fragment = document.createDocumentFragment(); + fragment.appendChild(CE("span", {}, $option.parentElement.label)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment); + } else $label.textContent = content; + else $label.textContent = content; + if ($label.classList.toggle("bx-line-through", $option && $option.disabled), isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); + const disablePrev = visibleIndex <= 0, disableNext = visibleIndex === $select.querySelectorAll("option").length - 1; + $btnPrev.classList.toggle("bx-inactive", disablePrev), $btnNext.classList.toggle("bx-inactive", disableNext), disablePrev && !disableNext && document.activeElement === $btnPrev && $btnNext.focus(), disableNext && !disablePrev && document.activeElement === $btnNext && $btnPrev.focus(); + }, normalizeIndex = (index) => { + return Math.min(Math.max(index, 0), $select.querySelectorAll("option").length - 1); + }, onPrevNext = (e) => { + if (!e.target) return; + const goNext = e.target.closest("button") === $btnNext, currentIndex = visibleIndex; + let newIndex = goNext ? currentIndex + 1 : currentIndex - 1; + if (newIndex = normalizeIndex(newIndex), visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; + if (isMultiple) render(); + else BxEvent.dispatch($select, "input"); + }; + $select.addEventListener("input", render), $btnPrev.addEventListener("click", onPrevNext), $btnNext.addEventListener("click", onPrevNext), new MutationObserver((mutationList, observer2) => { + mutationList.forEach((mutation) => { + if (mutation.type === "childList" || mutation.type === "attributes") render(); + }); + }).observe($select, { + subtree: !0, + childList: !0, + attributes: !0 + }), render(); + const $div = CE("div", { + class: "bx-select", + _nearby: { + orientation: "horizontal", + focus: $btnNext + } + }, $select, $btnPrev, $content, $btnNext); + return Object.defineProperty($div, "value", { + get() { + return $select.value; + }, + set(value) { + $div.setValue(value); + } + }), $div.addEventListener = function() { + $select.addEventListener.apply($select, arguments); + }, $div.removeEventListener = function() { + $select.removeEventListener.apply($select, arguments); + }, $div.dispatchEvent = function() { + return $select.dispatchEvent.apply($select, arguments); + }, $div.setValue = (value) => { + if ("setValue" in $select) $select.setValue(value); + else $select.value = value; + }, $div; + } +} +var controller_shortcuts_default = "if (window.BX_EXPOSED.disableGamepadPolling) {\n this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(50) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, 50);\n return;\n}\n\nconst currentGamepad = ${gamepadVar};\n\n// Share button on XS controller\nif (currentGamepad.buttons[17] && currentGamepad.buttons[17].pressed) {\n window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));\n}\n\nconst btnHome = currentGamepad.buttons[16];\nif (btnHome) {\n if (!this.bxHomeStates) {\n this.bxHomeStates = {};\n }\n\n let intervalMs = 0;\n let hijack = false;\n\n if (btnHome.pressed) {\n hijack = true;\n intervalMs = 16;\n this.gamepadIsIdle.set(currentGamepad.index, false);\n\n if (this.bxHomeStates[currentGamepad.index]) {\n const lastTimestamp = this.bxHomeStates[currentGamepad.index].timestamp;\n\n if (currentGamepad.timestamp !== lastTimestamp) {\n this.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp;\n\n const handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad);\n if (handled) {\n this.bxHomeStates[currentGamepad.index].shortcutPressed += 1;\n }\n }\n } else {\n // First time pressing > save current timestamp\n window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index);\n this.bxHomeStates[currentGamepad.index] = {\n shortcutPressed: 0,\n timestamp: currentGamepad.timestamp,\n };\n }\n } else if (this.bxHomeStates[currentGamepad.index]) {\n hijack = true;\n const info = structuredClone(this.bxHomeStates[currentGamepad.index]);\n\n // Home button released\n this.bxHomeStates[currentGamepad.index] = null;\n\n if (info.shortcutPressed === 0) {\n const fakeGamepadMappings = [{\n GamepadIndex: currentGamepad.index,\n A: 0,\n B: 0,\n X: 0,\n Y: 0,\n LeftShoulder: 0,\n RightShoulder: 0,\n LeftTrigger: 0,\n RightTrigger: 0,\n View: 0,\n Menu: 0,\n LeftThumb: 0,\n RightThumb: 0,\n DPadUp: 0,\n DPadDown: 0,\n DPadLeft: 0,\n DPadRight: 0,\n Nexus: 1,\n LeftThumbXAxis: 0,\n LeftThumbYAxis: 0,\n RightThumbXAxis: 0,\n RightThumbYAxis: 0,\n PhysicalPhysicality: 0,\n VirtualPhysicality: 0,\n Dirty: true,\n Virtual: false,\n }];\n\n const isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500;\n intervalMs = isLongPress ? 500 : 100;\n\n this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings);\n } else {\n intervalMs = 4;\n }\n }\n\n if (hijack && intervalMs) {\n // Listen to next button press\n this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs);\n\n // Hijack this button\n return;\n }\n}\n"; +var expose_stream_session_default = "window.BX_EXPOSED.streamSession = this;\n\nconst orgSetMicrophoneState = this.setMicrophoneState.bind(this);\nthis.setMicrophoneState = state => {\n orgSetMicrophoneState(state);\n\n const evt = new Event(BxEvent.MICROPHONE_STATE_CHANGED);\n evt.microphoneState = state;\n\n window.dispatchEvent(evt);\n};\n\nwindow.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY));\n\n// Patch updateDimensions() to make native touch work correctly with WebGL2\nlet updateDimensionsStr = this.updateDimensions.toString();\n\nif (updateDimensionsStr.startsWith('function ')) {\n updateDimensionsStr = updateDimensionsStr.substring(9);\n}\n\n// if(r){\nconst renderTargetVar = updateDimensionsStr.match(/if\\((\\w+)\\){/)[1];\n\nupdateDimensionsStr = updateDimensionsStr.replaceAll(renderTargetVar + '.scroll', 'scroll');\n\nupdateDimensionsStr = updateDimensionsStr.replace(`if(${renderTargetVar}){`, `\nif (${renderTargetVar}) {\n const scrollWidth = ${renderTargetVar}.dataset.width ? parseInt(${renderTargetVar}.dataset.width) : ${renderTargetVar}.scrollWidth;\n const scrollHeight = ${renderTargetVar}.dataset.height ? parseInt(${renderTargetVar}.dataset.height) : ${renderTargetVar}.scrollHeight;\n`);\n\neval(`this.updateDimensions = function ${updateDimensionsStr}`);\n"; +var local_co_op_enable_default = "let match;\nlet onGamepadChangedStr = this.onGamepadChanged.toString();\n\nif (onGamepadChangedStr.startsWith('function ')) {\n onGamepadChangedStr = onGamepadChangedStr.substring(9);\n}\n\nonGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]');\neval(`this.onGamepadChanged = function ${onGamepadChangedStr}`);\n\nlet onGamepadInputStr = this.onGamepadInput.toString();\n\nmatch = onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/);\nif (match) {\n const gamepadIndexVar = match[0];\n onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', `this.gamepadStates.get(${gamepadIndexVar},`);\n eval(`this.onGamepadInput = function ${onGamepadInputStr}`);\n BxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support');\n} else {\n BxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support');\n}\n"; +var set_currently_focused_interactable_default = "e && BxEvent.dispatch(window, BxEvent.NAVIGATION_FOCUS_CHANGED, {element: e});\n"; +var remote_play_enable_default = "connectMode: window.BX_REMOTE_PLAY_CONFIG ? \"xhome-connect\" : \"cloud-connect\",\nremotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',\n"; +var remote_play_keep_alive_default = "const msg = JSON.parse(e);\nif (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) {\n try {\n this.sendKeepAlive();\n return;\n } catch (ex) { console.log(ex); }\n}\n"; +var vibration_adjust_default = "if (!window.BX_ENABLE_CONTROLLER_VIBRATION) {\n return void(0);\n}\n\nconst intensity = window.BX_VIBRATION_INTENSITY;\nif (intensity === 0) {\n return void(0);\n}\n\nif (intensity < 1) {\n e.leftMotorPercent *= intensity;\n e.rightMotorPercent *= intensity;\n e.leftTriggerMotorPercent *= intensity;\n e.rightTriggerMotorPercent *= intensity;\n}\n"; +var FeatureGates = { + PwaPrompt: !1, + EnableWifiWarnings: !1, + EnableUpdateRequiredPage: !1, + ShowForcedUpdateScreen: !1 +}; +if (getPref("ui_home_context_menu_disabled")) FeatureGates.EnableHomeContextMenu = !1; +if (getPref("block_social_features")) FeatureGates.EnableGuideChatTab = !1; +if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates); +class PatcherUtils { + static indexOf(txt, searchString, startIndex, maxRange) { + const index = txt.indexOf(searchString, startIndex); + if (index < 0 || maxRange && index - startIndex > maxRange) return -1; + return index; + } + static lastIndexOf(txt, searchString, startIndex, maxRange) { + const index = txt.lastIndexOf(searchString, startIndex); + if (index < 0 || maxRange && startIndex - index > maxRange) return -1; + return index; + } + static insertAt(txt, index, insertString) { + return txt.substring(0, index) + insertString + txt.substring(index); + } + static replaceWith(txt, index, fromString, toString) { + return txt.substring(0, index) + toString + txt.substring(index + fromString.length); + } +} +var ENDING_CHUNKS_PATCH_NAME = "loadingEndingChunks", LOG_TAG3 = "Patcher", PATCHES = { + disableAiTrack(str) { + let text = ".track=function("; + const index = str.indexOf(text); + if (index < 0) return !1; + if (PatcherUtils.indexOf(str, '"AppInsightsCore', index, 200) < 0) return !1; + return PatcherUtils.replaceWith(str, index, text, ".track=function(e){},!!function("); + }, + disableTelemetry(str) { + let text = ".disableTelemetry=function(){return!1}"; + if (!str.includes(text)) return !1; + return str.replace(text, ".disableTelemetry=function(){return!0}"); + }, + disableTelemetryProvider(str) { + let text = "this.enableLightweightTelemetry=!"; + if (!str.includes(text)) return !1; + const newCode = [ + "this.trackEvent", + "this.trackPageView", + "this.trackHttpCompleted", + "this.trackHttpFailed", + "this.trackError", + "this.trackErrorLike", + "this.onTrackEvent", + "()=>{}" + ].join("="); + return str.replace(text, newCode + ";" + text); + }, + disableIndexDbLogging(str) { + let text = ",this.logsDb=new"; + if (!str.includes(text)) return !1; + let newCode = ",this.log=()=>{}"; + return str.replace(text, newCode + text); + }, + websiteLayout(str) { + let text = '?"tv":"default"'; + if (!str.includes(text)) return !1; + const layout = getPref("ui_layout") === "tv" ? "tv" : "default"; + return str.replace(text, `?"${layout}":"${layout}"`); + }, + remotePlayDirectConnectUrl(str) { + const index = str.indexOf("/direct-connect"); + if (index < 0) return !1; + return str.replace(str.substring(index - 9, index + 15), "https://www.xbox.com/play"); + }, + remotePlayKeepAlive(str) { + let text = "onServerDisconnectMessage(e){"; + if (!str.includes(text)) return !1; + return str = str.replace(text, text + remote_play_keep_alive_default), str; + }, + remotePlayConnectMode(str) { + let text = 'connectMode:"cloud-connect",'; + if (!str.includes(text)) return !1; + return str.replace(text, remote_play_enable_default); + }, + remotePlayDisableAchievementToast(str) { + let text = ".AchievementUnlock:{"; + if (!str.includes(text)) return !1; + const newCode = "if (!!window.BX_REMOTE_PLAY_CONFIG) return;"; + return str.replace(text, text + newCode); + }, + remotePlayRecentlyUsedTitleIds(str) { + let text = "(e.data.recentlyUsedTitleIds)){"; + if (!str.includes(text)) return !1; + const newCode = "if (window.BX_REMOTE_PLAY_CONFIG) return;"; + return str.replace(text, text + newCode); + }, + blockWebRtcStatsCollector(str) { + let text = "this.shouldCollectStats=!0"; + if (!str.includes(text)) return !1; + return str.replace(text, "this.shouldCollectStats=!1"); + }, + patchPollGamepads(str) { + const index = str.indexOf("},this.pollGamepads=()=>{"); + if (index < 0) return !1; + const nextIndex = str.indexOf("setTimeout(this.pollGamepads", index); + if (nextIndex === -1) return !1; + let codeBlock = str.substring(index, nextIndex); + if (getPref("block_tracking")) codeBlock = codeBlock.replaceAll("this.inputPollingIntervalStats.addValue", ""); + const match = codeBlock.match(/this\.gamepadTimestamps\.set\((\w+)\.index/); + if (match) { + const gamepadVar = match[1], newCode = renderString(controller_shortcuts_default, { + gamepadVar + }); + codeBlock = codeBlock.replace("this.gamepadTimestamps.set", newCode + "this.gamepadTimestamps.set"); + } + return str.substring(0, index) + codeBlock + str.substring(nextIndex); + }, + enableXcloudLogger(str) { + let text = "this.telemetryProvider=e}log(e,t,r){"; + if (!str.includes(text)) return !1; + const newCode = ` +const [logTag, logLevel, logMessage] = Array.from(arguments); +const logFunc = [console.debug, console.log, console.warn, console.error][logLevel]; +logFunc(logTag, '//', logMessage); +`; + return str = str.replaceAll(text, text + newCode), str; + }, + enableConsoleLogging(str) { + let text = "static isConsoleLoggingAllowed(){"; + if (!str.includes(text)) return !1; + return str = str.replaceAll(text, text + "return true;"), str; + }, + playVibration(str) { + let text = "}playVibration(e){"; + if (!str.includes(text)) return !1; + return VibrationManager.updateGlobalVars(), str = str.replaceAll(text, text + vibration_adjust_default), str; + }, + overrideSettings(str) { + const index = str.indexOf(",EnableStreamGate:"); + if (index < 0) return !1; + const endIndex = str.indexOf("},", index); + let newSettings = JSON.stringify(FeatureGates); + newSettings = newSettings.substring(1, newSettings.length - 1); + const newCode = newSettings; + return str = str.substring(0, endIndex) + "," + newCode + str.substring(endIndex), str; + }, + disableGamepadDisconnectedScreen(str) { + const index = str.indexOf('"GamepadDisconnected_Title",'); + if (index < 0) return !1; + const constIndex = str.indexOf("const", index - 30); + return str = str.substring(0, constIndex) + "e.onClose();return null;" + str.substring(constIndex), str; + }, + patchUpdateInputConfigurationAsync(str) { + let text = "async updateInputConfigurationAsync(e){"; + if (!str.includes(text)) return !1; + const newCode = "e.enableTouchInput = true;"; + return str = str.replace(text, text + newCode), str; + }, + loadingEndingChunks(str) { + let text = '"FamilySagaManager"'; + if (!str.includes(text)) return !1; + return BxLogger.info(LOG_TAG3, "Remaining patches:", PATCH_ORDERS), PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS), str; + }, + disableStreamGate(str) { + const index = str.indexOf('case"partially-ready":'); + if (index < 0) return !1; + const bracketIndex = str.indexOf("=>{", index - 150) + 3; + return str = str.substring(0, bracketIndex) + "return 0;" + str.substring(bracketIndex), str; + }, + exposeTouchLayoutManager(str) { + let text = "this._perScopeLayoutsStream=new"; + if (!str.includes(text)) return !1; + const newCode = ` +true; +window.BX_EXPOSED["touchLayoutManager"] = this; +window.dispatchEvent(new Event("${BxEvent.TOUCH_LAYOUT_MANAGER_READY}")); +`; + return str = str.replace(text, newCode + text), str; + }, + patchBabylonRendererClass(str) { + let index = str.indexOf(".current.render(),"); + if (index < 0) return !1; + index -= 1; + const newCode = ` +if (window.BX_EXPOSED.stopTakRendering) { + try { + document.getElementById('BabylonCanvasContainer-main')?.parentElement.classList.add('bx-offscreen'); + ${str[index]}.current.dispose(); + } catch (e) {} + window.BX_EXPOSED.stopTakRendering = false; + return; +} +`; + return str = str.substring(0, index) + newCode + str.substring(index), str; + }, + supportLocalCoOp(str) { + let text = "this.gamepadMappingsToSend=[],"; + if (!str.includes(text)) return !1; + const newCode = `true; ${local_co_op_enable_default}; true,`; + return str = str.replace(text, text + newCode), str; + }, + forceFortniteConsole(str) { + let text = "sendTouchInputEnabledMessage(e){"; + if (!str.includes(text)) return !1; + const newCode = "window.location.pathname.includes('/launch/fortnite/') && (e = false);"; + return str = str.replace(text, text + newCode), str; + }, + disableTakRenderer(str) { + let text = "const{TakRenderer:"; + if (!str.includes(text)) return !1; + let autoOffCode = ""; + if (getPref("stream_touch_controller") === "off") autoOffCode = "return;"; + else if (getPref("stream_touch_controller_auto_off")) autoOffCode = ` +const gamepads = window.navigator.getGamepads(); +let gamepadFound = false; +for (let gamepad of gamepads) { + if (gamepad && gamepad.connected) { + gamepadFound = true; + break; + } +} +if (gamepadFound) { + return; +} +`; + const newCode = ` +${autoOffCode} +const titleInfo = window.BX_EXPOSED.getTitleInfo(); +if (titleInfo && !titleInfo.details.hasTouchSupport && !titleInfo.details.hasFakeTouchSupport) { + return; +} +`; + return str = str.replace(text, newCode + text), str; + }, + streamCombineSources(str) { + let text = "this.useCombinedAudioVideoStream=!!this.deviceInformation.isTizen"; + if (!str.includes(text)) return !1; + return str = str.replace(text, "this.useCombinedAudioVideoStream=true"), str; + }, + patchStreamHud(str) { + let text = "let{onCollapse"; + if (!str.includes(text)) return !1; + let newCode = ` +// Expose onShowStreamMenu +window.BX_EXPOSED.showStreamMenu = e.onShowStreamMenu; +// Restore the "..." button +e.guideUI = null; +`; + if (getPref("stream_touch_controller") === "off") newCode += "e.canShowTakHUD = false;"; + return str = str.replace(text, newCode + text), str; + }, + broadcastPollingMode(str) { + let text = ".setPollingMode=e=>{"; + if (!str.includes(text)) return !1; + const newCode = ` +BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e.toLowerCase()}); +`; + return str = str.replace(text, text + newCode), str; + }, + patchGamepadPolling(str) { + let index = str.indexOf(".shouldHandleGamepadInput)())return void"); + if (index < 0) return !1; + return index = str.indexOf("{", index - 20) + 1, str = str.substring(0, index) + "if (window.BX_EXPOSED.disableGamepadPolling) return;" + str.substring(index), str; + }, + patchXcloudTitleInfo(str) { + let text = "async cloudConnect", index = str.indexOf(text); + if (index < 0) return !1; + let backetIndex = str.indexOf("{", index); + const titleInfoVar = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1].split(",")[0], newCode = ` +${titleInfoVar} = window.BX_EXPOSED.modifyTitleInfo(${titleInfoVar}); +BxLogger.info('patchXcloudTitleInfo', ${titleInfoVar}); +`; + return str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1), str; + }, + patchRemotePlayMkb(str) { + let text = "async homeConsoleConnect", index = str.indexOf(text); + if (index < 0) return !1; + let backetIndex = str.indexOf("{", index); + const configsVar = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1].split(",")[1], newCode = ` +Object.assign(${configsVar}.inputConfiguration, { + enableMouseInput: false, + enableKeyboardInput: false, + enableAbsoluteMouse: false, +}); +BxLogger.info('patchRemotePlayMkb', ${configsVar}); +`; + return str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1), str; + }, + patchAudioMediaStream(str) { + let text = ".srcObject=this.audioMediaStream,"; + if (!str.includes(text)) return !1; + const newCode = "window.BX_EXPOSED.setupGainNode(arguments[1], this.audioMediaStream),"; + return str = str.replace(text, text + newCode), str; + }, + patchCombinedAudioVideoMediaStream(str) { + let text = ".srcObject=this.combinedAudioVideoStream"; + if (!str.includes(text)) return !1; + const newCode = ",window.BX_EXPOSED.setupGainNode(arguments[0], this.combinedAudioVideoStream)"; + return str = str.replace(text, text + newCode), str; + }, + patchTouchControlDefaultOpacity(str) { + let text = "opacityMultiplier:1"; + if (!str.includes(text)) return !1; + const newCode = `opacityMultiplier: ${(getPref("stream_touch_controller_default_opacity") / 100).toFixed(1)}`; + return str = str.replace(text, newCode), str; + }, + patchShowSensorControls(str) { + let text = "{shouldShowSensorControls:"; + if (!str.includes(text)) return !1; + const newCode = "{shouldShowSensorControls: (window.BX_EXPOSED && window.BX_EXPOSED.shouldShowSensorControls) ||"; + return str = str.replace(text, newCode), str; + }, + exposeStreamSession(str) { + let text = ",this._connectionType="; + if (!str.includes(text)) return !1; + const newCode = `; +${expose_stream_session_default} +true` + text; + return str = str.replace(text, newCode), str; + }, + skipFeedbackDialog(str) { + let text = "&&this.shouldTransitionToFeedback("; + if (!str.includes(text)) return !1; + return str = str.replace(text, "&& false " + text), str; + }, + enableNativeMkb(str) { + let text = "e.mouseSupported&&e.keyboardSupported&&e.fullscreenSupported;"; + if (!str.includes(text)) return !1; + return str = str.replace(text, text + "return true;"), str; + }, + patchMouseAndKeyboardEnabled(str) { + let text = "get mouseAndKeyboardEnabled(){"; + if (!str.includes(text)) return !1; + return str = str.replace(text, text + "return true;"), str; + }, + exposeInputSink(str) { + let text = "this.controlChannel=null,this.inputChannel=null"; + if (!str.includes(text)) return !1; + const newCode = "window.BX_EXPOSED.inputSink = this;"; + return str = str.replace(text, newCode + text), str; + }, + disableNativeRequestPointerLock(str) { + let text = "async requestPointerLock(){"; + if (!str.includes(text)) return !1; + return str = str.replace(text, text + "return;"), str; + }, + patchRequestInfoCrash(str) { + let text = 'if(!e)throw new Error("RequestInfo.origin is falsy");'; + if (!str.includes(text)) return !1; + return str = str.replace(text, 'if (!e) e = "https://www.xbox.com";'), str; + }, + exposeDialogRoutes(str) { + let text = "return{goBack:function(){"; + if (!str.includes(text)) return !1; + return str = str.replace(text, "return window.BX_EXPOSED.dialogRoutes = {goBack:function(){"), str; + }, + enableTvRoutes(str) { + let index = str.indexOf(".LoginDeviceCode.path,"); + if (index < 0) return !1; + const match = /render:.*?jsx\)\(([^,]+),/.exec(str.substring(index, index + 100)); + if (!match) return !1; + const funcName = match[1]; + if (index = str.indexOf(`const ${funcName}=e=>{`), index > -1 && (index = str.indexOf("return ", index)), index > -1 && (index = str.indexOf("?", index)), index < 0) return !1; + return str = str.substring(0, index) + "|| true" + str.substring(index), str; + }, + ignorePlayWithFriendsSection(str) { + let index = str.indexOf('location:"PlayWithFriendsRow",'); + if (index < 0) return !1; + if (index = PatcherUtils.lastIndexOf(str, "return", index, 50), index < 0) return !1; + return str = PatcherUtils.replaceWith(str, index, "return", "return null;"), str; + }, + ignoreAllGamesSection(str) { + let index = str.indexOf('className:"AllGamesRow-module__allGamesRowContainer'); + if (index < 0) return !1; + if (index = PatcherUtils.indexOf(str, "grid:!0,", index, 1500), index < 0) return !1; + if (index = PatcherUtils.lastIndexOf(str, "(0,", index, 70), index < 0) return !1; + return str = PatcherUtils.insertAt(str, index, "true ? null :"), str; + }, + ignorePlayWithTouchSection(str) { + let index = str.indexOf('("Play_With_Touch"),'); + if (index < 0) return !1; + if (index = PatcherUtils.lastIndexOf(str, "const ", index, 30), index < 0) return !1; + return str = PatcherUtils.insertAt(str, index, "return null;"), str; + }, + ignoreSiglSections(str) { + let index = str.indexOf("SiglRow-module__heroCard___"); + if (index < 0) return !1; + if (index = PatcherUtils.lastIndexOf(str, "const[", index, 300), index < 0) return !1; + const PREF_HIDE_SECTIONS = getPref("ui_hide_sections"), siglIds = [], sections = { + "native-mkb": "8fa264dd-124f-4af3-97e8-596fcdf4b486", + "most-popular": "e7590b22-e299-44db-ae22-25c61405454c" + }; + PREF_HIDE_SECTIONS.forEach((section) => { + const galleryId = sections[section]; + galleryId && siglIds.push(galleryId); + }); + const newCode = ` +if (e && e.id) { + const siglId = e.id; + if (${siglIds.map((item2) => `siglId === "${item2}"`).join(" || ")}) { + return null; + } +} +`; + return str = PatcherUtils.insertAt(str, index, newCode), str; + }, + overrideStorageGetSettings(str) { + let text = "}getSetting(e){"; + if (!str.includes(text)) return !1; + const newCode = ` +// console.log('setting', this.baseStorageKey, e); +if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { + const settings = window.BX_EXPOSED.overrideSettings[this.baseStorageKey]; + if (e in settings) { + return settings[e]; + } +} +`; + return str = str.replace(text, text + newCode), str; + }, + alwaysShowStreamHud(str) { + let index = str.indexOf(",{onShowStreamMenu:"); + if (index < 0) return !1; + if (index = str.indexOf("&&(0,", index - 100), index < 0) return !1; + const commaIndex = str.indexOf(",", index - 10); + return str = str.substring(0, commaIndex) + ",true" + str.substring(index), str; + }, + patchSetCurrentlyFocusedInteractable(str) { + let index = str.indexOf(".setCurrentlyFocusedInteractable=("); + if (index < 0) return !1; + return index = str.indexOf("{", index) + 1, str = str.substring(0, index) + set_currently_focused_interactable_default + str.substring(index), str; + }, + detectProductDetailsPage(str) { + let index = str.indexOf('{location:"ProductDetailPage",'); + if (index < 0) return !1; + if (index = str.indexOf("return", index - 40), index < 0) return !1; + return str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, {component: "product-details"});' + str.substring(index), str; + }, + detectBrowserRouterReady(str) { + let text = "BrowserRouter:()=>"; + if (!str.includes(text)) return !1; + let index = str.indexOf("{history:this.history,"); + if (index < 0) return !1; + if (index = PatcherUtils.lastIndexOf(str, "return", index, 100), index < 0) return !1; + return str = PatcherUtils.insertAt(str, index, "window.BxEvent.dispatch(window, window.BxEvent.XCLOUD_ROUTER_HISTORY_READY, {history: this.history});"), str; + }, + guideAchievementsDefaultLocked(str) { + let index = str.indexOf("FilterButton-module__container"); + if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, ".All", index, 150)), index < 0) return !1; + if (str = PatcherUtils.replaceWith(str, index, ".All", ".Locked"), index = str.indexOf('"Guide_Achievements_Unlocked_Empty","Guide_Achievements_Locked_Empty"'), index >= 0 && (index = PatcherUtils.indexOf(str, ".All", index, 250)), index < 0) return !1; + return str = PatcherUtils.replaceWith(str, index, ".All", ".Locked"), str; + } +}, PATCH_ORDERS = [ + ...getPref("native_mkb_enabled") === "on" ? [ + "enableNativeMkb", + "patchMouseAndKeyboardEnabled", + "disableNativeRequestPointerLock", + "exposeInputSink" + ] : [], + "detectBrowserRouterReady", + "patchRequestInfoCrash", + "disableStreamGate", + "overrideSettings", + "broadcastPollingMode", + "patchGamepadPolling", + "exposeStreamSession", + "exposeDialogRoutes", + "guideAchievementsDefaultLocked", + "enableTvRoutes", + AppInterface && "detectProductDetailsPage", + "overrideStorageGetSettings", + getPref("ui_game_card_show_wait_time") && "patchSetCurrentlyFocusedInteractable", + getPref("ui_layout") !== "default" && "websiteLayout", + getPref("local_co_op_enabled") && "supportLocalCoOp", + getPref("game_fortnite_force_console") && "forceFortniteConsole", + getPref("ui_hide_sections").includes("friends") && "ignorePlayWithFriendsSection", + getPref("ui_hide_sections").includes("all-games") && "ignoreAllGamesSection", + getPref("ui_hide_sections").includes("touch") && "ignorePlayWithTouchSection", + (getPref("ui_hide_sections").includes("native-mkb") || getPref("ui_hide_sections").includes("most-popular")) && "ignoreSiglSections", + ...getPref("block_tracking") ? [ + "disableAiTrack", + "disableTelemetry", + "blockWebRtcStatsCollector", + "disableIndexDbLogging", + "disableTelemetryProvider" + ] : [], + ...getPref("xhome_enabled") ? [ + "remotePlayKeepAlive", + "remotePlayDirectConnectUrl", + "remotePlayDisableAchievementToast", + "remotePlayRecentlyUsedTitleIds", + STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync" + ] : [], + ...BX_FLAGS.EnableXcloudLogging ? [ + "enableConsoleLogging", + "enableXcloudLogger" + ] : [] +].filter((item2) => !!item2), PLAYING_PATCH_ORDERS = [ + "patchXcloudTitleInfo", + "disableGamepadDisconnectedScreen", + "patchStreamHud", + "playVibration", + "alwaysShowStreamHud", + getPref("audio_enable_volume_control") && !getPref("stream_combine_sources") && "patchAudioMediaStream", + getPref("audio_enable_volume_control") && getPref("stream_combine_sources") && "patchCombinedAudioVideoMediaStream", + getPref("stream_disable_feedback_dialog") && "skipFeedbackDialog", + ...STATES.userAgent.capabilities.touch ? [ + getPref("stream_touch_controller") === "all" && "patchShowSensorControls", + getPref("stream_touch_controller") === "all" && "exposeTouchLayoutManager", + (getPref("stream_touch_controller") === "off" || getPref("stream_touch_controller_auto_off") || !STATES.userAgent.capabilities.touch) && "disableTakRenderer", + getPref("stream_touch_controller_default_opacity") !== 100 && "patchTouchControlDefaultOpacity", + "patchBabylonRendererClass" + ] : [], + BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging", + "patchPollGamepads", + getPref("stream_combine_sources") && "streamCombineSources", + ...getPref("xhome_enabled") ? [ + "patchRemotePlayMkb", + "remotePlayConnectMode" + ] : [] +].filter((item2) => !!item2), ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS]; +class Patcher { + static #patchFunctionBind() { + const nativeBind = Function.prototype.bind; + Function.prototype.bind = function() { + let valid = !1; + if (this.name.length <= 2 && arguments.length === 2 && arguments[0] === null) { + if (arguments[1] === 0 || typeof arguments[1] === "function") valid = !0; + } + if (!valid) return nativeBind.apply(this, arguments); + if (PatcherCache.init(), typeof arguments[1] === "function") BxLogger.info(LOG_TAG3, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind; + const orgFunc = this, newFunc = (a, item2) => { + Patcher.patch(item2), orgFunc(a, item2); + }; + return nativeBind.apply(newFunc, arguments); + }; + } + static patch(item) { + let patchesToCheck, appliedPatches; + const patchesMap = {}; + for (let id in item[1]) { + appliedPatches = []; + const cachedPatches = PatcherCache.getPatches(id); + if (cachedPatches) patchesToCheck = cachedPatches.slice(0), patchesToCheck.push(...PATCH_ORDERS); + else patchesToCheck = PATCH_ORDERS.slice(0); + if (!patchesToCheck.length) continue; + const func = item[1][id], funcStr = func.toString(); + let patchedFuncStr = funcStr, modified = !1; + for (let patchIndex = 0;patchIndex < patchesToCheck.length; patchIndex++) { + const patchName = patchesToCheck[patchIndex]; + if (appliedPatches.indexOf(patchName) > -1) continue; + if (!PATCHES[patchName]) continue; + const tmpStr = PATCHES[patchName].call(null, patchedFuncStr); + if (!tmpStr) continue; + modified = !0, patchedFuncStr = tmpStr, BxLogger.info(LOG_TAG3, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName); + } + if (modified) try { + item[1][id] = eval(patchedFuncStr); + } catch (e) { + if (e instanceof Error) BxLogger.error(LOG_TAG3, "Error", appliedPatches, e.message, patchedFuncStr); + } + if (appliedPatches.length) patchesMap[id] = appliedPatches; + } + if (Object.keys(patchesMap).length) PatcherCache.saveToCache(patchesMap); + } + static init() { + Patcher.#patchFunctionBind(); + } +} +class PatcherCache { + static #KEY_CACHE = "better_xcloud_patches_cache"; + static #KEY_SIGNATURE = "better_xcloud_patches_cache_signature"; + static #CACHE; + static #isInitialized = !1; + static #getSignature() { + const scriptVersion = SCRIPT_VERSION, webVersion = document.querySelector("meta[name=gamepass-app-version]")?.content, patches = JSON.stringify(ALL_PATCHES); + return hashCode(scriptVersion + webVersion + patches); + } + static clear() { + window.localStorage.removeItem(PatcherCache.#KEY_CACHE), PatcherCache.#CACHE = {}; + } + static checkSignature() { + const storedSig = window.localStorage.getItem(PatcherCache.#KEY_SIGNATURE) || 0, currentSig = PatcherCache.#getSignature(); + if (currentSig !== parseInt(storedSig)) BxLogger.warning(LOG_TAG3, "Signature changed"), window.localStorage.setItem(PatcherCache.#KEY_SIGNATURE, currentSig.toString()), PatcherCache.clear(); + else BxLogger.info(LOG_TAG3, "Signature unchanged"); + } + static #cleanupPatches(patches) { + return patches.filter((item2) => { + for (let id2 in PatcherCache.#CACHE) + if (PatcherCache.#CACHE[id2].includes(item2)) return !1; + return !0; + }); + } + static getPatches(id2) { + return PatcherCache.#CACHE[id2]; + } + static saveToCache(subCache) { + for (let id2 in subCache) { + const patchNames = subCache[id2]; + let data = PatcherCache.#CACHE[id2]; + if (!data) PatcherCache.#CACHE[id2] = patchNames; + else for (let patchName of patchNames) + if (!data.includes(patchName)) data.push(patchName); + } + window.localStorage.setItem(PatcherCache.#KEY_CACHE, JSON.stringify(PatcherCache.#CACHE)); + } + static init() { + if (PatcherCache.#isInitialized) return; + if (PatcherCache.#isInitialized = !0, PatcherCache.checkSignature(), PatcherCache.#CACHE = JSON.parse(window.localStorage.getItem(PatcherCache.#KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG3, PatcherCache.#CACHE), window.location.pathname.includes("/play/")) PATCH_ORDERS.push(...PLAYING_PATCH_ORDERS); + else PATCH_ORDERS.push(ENDING_CHUNKS_PATCH_NAME); + PATCH_ORDERS = PatcherCache.#cleanupPatches(PATCH_ORDERS), PLAYING_PATCH_ORDERS = PatcherCache.#cleanupPatches(PLAYING_PATCH_ORDERS), BxLogger.info(LOG_TAG3, PATCH_ORDERS.slice(0)), BxLogger.info(LOG_TAG3, PLAYING_PATCH_ORDERS.slice(0)); + } +} +class FullscreenText { + static instance; + static getInstance() { + if (!FullscreenText.instance) FullscreenText.instance = new FullscreenText; + return FullscreenText.instance; + } + $text; + constructor() { + this.$text = CE("div", { + class: "bx-fullscreen-text bx-gone" + }), document.documentElement.appendChild(this.$text); + } + show(msg) { + document.body.classList.add("bx-no-scroll"), this.$text.classList.remove("bx-gone"), this.$text.textContent = msg; + } + hide() { + document.body.classList.remove("bx-no-scroll"), this.$text.classList.add("bx-gone"); + } +} +class SettingsNavigationDialog extends NavigationDialog { + static instance; + static getInstance() { + if (!SettingsNavigationDialog.instance) SettingsNavigationDialog.instance = new SettingsNavigationDialog; + return SettingsNavigationDialog.instance; + } + $container; + $tabs; + $settings; + $btnReload; + $btnGlobalReload; + $noteGlobalReload; + $btnSuggestion; + renderFullSettings; + suggestedSettings = { + recommended: {}, + default: {}, + lowest: {}, + highest: {} + }; + suggestedSettingLabels = {}; + settingElements = {}; + TAB_GLOBAL_ITEMS = [{ + group: "general", + label: t("better-xcloud"), + helpUrl: "https://better-xcloud.github.io/features/", + items: [ + ($parent) => { + const PREF_LATEST_VERSION = getPref("version_latest"), topButtons = []; + if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) { + const opts = { + label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }), + style: 1 | 32 | 64 + }; + if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript(); + else opts.url = "https://github.com/redphx/better-xcloud/releases/latest"; + topButtons.push(createButton(opts)); + } + if (AppInterface) topButtons.push(createButton({ + label: t("app-settings"), + icon: BxIcon.STREAM_SETTINGS, + style: 64 | 32, + onClick: (e) => { + AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide(); + } + })); + else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({ + label: "🔥 " + t("install-android"), + style: 64 | 32, + url: "https://better-xcloud.github.io/android" + })); + this.$btnGlobalReload = createButton({ + label: t("settings-reload"), + classes: ["bx-settings-reload-button", "bx-gone"], + style: 32 | 64, + onClick: (e) => { + this.reloadPage(); + } + }), topButtons.push(this.$btnGlobalReload), this.$noteGlobalReload = CE("span", { + class: "bx-settings-reload-note" + }, t("settings-reload-note")), topButtons.push(this.$noteGlobalReload), this.$btnSuggestion = CE("div", { + class: "bx-suggest-toggler bx-focusable", + tabindex: 0 + }, CE("label", {}, t("suggest-settings")), CE("span", {}, "❯")), this.$btnSuggestion.addEventListener("click", this.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion); + const $div = CE("div", { + class: "bx-top-buttons", + _nearby: { + orientation: "vertical" + } + }, ...topButtons); + $parent.appendChild($div); + }, + "bx_locale", + "server_bypass_restriction", + "ui_controller_friendly", + "xhome_enabled" + ] + }, { + group: "server", + label: t("server"), + items: [ + "server_region", + "stream_preferred_locale", + "prefer_ipv6_server" + ] + }, { + group: "stream", + label: t("stream"), + items: [ + "stream_target_resolution", + "stream_codec_profile", + "bitrate_video_max", + "audio_enable_volume_control", + "stream_disable_feedback_dialog", + "screenshot_apply_filters", + "audio_mic_on_playing", + "game_fortnite_force_console", + "stream_combine_sources" + ] + }, { + requiredVariants: "full", + group: "co-op", + label: t("local-co-op"), + items: [ + "local_co_op_enabled" + ] + }, { + requiredVariants: "full", + group: "mkb", + label: t("mouse-and-keyboard"), + items: [ + "native_mkb_enabled", + "mkb_enabled", + "mkb_hide_idle_cursor" + ] + }, { + requiredVariants: "full", + group: "touch-control", + label: t("touch-controller"), + note: !STATES.userAgent.capabilities.touch ? "⚠️ " + t("device-unsupported-touch") : null, + unsupported: !STATES.userAgent.capabilities.touch, + items: [ + "stream_touch_controller", + "stream_touch_controller_auto_off", + "stream_touch_controller_default_opacity", + "stream_touch_controller_style_standard", + "stream_touch_controller_style_custom" + ] + }, { + group: "ui", + label: t("ui"), + items: [ + "ui_layout", + "ui_game_card_show_wait_time", + "ui_home_context_menu_disabled", + "controller_show_connection_status", + "stream_simplify_menu", + "skip_splash_video", + !AppInterface && "ui_scrollbar_hide", + "hide_dots_icon", + "reduce_animations", + "block_social_features", + "ui_hide_sections" + ] + }, { + requiredVariants: "full", + group: "game-bar", + label: t("game-bar"), + items: [ + "game_bar_position" + ] + }, { + group: "loading-screen", + label: t("loading-screen"), + items: [ + "ui_loading_screen_game_art", + "ui_loading_screen_wait_time", + "ui_loading_screen_rocket" + ] + }, { + group: "other", + label: t("other"), + items: [ + "block_tracking" + ] + }, { + group: "advanced", + label: t("advanced"), + items: [ + { + pref: "user_agent_profile", + onCreated: (setting, $control) => { + const defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", { + id: `bx_setting_inp_${setting.pref}`, + type: "text", + placeholder: defaultUserAgent, + autocomplete: "off", + class: "bx-settings-custom-user-agent", + tabindex: 0 + }); + $inpCustomUserAgent.addEventListener("input", (e) => { + const profile = $control.value, custom = e.target.value.trim(); + UserAgent.updateStorage(profile, custom), this.onGlobalSettingChanged(e); + }), $control.insertAdjacentElement("afterend", $inpCustomUserAgent), setNearby($inpCustomUserAgent.parentElement, { + orientation: "vertical" + }); + } + } + ] + }, { + group: "footer", + items: [ + ($parent) => { + $parent.appendChild(CE("a", { + class: "bx-donation-link", + href: "https://ko-fi.com/redphx", + target: "_blank", + tabindex: 0 + }, `❤️ ${t("support-better-xcloud")}`)); + }, + ($parent) => { + try { + const appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10); + $parent.appendChild(CE("div", { + class: "bx-settings-app-version" + }, `xCloud website version ${appVersion} (${appDate})`)); + } catch (e) {} + }, + ($parent) => { + const debugInfo = deepClone(BX_FLAGS.DeviceInfo); + debugInfo.settings = JSON.parse(window.localStorage.getItem("better_xcloud") || "{}"); + const $debugInfo = CE("div", { class: "bx-debug-info" }, createButton({ + label: "Debug info", + style: 4 | 64 | 32, + onClick: (e) => { + const $pre = e.target.closest("button")?.nextElementSibling; + $pre.classList.toggle("bx-gone"), $pre.scrollIntoView(); + } + }), CE("pre", { + class: "bx-focusable bx-gone", + tabindex: 0, + on: { + click: async (e) => { + await copyToClipboard(e.target.innerText); + } + } + }, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```")); + $parent.appendChild($debugInfo); + } + ] + }]; + TAB_DISPLAY_ITEMS = [{ + requiredVariants: "full", + group: "audio", + label: t("audio"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#audio", + items: [{ + pref: "audio_volume", + onChange: (e, value) => { + SoundShortcut.setGainNodeVolume(value); + }, + params: { + disabled: !getPref("audio_enable_volume_control") + }, + onCreated: (setting, $elm) => { + const $range = $elm.querySelector("input[type=range"); + window.addEventListener(BxEvent.SETTINGS_CHANGED, (e) => { + const { storageKey, settingKey, settingValue } = e; + if (storageKey !== "better_xcloud" || settingKey !== "audio_volume") return; + $range.value = settingValue, BxEvent.dispatch($range, "input", { + ignoreOnChange: !0 + }); + }); + } + }] + }, { + group: "video", + label: t("video"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#video", + items: [{ + pref: "video_player_type", + onChange: onChangeVideoPlayerType + }, { + pref: "video_power_preference", + onChange: () => { + const streamPlayer = STATES.currentStream.streamPlayer; + if (!streamPlayer) return; + streamPlayer.reloadPlayer(), updateVideoPlayer(); + } + }, { + pref: "video_processing", + onChange: updateVideoPlayer + }, { + pref: "video_ratio", + onChange: updateVideoPlayer + }, { + pref: "video_sharpness", + onChange: updateVideoPlayer + }, { + pref: "video_saturation", + onChange: updateVideoPlayer + }, { + pref: "video_contrast", + onChange: updateVideoPlayer + }, { + pref: "video_brightness", + onChange: updateVideoPlayer + }] + }]; + TAB_CONTROLLER_ITEMS = [ + { + group: "controller", + label: t("controller"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#controller", + items: [{ + pref: "controller_enable_vibration", + unsupported: !VibrationManager.supportControllerVibration(), + onChange: () => VibrationManager.updateGlobalVars() + }, { + pref: "controller_device_vibration", + unsupported: !VibrationManager.supportDeviceVibration(), + onChange: () => VibrationManager.updateGlobalVars() + }, (VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && { + pref: "controller_vibration_intensity", + unsupported: !VibrationManager.supportDeviceVibration(), + onChange: () => VibrationManager.updateGlobalVars() + }] + }, + STATES.userAgent.capabilities.touch && { + group: "touch-control", + label: t("touch-controller"), + items: [{ + label: t("layout"), + content: CE("select", { + disabled: !0 + }, CE("option", {}, t("default"))), + onCreated: (setting, $elm) => { + $elm.addEventListener("input", (e) => { + TouchController.applyCustomLayout($elm.value, 1000); + }), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => { + const customLayouts = TouchController.getCustomLayouts(); + while ($elm.firstChild) + $elm.removeChild($elm.firstChild); + if ($elm.disabled = !customLayouts, !customLayouts) { + $elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("input")); + return; + } + const $fragment = document.createDocumentFragment(); + for (let key in customLayouts.layouts) { + const layout = customLayouts.layouts[key]; + let name; + if (layout.author) name = `${layout.name} (${layout.author})`; + else name = layout.name; + const $option = CE("option", { value: key }, name); + $fragment.appendChild($option); + } + $elm.appendChild($fragment), $elm.value = customLayouts.default_layout; + }); + } + }] + } + ]; + TAB_VIRTUAL_CONTROLLER_ITEMS = [{ + group: "mkb", + label: t("virtual-controller"), + helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/", + content: MkbRemapper.INSTANCE.render() + }]; + TAB_NATIVE_MKB_ITEMS = [{ + requiredVariants: "full", + group: "native-mkb", + label: t("native-mkb"), + items: [!1, !1] + }]; + TAB_SHORTCUTS_ITEMS = [{ + requiredVariants: "full", + group: "controller-shortcuts", + label: t("controller-shortcuts"), + content: !1 + }]; + TAB_STATS_ITEMS = [{ + group: "stats", + label: t("stream-stats"), + helpUrl: "https://better-xcloud.github.io/stream-stats/", + items: [ + { + pref: "stats_show_when_playing" + }, + { + pref: "stats_quick_glance", + onChange: (e) => { + const streamStats = StreamStats.getInstance(); + e.target.checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); + } + }, + { + pref: "stats_items", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_position", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_text_size", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_opacity", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_transparent", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_conditional_formatting", + onChange: StreamStats.refreshStyles + } + ] + }]; + SETTINGS_UI = [ + { + icon: BxIcon.HOME, + group: "global", + items: this.TAB_GLOBAL_ITEMS + }, + { + icon: BxIcon.DISPLAY, + group: "stream", + items: this.TAB_DISPLAY_ITEMS + }, + { + icon: BxIcon.CONTROLLER, + group: "controller", + items: this.TAB_CONTROLLER_ITEMS, + requiredVariants: "full" + }, + getPref("mkb_enabled") && { + icon: BxIcon.VIRTUAL_CONTROLLER, + group: "mkb", + items: this.TAB_VIRTUAL_CONTROLLER_ITEMS, + requiredVariants: "full" + }, + AppInterface && getPref("native_mkb_enabled") === "on" && { + icon: BxIcon.NATIVE_MKB, + group: "native-mkb", + items: this.TAB_NATIVE_MKB_ITEMS, + requiredVariants: "full" + }, + { + icon: BxIcon.COMMAND, + group: "shortcuts", + items: this.TAB_SHORTCUTS_ITEMS, + requiredVariants: "full" + }, + { + icon: BxIcon.STREAM_STATS, + group: "stats", + items: this.TAB_STATS_ITEMS + } + ]; + constructor() { + super(); + this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(); + } + getDialog() { + return this; + } + getContent() { + return this.$container; + } + onMounted() { + if (!this.renderFullSettings) return; + if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); + const $selectUserAgent = document.querySelector(`#bx_setting_${"user_agent_profile"}`); + if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1; + } + reloadPage() { + this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload(); + } + async getRecommendedSettings(deviceCode) { + try { + const json = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${deviceCode.toLowerCase()}.json`)).json(), recommended = {}; + if (json.schema_version !== 1) return null; + const scriptSettings = json.settings.script; + if (scriptSettings._base) { + let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base; + for (let profile of base) + Object.assign(recommended, this.suggestedSettings[profile]); + delete scriptSettings._base; + } + let key; + for (key in scriptSettings) + recommended[key] = scriptSettings[key]; + return BX_FLAGS.DeviceInfo.deviceType = json.device_type, this.suggestedSettings.recommended = recommended, json.device_name; + } catch (e) {} + return null; + } + addDefaultSuggestedSetting(prefKey, value) { + let key; + for (key in this.suggestedSettings) + if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value; + } + generateDefaultSuggestedSettings() { + let key; + for (key in this.suggestedSettings) { + if (key === "default") continue; + let prefKey; + for (prefKey in this.suggestedSettings[key]) + if (!(prefKey in this.suggestedSettings.default)) this.suggestedSettings.default[prefKey] = getPrefDefinition(prefKey).default; + } + } + isSupportedVariant(requiredVariants) { + if (typeof requiredVariants === "undefined") return !0; + return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT); + } + async renderSuggestions(e) { + const $btnSuggest = e.target.closest("div"); + $btnSuggest.toggleAttribute("bx-open"); + let $content = $btnSuggest.nextElementSibling; + if ($content) { + BxEvent.dispatch($content.querySelector("select"), "input"); + return; + } + for (let settingTab of this.SETTINGS_UI) { + if (!settingTab || !settingTab.items) continue; + for (let settingTabContent of settingTab.items) { + if (!settingTabContent || !settingTabContent.items) continue; + for (let setting of settingTabContent.items) { + let prefKey; + if (typeof setting === "string") prefKey = setting; + else if (typeof setting === "object") prefKey = setting.pref; + if (prefKey) this.suggestedSettingLabels[prefKey] = settingTabContent.label; + } + } + } + let recommendedDevice = ""; + if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) { + if (BX_FLAGS.DeviceInfo.androidInfo) { + const deviceCode = BX_FLAGS.DeviceInfo.androidInfo.board; + recommendedDevice = await this.getRecommendedSettings(deviceCode); + } + } + const hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType; + if (deviceType === "android-handheld") this.addDefaultSuggestedSetting("stream_touch_controller", "off"), this.addDefaultSuggestedSetting("controller_device_vibration", "on"); + else if (deviceType === "android") this.addDefaultSuggestedSetting("controller_device_vibration", "auto"); + else if (deviceType === "android-tv") this.addDefaultSuggestedSetting("stream_touch_controller", "off"); + this.generateDefaultSuggestedSettings(); + const $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", {}, hasRecommendedSettings && CE("option", { value: "recommended" }, t("recommended")), !hasRecommendedSettings && CE("option", { value: "highest" }, t("highest-quality")), CE("option", { value: "default" }, t("default")), CE("option", { value: "lowest" }, t("lowest-quality"))); + $select.addEventListener("input", (e2) => { + const profile = $select.value; + removeChildElements($suggestedSettings); + const fragment = document.createDocumentFragment(); + let note; + if (profile === "recommended") note = t("recommended-settings-for-device", { device: recommendedDevice }); + else if (profile === "highest") note = "⚠️ " + t("highest-quality-note"); + note && fragment.appendChild(CE("div", { class: "bx-suggest-note" }, note)); + const settings = this.suggestedSettings[profile]; + let prefKey; + for (prefKey in settings) { + const currentValue = getPref(prefKey, !1), suggestedValue = settings[prefKey], currentValueText = STORAGE.Global.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue; + let $child, $value; + if (isSameValue) $value = currentValueText; + else { + const suggestedValueText = STORAGE.Global.getValueText(prefKey, suggestedValue); + $value = currentValueText + " ➔ " + suggestedValueText; + } + let $checkbox; + const breadcrumb = this.suggestedSettingLabels[prefKey] + " ❯ " + STORAGE.Global.getLabel(prefKey); + if ($child = CE("div", { + class: `bx-suggest-row ${isSameValue ? "bx-suggest-ok" : "bx-suggest-change"}` + }, $checkbox = CE("input", { + type: "checkbox", + tabindex: 0, + checked: !0, + id: `bx_suggest_${prefKey}` + }), CE("label", { + for: `bx_suggest_${prefKey}` + }, CE("div", { + class: "bx-suggest-label" + }, breadcrumb), CE("div", { + class: "bx-suggest-value" + }, $value))), isSameValue) + $checkbox.disabled = !0, $checkbox.checked = !0; + fragment.appendChild($child); + } + $suggestedSettings.appendChild(fragment); + }), BxEvent.dispatch($select, "input"); + const onClickApply = () => { + const profile = $select.value, settings = this.suggestedSettings[profile]; + let prefKey; + for (prefKey in settings) { + const suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${prefKey}`); + if (!$checkBox.checked || $checkBox.disabled) continue; + const $control = this.settingElements[prefKey]; + if (!$control) { + setPref(prefKey, suggestedValue); + continue; + } + if ("setValue" in $control) $control.setValue(suggestedValue); + else $control.value = suggestedValue; + BxEvent.dispatch($control, "input", { + manualTrigger: !0 + }); + } + BxEvent.dispatch($select, "input"); + }, $btnApply = createButton({ + label: t("apply"), + style: 64 | 32, + onClick: onClickApply + }); + $content = CE("div", { + class: "bx-suggest-box", + _nearby: { + orientation: "vertical" + } + }, BxSelectElement.wrap($select), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", { + class: "bx-suggest-link bx-focusable", + href: "https://better-xcloud.github.io/guide/android-webview-tweaks/", + target: "_blank", + tabindex: 0 + }, "🤓 " + t("how-to-improve-app-performance")), BX_FLAGS.DeviceInfo.deviceType.includes("android") && !hasRecommendedSettings && CE("a", { + class: "bx-suggest-link bx-focusable", + href: "https://github.com/redphx/better-xcloud-devices", + target: "_blank", + tabindex: 0 + }, t("suggest-settings-link"))), $btnSuggest?.insertAdjacentElement("afterend", $content); + } + renderTab(settingTab) { + const $svg = createSvgIcon(settingTab.icon); + return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", (e) => { + for (let $child of Array.from(this.$settings.children)) + if ($child.getAttribute("data-tab-group") === settingTab.group) { + if ($child.classList.remove("bx-gone"), getPref("ui_controller_friendly")) this.dialogManager.calculateSelectBoxes($child); + } else $child.classList.add("bx-gone"); + for (let $child of Array.from(this.$tabs.children)) + $child.classList.remove("bx-active"); + $svg.classList.add("bx-active"); + }), $svg; + } + onGlobalSettingChanged(e) { + PatcherCache.clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger"); + } + renderServerSetting(setting) { + let selectedValue; + const $control = CE("select", { + id: `bx_setting_${setting.pref}`, + title: setting.label, + tabindex: 0 + }); + $control.name = $control.id, $control.addEventListener("input", (e) => { + setPref(setting.pref, e.target.value), this.onGlobalSettingChanged(e); + }), selectedValue = getPref("server_region"), setting.options = {}; + for (let regionName in STATES.serverRegions) { + const region = STATES.serverRegions[regionName]; + let value = regionName, label = `${region.shortName} - ${regionName}`; + if (region.isDefault) { + if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default"; + } + setting.options[value] = label; + } + for (let value in setting.options) { + const label = setting.options[value], $option = CE("option", { value }, label); + $control.appendChild($option); + } + return $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control; + } + renderSettingRow(settingTab, $tabContent, settingTabContent, setting) { + if (typeof setting === "string") setting = { + pref: setting + }; + const pref = setting.pref; + let $control; + if (setting.content) if (typeof setting.content === "function") $control = setting.content.apply(this); + else $control = setting.content; + else if (!setting.unsupported) { + if (pref === "server_region") $control = this.renderServerSetting(setting); + else if (pref === "bx_locale") $control = SettingElement.fromPref(pref, STORAGE.Global, async (e) => { + const newLocale = e.target.value; + if (getPref("ui_controller_friendly")) { + let timeoutId = e.target.timeoutId; + timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => { + Translations.refreshLocale(newLocale), Translations.updateTranslations(); + }, 500); + } else Translations.refreshLocale(newLocale), Translations.updateTranslations(); + this.onGlobalSettingChanged(e); + }); + else if (pref === "user_agent_profile") $control = SettingElement.fromPref("user_agent_profile", STORAGE.Global, (e) => { + const value = e.target.value; + let isCustom = value === "custom", userAgent2 = UserAgent.get(value); + UserAgent.updateStorage(value); + const $inp = $control.nextElementSibling; + $inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e); + }); + else { + let onChange = setting.onChange; + if (!onChange && settingTab.group === "global") onChange = this.onGlobalSettingChanged.bind(this); + $control = SettingElement.fromPref(pref, STORAGE.Global, onChange, setting.params); + } + if ($control instanceof HTMLSelectElement && getPref("ui_controller_friendly")) $control = BxSelectElement.wrap($control); + pref && (this.settingElements[pref] = $control); + } + let prefDefinition = null; + if (pref) prefDefinition = getPrefDefinition(pref); + if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return; + let label = prefDefinition?.label || setting.label, note = prefDefinition?.note || setting.note; + const experimental = prefDefinition?.experimental || setting.experimental; + if (settingTabContent.label && setting.pref) { + if (prefDefinition?.suggest) typeof prefDefinition.suggest.lowest !== "undefined" && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest), typeof prefDefinition.suggest.highest !== "undefined" && (this.suggestedSettings.highest[setting.pref] = prefDefinition.suggest.highest); + } + if (experimental) if (label = "🧪 " + label, !note) note = t("experimental"); + else note = `${t("experimental")}: ${note}`; + let $label; + const $row = CE("label", { + class: "bx-settings-row", + for: `bx_setting_${pref}`, + "data-type": settingTabContent.group, + _nearby: { + orientation: "horizontal" + } + }, $label = CE("span", { class: "bx-settings-label" }, label, note && CE("div", { class: "bx-settings-dialog-note" }, note), setting.unsupported && CE("div", { class: "bx-settings-dialog-note" }, t("browser-unsupported-feature"))), !setting.unsupported && $control), $link = $label.querySelector("a"); + if ($link) $link.classList.add("bx-focusable"), setNearby($label, { + focus: $link + }); + $tabContent.appendChild($row), setting.onCreated && setting.onCreated(setting, $control); + } + setupDialog() { + let $tabs, $settings; + const $container = CE("div", { + class: "bx-settings-dialog", + _nearby: { + orientation: "horizontal" + } + }, CE("div", { + class: "bx-settings-tabs-container", + _nearby: { + orientation: "vertical", + focus: () => { + return this.dialogManager.focus($tabs); + }, + loop: (direction) => { + if (direction === 1 || direction === 3) return this.focusVisibleTab(direction === 1 ? "last" : "first"), !0; + return !1; + } + } + }, $tabs = CE("div", { + class: "bx-settings-tabs bx-hide-scroll-bar", + _nearby: { + focus: () => this.focusActiveTab() + } + }), CE("div", {}, this.$btnReload = createButton({ + icon: BxIcon.REFRESH, + style: 32 | 16, + onClick: (e) => { + this.reloadPage(); + } + }), createButton({ + icon: BxIcon.CLOSE, + style: 32 | 16, + onClick: (e) => { + this.dialogManager.hide(); + } + }))), $settings = CE("div", { + class: "bx-settings-tab-contents", + _nearby: { + orientation: "vertical", + focus: () => this.jumpToSettingGroup("next"), + loop: (direction) => { + if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0; + return !1; + } + } + })); + this.$container = $container, this.$tabs = $tabs, this.$settings = $settings, $container.addEventListener("click", (e) => { + if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide(); + }); + for (let settingTab of this.SETTINGS_UI) { + if (!settingTab) continue; + if (!this.isSupportedVariant(settingTab.requiredVariants)) continue; + if (settingTab.group !== "global" && !this.renderFullSettings) continue; + const $svg = this.renderTab(settingTab); + $tabs.appendChild($svg); + const $tabContent = CE("div", { + class: "bx-gone", + "data-tab-group": settingTab.group + }); + for (let settingTabContent of settingTab.items) { + if (settingTabContent === !1) continue; + if (!this.isSupportedVariant(settingTabContent.requiredVariants)) continue; + if (!this.renderFullSettings && settingTab.group === "global" && settingTabContent.group !== "general" && settingTabContent.group !== "footer") continue; + let label = settingTabContent.label; + if (label === t("better-xcloud")) { + if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)"; + label = createButton({ + label, + url: "https://github.com/redphx/better-xcloud/releases", + style: 1024 | 8 | 32 + }); + } + if (label) { + const $title = CE("h2", { + _nearby: { + orientation: "horizontal" + } + }, CE("span", {}, label), settingTabContent.helpUrl && createButton({ + icon: BxIcon.QUESTION, + style: 4 | 32, + url: settingTabContent.helpUrl, + title: t("help") + })); + $tabContent.appendChild($title); + } + if (settingTabContent.note) { + let $note; + if (typeof settingTabContent.note === "string") $note = CE("b", { class: "bx-note-unsupported" }, settingTabContent.note); + else $note = settingTabContent.note; + $tabContent.appendChild($note); + } + if (settingTabContent.unsupported) continue; + if (settingTabContent.content) { + $tabContent.appendChild(settingTabContent.content); + continue; + } + settingTabContent.items = settingTabContent.items || []; + for (let setting of settingTabContent.items) { + if (setting === !1) continue; + if (typeof setting === "function") { + setting.apply(this, [$tabContent]); + continue; + } + this.renderSettingRow(settingTab, $tabContent, settingTabContent, setting); + } + } + $settings.appendChild($tabContent); + } + $tabs.firstElementChild.dispatchEvent(new Event("click")); + } + focusTab(tabId) { + const $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`); + $tab && $tab.dispatchEvent(new Event("click")); + } + focusIfNeeded() { + this.jumpToSettingGroup("next"); + } + focusActiveTab() { + const $currentTab = this.$tabs.querySelector(".bx-active"); + return $currentTab && $currentTab.focus(), !0; + } + focusVisibleSetting(type = "first") { + const controls = Array.from(this.$settings.querySelectorAll("div[data-tab-group]:not(.bx-gone) > *")); + if (!controls.length) return !1; + if (type === "last") controls.reverse(); + for (let $control of controls) { + if (!($control instanceof HTMLElement)) continue; + const $focusable = this.dialogManager.findFocusableElement($control); + if ($focusable) { + if (this.dialogManager.focus($focusable)) return !0; + } + } + return !1; + } + focusVisibleTab(type = "first") { + const tabs = Array.from(this.$tabs.querySelectorAll("svg:not(.bx-gone)")); + if (!tabs.length) return !1; + if (type === "last") tabs.reverse(); + for (let $tab of tabs) + if (this.dialogManager.focus($tab)) return !0; + return !1; + } + jumpToSettingGroup(direction) { + const $tabContent = this.$settings.querySelector("div[data-tab-group]:not(.bx-gone)"); + if (!$tabContent) return !1; + let $header; + const $focusing = document.activeElement; + if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2"); + else { + const $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling"; + let $tmp = $parent, times = 0; + while (!0) { + if (!$tmp) break; + if ($tmp.tagName === "H2") { + if ($header = $tmp, !$tmp.nextElementSibling?.classList.contains("bx-note-unsupported")) { + if (++times, direction === "next" || times >= 2) break; + } + } + $tmp = $tmp[siblingProperty]; + } + } + let $target; + if ($header) $target = this.dialogManager.findNextTarget($header, 3, !1); + if ($target) return this.dialogManager.focus($target); + return !1; + } + handleKeyPress(key) { + let handled = !0; + switch (key) { + case "Tab": + this.focusActiveTab(); + break; + case "Home": + this.focusVisibleSetting("first"); + break; + case "End": + this.focusVisibleSetting("last"); + break; + case "PageUp": + this.jumpToSettingGroup("previous"); + break; + case "PageDown": + this.jumpToSettingGroup("next"); + break; + default: + handled = !1; + break; + } + return handled; + } + handleGamepad(button) { + let handled = !0; + switch (button) { + case 4: + case 5: + this.focusActiveTab(); + break; + case 6: + this.jumpToSettingGroup("previous"); + break; + case 7: + this.jumpToSettingGroup("next"); + break; + default: + handled = !1; + break; + } + return handled; + } +} +var LOG_TAG4 = "MkbHandler", PointerToMouseButton = { + 1: 0, + 2: 2, + 4: 1 +}; +class WebSocketMouseDataProvider extends MouseDataProvider { + #pointerClient; + #connected = !1; + init() { + this.#pointerClient = PointerClient.getInstance(), this.#connected = !1; + try { + this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.#connected = !0; + } catch (e) { + Toast.show("Cannot enable Mouse & Keyboard feature"); + } + } + start() { + this.#connected && AppInterface.requestPointerCapture(); + } + stop() { + this.#connected && AppInterface.releasePointerCapture(); + } + destroy() { + this.#connected && this.#pointerClient?.stop(); + } +} +class PointerLockMouseDataProvider extends MouseDataProvider { + init() {} + start() { + window.addEventListener("mousemove", this.#onMouseMoveEvent), window.addEventListener("mousedown", this.#onMouseEvent), window.addEventListener("mouseup", this.#onMouseEvent), window.addEventListener("wheel", this.#onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.#disableContextMenu); + } + stop() { + document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.#onMouseMoveEvent), window.removeEventListener("mousedown", this.#onMouseEvent), window.removeEventListener("mouseup", this.#onMouseEvent), window.removeEventListener("wheel", this.#onWheelEvent), window.removeEventListener("contextmenu", this.#disableContextMenu); + } + destroy() {} + #onMouseMoveEvent = (e) => { + this.mkbHandler.handleMouseMove({ + movementX: e.movementX, + movementY: e.movementY + }); + }; + #onMouseEvent = (e) => { + e.preventDefault(); + const isMouseDown = e.type === "mousedown", data = { + mouseButton: e.button, + pressed: isMouseDown + }; + this.mkbHandler.handleMouseClick(data); + }; + #onWheelEvent = (e) => { + if (!KeyHelper.getKeyFromEvent(e)) return; + const data = { + vertical: e.deltaY, + horizontal: e.deltaX + }; + if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); + }; + #disableContextMenu = (e) => e.preventDefault(); +} +class EmulatedMkbHandler extends MkbHandler { + static #instance; + static getInstance() { + if (!EmulatedMkbHandler.#instance) EmulatedMkbHandler.#instance = new EmulatedMkbHandler; + return EmulatedMkbHandler.#instance; + } + #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); + static DEFAULT_PANNING_SENSITIVITY = 0.001; + static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; + static MAXIMUM_STICK_RANGE = 1.1; + static VIRTUAL_GAMEPAD_ID = "Xbox 360 Controller"; + #VIRTUAL_GAMEPAD = { + id: EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID, + index: 3, + connected: !1, + hapticActuators: null, + mapping: "standard", + axes: [0, 0, 0, 0], + buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })), + timestamp: performance.now(), + vibrationActuator: null + }; + #nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator); + #enabled = !1; + #mouseDataProvider; + #isPolling = !1; + #prevWheelCode = null; + #wheelStoppedTimeout; + #detectMouseStoppedTimeout; + #$message; + #escKeyDownTime = -1; + #STICK_MAP; + #LEFT_STICK_X = []; + #LEFT_STICK_Y = []; + #RIGHT_STICK_X = []; + #RIGHT_STICK_Y = []; + constructor() { + super(); + this.#STICK_MAP = { + 102: [this.#LEFT_STICK_X, 0, -1], + 103: [this.#LEFT_STICK_X, 0, 1], + 100: [this.#LEFT_STICK_Y, 1, -1], + 101: [this.#LEFT_STICK_Y, 1, 1], + 202: [this.#RIGHT_STICK_X, 2, -1], + 203: [this.#RIGHT_STICK_X, 2, 1], + 200: [this.#RIGHT_STICK_Y, 3, -1], + 201: [this.#RIGHT_STICK_Y, 3, 1] + }; + } + isEnabled = () => this.#enabled; + #patchedGetGamepads = () => { + const gamepads = this.#nativeGetGamepads() || []; + return gamepads[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD, gamepads; + }; + #getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD; + #updateStick(stick, x, y) { + const virtualGamepad = this.#getVirtualGamepad(); + virtualGamepad.axes[stick * 2] = x, virtualGamepad.axes[stick * 2 + 1] = y, virtualGamepad.timestamp = performance.now(); + } + #vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); + #resetGamepad = () => { + const gamepad = this.#getVirtualGamepad(); + gamepad.axes = [0, 0, 0, 0]; + for (let button of gamepad.buttons) + button.pressed = !1, button.value = 0; + gamepad.timestamp = performance.now(); + }; + #pressButton = (buttonIndex, pressed) => { + const virtualGamepad = this.#getVirtualGamepad(); + if (buttonIndex >= 100) { + let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]; + valueArr = valueArr, axisIndex = axisIndex; + for (let i = valueArr.length - 1;i >= 0; i--) + if (valueArr[i] === buttonIndex) valueArr.splice(i, 1); + pressed && valueArr.push(buttonIndex); + let value; + if (valueArr.length) value = this.#STICK_MAP[valueArr[valueArr.length - 1]][2]; + else value = 0; + virtualGamepad.axes[axisIndex] = value; + } else virtualGamepad.buttons[buttonIndex].pressed = pressed, virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0; + virtualGamepad.timestamp = performance.now(); + }; + #onKeyboardEvent = (e) => { + const isKeyDown = e.type === "keydown"; + if (e.code === "F8") { + if (!isKeyDown) e.preventDefault(), this.toggle(); + return; + } + if (e.code === "Escape") { + if (e.preventDefault(), this.#enabled && isKeyDown) { + if (this.#escKeyDownTime === -1) this.#escKeyDownTime = performance.now(); + else if (performance.now() - this.#escKeyDownTime >= 1000) this.stop(); + } else this.#escKeyDownTime = -1; + return; + } + if (!this.#isPolling) return; + const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]; + if (typeof buttonIndex === "undefined") return; + if (e.repeat) return; + e.preventDefault(), this.#pressButton(buttonIndex, isKeyDown); + }; + #onMouseStopped = () => { + this.#detectMouseStoppedTimeout = null; + const analog = this.#CURRENT_PRESET_DATA.mouse["map_to"] === 1 ? 0 : 1; + this.#updateStick(analog, 0, 0); + }; + handleMouseClick = (data) => { + let mouseButton; + if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton; + else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton]; + const keyCode = "Mouse" + mouseButton, key = { + code: keyCode, + name: KeyHelper.codeToKeyName(keyCode) + }; + if (!key.name) return; + const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + if (typeof buttonIndex === "undefined") return; + this.#pressButton(buttonIndex, data.pressed); + }; + handleMouseMove = (data) => { + const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse["map_to"]; + if (mouseMapTo === 0) return; + this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout), this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50); + const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse["deadzone_counterweight"]; + let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse["sensitivity_x"], y = data.movementY * this.#CURRENT_PRESET_DATA.mouse["sensitivity_y"], length = this.#vectorLength(x, y); + if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; + else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length, y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length; + const analog = mouseMapTo === 1 ? 0 : 1; + this.#updateStick(analog, x, y); + }; + handleMouseWheel = (data) => { + let code = ""; + if (data.vertical < 0) code = "ScrollUp"; + else if (data.vertical > 0) code = "ScrollDown"; + else if (data.horizontal < 0) code = "ScrollLeft"; + else if (data.horizontal > 0) code = "ScrollRight"; + if (!code) return !1; + const key = { + code, + name: KeyHelper.codeToKeyName(code) + }, buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + if (typeof buttonIndex === "undefined") return !1; + if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout), this.#pressButton(buttonIndex, !0); + return this.#wheelStoppedTimeout = window.setTimeout(() => { + this.#prevWheelCode = null, this.#pressButton(buttonIndex, !1); + }, 20), !0; + }; + toggle = (force) => { + if (typeof force !== "undefined") this.#enabled = force; + else this.#enabled = !this.#enabled; + if (this.#enabled) document.body.requestPointerLock(); + else document.pointerLockElement && document.exitPointerLock(); + }; + #getCurrentPreset = () => { + return new Promise((resolve) => { + const presetId = getPref("mkb_default_preset_id"); + LocalDb.INSTANCE.getPreset(presetId).then((preset) => { + resolve(preset); + }); + }); + }; + refreshPresetData = () => { + this.#getCurrentPreset().then((preset) => { + this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET), this.#resetGamepad(); + }); + }; + waitForMouseData = (wait) => { + this.#$message && this.#$message.classList.toggle("bx-gone", !wait); + }; + #onPollingModeChanged = (e) => { + if (!this.#$message) return; + if (e.mode === "none") this.#$message.classList.remove("bx-offscreen"); + else this.#$message.classList.add("bx-offscreen"); + }; + #onDialogShown = () => { + document.pointerLockElement && document.exitPointerLock(); + }; + #initMessage = () => { + if (!this.#$message) this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, CE("div", {}, CE("p", {}, t("virtual-controller")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "virtual" }, createButton({ + style: 1 | 256 | 64, + label: t("activate"), + onClick: ((e) => { + e.preventDefault(), e.stopPropagation(), this.toggle(!0); + }).bind(this) + }), CE("div", {}, createButton({ + label: t("ignore"), + style: 4, + onClick: (e) => { + e.preventDefault(), e.stopPropagation(), this.toggle(!1), this.waitForMouseData(!1); + } + }), createButton({ + label: t("edit"), + onClick: (e) => { + e.preventDefault(), e.stopPropagation(); + const dialog = SettingsNavigationDialog.getInstance(); + dialog.focusTab("mkb"), NavigationDialogManager.getInstance().show(dialog); + } + })))); + if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); + }; + #onPointerLockChange = () => { + if (document.pointerLockElement) this.start(); + else this.stop(); + }; + #onPointerLockError = (e) => { + console.log(e), this.stop(); + }; + #onPointerLockRequested = () => { + this.start(); + }; + #onPointerLockExited = () => { + this.#mouseDataProvider?.stop(); + }; + handleEvent(event) { + switch (event.type) { + case BxEvent.POINTER_LOCK_REQUESTED: + this.#onPointerLockRequested(); + break; + case BxEvent.POINTER_LOCK_EXITED: + this.#onPointerLockExited(); + break; + } + } + init = () => { + if (this.refreshPresetData(), this.#enabled = !1, AppInterface) this.#mouseDataProvider = new WebSocketMouseDataProvider(this); + else this.#mouseDataProvider = new PointerLockMouseDataProvider(this); + if (this.#mouseDataProvider.init(), window.addEventListener("keydown", this.#onKeyboardEvent), window.addEventListener("keyup", this.#onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this); + else document.addEventListener("pointerlockchange", this.#onPointerLockChange), document.addEventListener("pointerlockerror", this.#onPointerLockError); + if (this.#initMessage(), this.#$message?.classList.add("bx-gone"), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("virtual-controller"), { html: !0 }), this.waitForMouseData(!1); + else this.waitForMouseData(!0); + }; + destroy = () => { + if (this.#isPolling = !1, this.#enabled = !1, this.stop(), this.waitForMouseData(!1), document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("keydown", this.#onKeyboardEvent), window.removeEventListener("keyup", this.#onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this); + else document.removeEventListener("pointerlockchange", this.#onPointerLockChange), document.removeEventListener("pointerlockerror", this.#onPointerLockError); + window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), this.#mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged); + }; + start = () => { + if (!this.#enabled) this.#enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); + this.#isPolling = !0, this.#escKeyDownTime = -1, this.#resetGamepad(), window.navigator.getGamepads = this.#patchedGetGamepads, this.waitForMouseData(!1), this.#mouseDataProvider?.start(); + const virtualGamepad = this.#getVirtualGamepad(); + virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", { + gamepad: virtualGamepad + }), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); + }; + stop = () => { + this.#enabled = !1, this.#isPolling = !1, this.#escKeyDownTime = -1; + const virtualGamepad = this.#getVirtualGamepad(); + if (virtualGamepad.connected) this.#resetGamepad(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", { + gamepad: virtualGamepad + }), window.navigator.getGamepads = this.#nativeGetGamepads; + this.waitForMouseData(!0), this.#mouseDataProvider?.stop(); + }; + static setupEvents() { + window.addEventListener(BxEvent.STREAM_PLAYING, () => { + if (STATES.currentStream.titleInfo?.details.hasMkbSupport) { + if (AppInterface && getPref("native_mkb_enabled") === "on") AppInterface && NativeMkbHandler.getInstance().init(); + } else if (getPref("mkb_enabled") && (AppInterface || !UserAgent.isMobile())) BxLogger.info(LOG_TAG4, "Emulate MKB"), EmulatedMkbHandler.getInstance().init(); + }); + } +} +var BxExposed = { + getTitleInfo: () => STATES.currentStream.titleInfo, + modifyTitleInfo: (titleInfo) => { + titleInfo = deepClone(titleInfo); + let supportedInputTypes = titleInfo.details.supportedInputTypes; + if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) supportedInputTypes.push("MKB"); + if (getPref("native_mkb_enabled") === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "MKB"); + if (titleInfo.details.hasMkbSupport = supportedInputTypes.includes("MKB"), STATES.userAgent.capabilities.touch) { + let touchControllerAvailability = getPref("stream_touch_controller"); + if (touchControllerAvailability !== "off" && getPref("stream_touch_controller_auto_off")) { + const gamepads = window.navigator.getGamepads(); + let gamepadFound = !1; + for (let gamepad of gamepads) + if (gamepad && gamepad.connected) { + gamepadFound = !0; + break; + } + gamepadFound && (touchControllerAvailability = "off"); + } + if (touchControllerAvailability === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "CustomTouchOverlay" && i !== "GenericTouch"), titleInfo.details.supportedTabs = []; + if (titleInfo.details.hasNativeTouchSupport = supportedInputTypes.includes("NativeTouch"), titleInfo.details.hasTouchSupport = titleInfo.details.hasNativeTouchSupport || supportedInputTypes.includes("CustomTouchOverlay") || supportedInputTypes.includes("GenericTouch"), !titleInfo.details.hasTouchSupport && touchControllerAvailability === "all") titleInfo.details.hasFakeTouchSupport = !0, supportedInputTypes.push("GenericTouch"); + } + return titleInfo.details.supportedInputTypes = supportedInputTypes, STATES.currentStream.titleInfo = titleInfo, BxEvent.dispatch(window, BxEvent.TITLE_INFO_READY), titleInfo; + }, + setupGainNode: ($media, audioStream) => { + if ($media instanceof HTMLAudioElement) $media.muted = !0, $media.addEventListener("playing", (e) => { + $media.muted = !0, $media.pause(); + }); + else $media.muted = !0, $media.addEventListener("playing", (e) => { + $media.muted = !0; + }); + try { + const audioCtx = STATES.currentStream.audioContext, source = audioCtx.createMediaStreamSource(audioStream), gainNode = audioCtx.createGain(); + source.connect(gainNode).connect(audioCtx.destination); + } catch (e) { + BxLogger.error("setupGainNode", e), STATES.currentStream.audioGainNode = null; + } + }, + handleControllerShortcut: !1, + resetControllerShortcut: !1, + overrideSettings: { + Tv_settings: { + hasCompletedOnboarding: !0 + } + }, + disableGamepadPolling: !1, + backButtonPressed: () => { + const navigationDialogManager = NavigationDialogManager.getInstance(); + if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0; + const dict = { + bubbles: !0, + cancelable: !0, + key: "XF86Back", + code: "XF86Back", + keyCode: 4, + which: 4 + }; + return document.body.dispatchEvent(new KeyboardEvent("keydown", dict)), document.body.dispatchEvent(new KeyboardEvent("keyup", dict)), !1; + } +}; +function localRedirect(path) { + const url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); + if (!$pageContent) return; + const $anchor = CE("a", { + href: url, + class: "bx-hidden bx-offscreen" + }, ""); + $anchor.addEventListener("click", (e) => { + window.setTimeout(() => { + $pageContent.removeChild($anchor); + }, 1000); + }), $pageContent.appendChild($anchor), $anchor.click(); +} +window.localRedirect = localRedirect; +function getPreferredServerRegion(shortName = !1) { + let preferredRegion = getPref("server_region"); + if (preferredRegion in STATES.serverRegions) if (shortName && STATES.serverRegions[preferredRegion].shortName) return STATES.serverRegions[preferredRegion].shortName; + else return preferredRegion; + for (let regionName in STATES.serverRegions) { + const region = STATES.serverRegions[regionName]; + if (!region.isDefault) continue; + if (shortName && region.shortName) return region.shortName; + else return regionName; + } + return null; +} +class HeaderSection { + static #$remotePlayBtn = createButton({ + classes: ["bx-header-remote-play-button", "bx-gone"], + icon: BxIcon.REMOTE_PLAY, + title: t("remote-play"), + style: 4 | 32 | 512, + onClick: (e) => { + RemotePlayManager.getInstance().togglePopup(); + } + }); + static #$settingsBtn = createButton({ + classes: ["bx-header-settings-button"], + label: "???", + style: 8 | 16 | 32 | 128, + onClick: (e) => { + SettingsNavigationDialog.getInstance().show(); + } + }); + static #$buttonsWrapper = CE("div", {}, getPref("xhome_enabled") ? HeaderSection.#$remotePlayBtn : null, HeaderSection.#$settingsBtn); + static #observer; + static #timeout; + static #injectSettingsButton($parent) { + if (!$parent) return; + const PREF_LATEST_VERSION = getPref("version_latest"), $btnSettings = HeaderSection.#$settingsBtn; + if (isElementVisible(HeaderSection.#$buttonsWrapper)) return; + if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true"); + $parent.appendChild(HeaderSection.#$buttonsWrapper); + } + static checkHeader() { + let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]"); + if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]"); + $target && HeaderSection.#injectSettingsButton($target); + } + static showRemotePlayButton() { + HeaderSection.#$remotePlayBtn.classList.remove("bx-gone"); + } + static watchHeader() { + const $root = document.querySelector("#PageContent header") || document.querySelector("#root"); + if (!$root) return; + HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout), HeaderSection.#timeout = null, HeaderSection.#observer && HeaderSection.#observer.disconnect(), HeaderSection.#observer = new MutationObserver((mutationList) => { + HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout), HeaderSection.#timeout = window.setTimeout(HeaderSection.checkHeader, 2000); + }), HeaderSection.#observer.observe($root, { subtree: !0, childList: !0 }), HeaderSection.checkHeader(); + } +} +class RemotePlayNavigationDialog extends NavigationDialog { + static instance; + static getInstance() { + if (!RemotePlayNavigationDialog.instance) RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog; + return RemotePlayNavigationDialog.instance; + } + STATE_LABELS = { + On: t("powered-on"), + Off: t("powered-off"), + ConnectedStandby: t("standby"), + Unknown: t("unknown") + }; + $container; + constructor() { + super(); + this.setupDialog(); + } + setupDialog() { + const $fragment = CE("div", { class: "bx-remote-play-container" }), $settingNote = CE("p", {}), currentResolution = getPref("xhome_resolution"); + let $resolutions = CE("select", {}, CE("option", { value: "1080p" }, "1080p"), CE("option", { value: "720p" }, "720p")); + if (getPref("ui_controller_friendly")) $resolutions = BxSelectElement.wrap($resolutions); + $resolutions.addEventListener("input", (e) => { + const value = e.target.value; + $settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setPref("xhome_resolution", value); + }), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", { + manualTrigger: !0 + }); + const $qualitySettings = CE("div", { + class: "bx-remote-play-settings" + }, CE("div", {}, CE("label", {}, t("target-resolution"), $settingNote), $resolutions)); + $fragment.appendChild($qualitySettings); + const manager = RemotePlayManager.getInstance(), consoles = manager.getConsoles(); + for (let con of consoles) { + const $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", {}, CE("span", { class: "bx-remote-play-device-name" }, con.deviceName), CE("span", { class: "bx-remote-play-console-type" }, con.consoleType.replace("Xbox", ""))), CE("div", { class: "bx-remote-play-power-state" }, this.STATE_LABELS[con.powerState])), createButton({ + classes: ["bx-remote-play-connect-button"], + label: t("console-connect"), + style: 1 | 32, + onClick: (e) => manager.play(con.serverId) + })); + $fragment.appendChild($child); + } + $fragment.appendChild(CE("div", { + class: "bx-remote-play-buttons", + _nearby: { + orientation: "horizontal" + } + }, createButton({ + icon: BxIcon.QUESTION, + style: 4 | 32, + url: "https://better-xcloud.github.io/remote-play", + label: t("help") + }), createButton({ + style: 4 | 32, + label: t("close"), + onClick: (e) => this.hide() + }))), this.$container = $fragment; + } + getDialog() { + return this; + } + getContent() { + return this.$container; + } + focusIfNeeded() { + const $btnConnect = this.$container.querySelector(".bx-remote-play-device-wrapper button"); + $btnConnect && $btnConnect.focus(); + } +} +var LOG_TAG5 = "RemotePlay"; +class RemotePlayManager { + static instance; + static getInstance() { + if (!this.instance) this.instance = new RemotePlayManager; + return this.instance; + } + isInitialized = !1; + XCLOUD_TOKEN; + XHOME_TOKEN; + consoles; + regions = []; + initialize() { + if (this.isInitialized) return; + this.isInitialized = !0, this.getXhomeToken(() => { + this.getConsolesList(() => { + BxLogger.info(LOG_TAG5, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY); + }); + }); + } + get xcloudToken() { + return this.XCLOUD_TOKEN; + } + set xcloudToken(token) { + this.XCLOUD_TOKEN = token; + } + get xhomeToken() { + return this.XHOME_TOKEN; + } + getConsoles() { + return this.consoles; + } + getXhomeToken(callback) { + if (this.XHOME_TOKEN) { + callback(); + return; + } + let GSSV_TOKEN; + try { + GSSV_TOKEN = JSON.parse(localStorage.getItem("xboxcom_xbl_user_info")).tokens["http://gssv.xboxlive.com/"].token; + } catch (e) { + for (let i = 0;i < localStorage.length; i++) { + const key = localStorage.key(i); + if (!key.startsWith("Auth.User.")) continue; + const json = JSON.parse(localStorage.getItem(key)); + for (let token of json.tokens) { + if (!token.relyingParty.includes("gssv.xboxlive.com")) continue; + GSSV_TOKEN = token.tokenData.token; + break; + } + break; + } + } + const request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", { + method: "POST", + body: JSON.stringify({ + offeringId: "xhome", + token: GSSV_TOKEN + }), + headers: { + "Content-Type": "application/json; charset=utf-8" + } + }); + fetch(request).then((resp) => resp.json()).then((json) => { + this.regions = json.offeringSettings.regions, this.XHOME_TOKEN = json.gsToken, callback(); + }); + } + async getConsolesList(callback) { + if (this.consoles) { + callback(); + return; + } + const options = { + method: "GET", + headers: { + Authorization: `Bearer ${this.XHOME_TOKEN}` + } + }; + for (let region of this.regions) + try { + const request = new Request(`${region.baseUri}/v6/servers/home?mr=50`, options), json = await (await fetch(request)).json(); + if (json.results.length === 0) continue; + this.consoles = json.results, STATES.remotePlay.server = region.baseUri; + break; + } catch (e) {} + if (!STATES.remotePlay.server) this.consoles = []; + callback(); + } + play(serverId, resolution) { + if (resolution) setPref("xhome_resolution", resolution); + STATES.remotePlay.config = { + serverId + }, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play"); + } + togglePopup(force = null) { + if (!this.isReady()) { + Toast.show(t("getting-consoles-list")); + return; + } + if (this.consoles.length === 0) { + Toast.show(t("no-consoles-found"), "", { instant: !0 }); + return; + } + if (AppInterface && AppInterface.showRemotePlayDialog) { + AppInterface.showRemotePlayDialog(JSON.stringify(this.consoles)), document.activeElement.blur(); + return; + } + RemotePlayNavigationDialog.getInstance().show(); + } + static detect() { + if (!getPref("xhome_enabled")) return; + if (STATES.remotePlay.isPlaying = window.location.pathname.includes("/launch/") && window.location.hash.startsWith("#remote-play"), STATES.remotePlay?.isPlaying) window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, window.history.replaceState({ origin: "better-xcloud" }, "", "https://www.xbox.com/" + location.pathname.substring(1, 6) + "/play"); + else window.BX_REMOTE_PLAY_CONFIG = null; + } + isReady() { + return this.consoles !== null; + } +} +class XhomeInterceptor { + static #consoleAddrs = {}; + static BASE_DEVICE_INFO = { + appInfo: { + env: { + clientAppId: window.location.host, + clientAppType: "browser", + clientAppVersion: "24.17.36", + clientSdkVersion: "10.1.14", + httpEnvironment: "prod", + sdkInstallId: "" + } + }, + dev: { + displayInfo: { + dimensions: { + widthInPixels: 1920, + heightInPixels: 1080 + }, + pixelDensity: { + dpiX: 1, + dpiY: 1 + } + }, + hw: { + make: "Microsoft", + model: "unknown", + sdktype: "web" + }, + os: { + name: "windows", + ver: "22631.2715", + platform: "desktop" + }, + browser: { + browserName: "chrome", + browserVersion: "125.0" + } + } + }; + static async#handleLogin(request) { + try { + const obj = await request.clone().json(); + obj.offeringId = "xhome", request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", { + method: "POST", + body: JSON.stringify(obj), + headers: { + "Content-Type": "application/json" + } + }); + } catch (e) { + alert(e), console.log(e); + } + return NATIVE_FETCH(request); + } + static async#handleConfiguration(request) { + const response = await NATIVE_FETCH(request), obj = await response.clone().json(); + console.log(obj); + const processPorts = (port) => { + const ports = new Set; + return port && ports.add(port), ports.add(9002), Array.from(ports); + }, serverDetails = obj.serverDetails; + if (serverDetails.ipAddress) XhomeInterceptor.#consoleAddrs[serverDetails.ipAddress] = processPorts(serverDetails.port); + if (serverDetails.ipV4Address) XhomeInterceptor.#consoleAddrs[serverDetails.ipV4Address] = processPorts(serverDetails.ipV4Port); + if (serverDetails.ipV6Address) XhomeInterceptor.#consoleAddrs[serverDetails.ipV6Address] = processPorts(serverDetails.ipV6Port); + return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; + } + static async#handleInputConfigs(request, opts) { + const response = await NATIVE_FETCH(request); + if (getPref("stream_touch_controller") !== "all") return response; + const obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0]; + TouchController.setXboxTitleId(xboxTitleId); + const inputConfigs = obj[0]; + let hasTouchSupport = inputConfigs.supportedTabs.length > 0; + if (!hasTouchSupport) { + const supportedInputTypes = inputConfigs.supportedInputTypes; + hasTouchSupport = supportedInputTypes.includes("NativeTouch") || supportedInputTypes.includes("CustomTouchOverlay"); + } + if (hasTouchSupport) TouchController.disable(), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, { + data: null + }); + else TouchController.enable(), TouchController.requestCustomLayouts(xboxTitleId); + return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; + } + static async#handleTitles(request) { + const clone = request.clone(), headers = {}; + for (let pair of clone.headers.entries()) + headers[pair[0]] = pair[1]; + headers.authorization = `Bearer ${RemotePlayManager.getInstance().xcloudToken}`; + const index = request.url.indexOf(".xboxlive.com"); + return request = new Request("https://wus.core.gssv-play-prod" + request.url.substring(index), { + method: clone.method, + body: await clone.text(), + headers + }), NATIVE_FETCH(request); + } + static async#handlePlay(request) { + const body = await request.clone().json(), newRequest = new Request(request, { + body: JSON.stringify(body) + }); + return NATIVE_FETCH(newRequest); + } + static async handle(request) { + TouchController.disable(); + const clone = request.clone(), headers = {}; + for (let pair of clone.headers.entries()) + headers[pair[0]] = pair[1]; + headers.authorization = `Bearer ${RemotePlayManager.getInstance().xhomeToken}`; + const deviceInfo = XhomeInterceptor.BASE_DEVICE_INFO; + if (getPref("xhome_resolution") === "720p") deviceInfo.dev.os.name = "android"; + headers["x-ms-device-info"] = JSON.stringify(deviceInfo); + const opts = { + method: clone.method, + headers + }; + if (clone.method === "POST") opts.body = await clone.text(); + let newUrl = request.url; + if (!newUrl.includes("/servers/home")) { + const index = request.url.indexOf(".xboxlive.com"); + newUrl = STATES.remotePlay.server + request.url.substring(index + 13); + } + request = new Request(newUrl, opts); + let url = typeof request === "string" ? request : request.url; + if (url.includes("/configuration")) return XhomeInterceptor.#handleConfiguration(request); + else if (url.endsWith("/sessions/home/play")) return XhomeInterceptor.#handlePlay(request); + else if (url.includes("inputconfigs")) return XhomeInterceptor.#handleInputConfigs(request, opts); + else if (url.includes("/login/user")) return XhomeInterceptor.#handleLogin(request); + else if (url.endsWith("/titles")) return XhomeInterceptor.#handleTitles(request); + else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request, XhomeInterceptor.#consoleAddrs); + return await NATIVE_FETCH(request); + } +} +class LoadingScreen { + static #$bgStyle; + static #$waitTimeBox; + static #waitTimeInterval = null; + static #orgWebTitle; + static #secondsToString(seconds) { + const m = Math.floor(seconds / 60), s = Math.floor(seconds % 60), mDisplay = m > 0 ? `${m}m` : "", sDisplay = `${s}s`.padStart(s >= 0 ? 3 : 4, "0"); + return mDisplay + sDisplay; + } + static setup() { + const titleInfo = STATES.currentStream.titleInfo; + if (!titleInfo) return; + if (!LoadingScreen.#$bgStyle) { + const $bgStyle = CE("style"); + document.documentElement.appendChild($bgStyle), LoadingScreen.#$bgStyle = $bgStyle; + } + if (LoadingScreen.#setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getPref("ui_loading_screen_rocket") === "hide") LoadingScreen.#hideRocket(); + } + static #hideRocket() { + let $bgStyle = LoadingScreen.#$bgStyle; + $bgStyle.textContent += "#game-stream div[class*=RocketAnimation-module__container] > svg{display:none}#game-stream video[class*=RocketAnimationVideo-module__video]{display:none}"; + } + static #setBackground(imageUrl) { + let $bgStyle = LoadingScreen.#$bgStyle; + imageUrl = imageUrl + "?w=1920", $bgStyle.textContent += '#game-stream{background-color:transparent !important;background-position:center center !important;background-repeat:no-repeat !important;background-size:cover !important}#game-stream rect[width="800"]{transition:opacity .3s ease-in-out !important}' + `#game-stream {background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;}`; + const bg = new Image; + bg.onload = (e) => { + $bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}'; + }, bg.src = imageUrl; + } + static setupWaitTime(waitTime) { + if (getPref("ui_loading_screen_rocket") === "hide-queue") LoadingScreen.#hideRocket(); + let secondsLeft = waitTime, $countDown, $estimated; + LoadingScreen.#orgWebTitle = document.title; + const endDate = new Date, timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60; + endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds); + let endDateStr = endDate.toISOString().slice(0, 19); + endDateStr = endDateStr.substring(0, 10) + " " + endDateStr.substring(11, 19), endDateStr += ` (${LoadingScreen.#secondsToString(waitTime)})`; + let $waitTimeBox = LoadingScreen.#$waitTimeBox; + if (!$waitTimeBox) $waitTimeBox = CE("div", { class: "bx-wait-time-box" }, CE("label", {}, t("server")), CE("span", {}, getPreferredServerRegion()), CE("label", {}, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", {}, t("wait-time-countdown")), $countDown = CE("span", {})), document.documentElement.appendChild($waitTimeBox), LoadingScreen.#$waitTimeBox = $waitTimeBox; + else $waitTimeBox.classList.remove("bx-gone"), $estimated = $waitTimeBox.querySelector(".bx-wait-time-estimated"), $countDown = $waitTimeBox.querySelector(".bx-wait-time-countdown"); + $estimated.textContent = endDateStr, $countDown.textContent = LoadingScreen.#secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`, LoadingScreen.#waitTimeInterval = window.setInterval(() => { + if (secondsLeft--, $countDown.textContent = LoadingScreen.#secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`, secondsLeft <= 0) LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval), LoadingScreen.#waitTimeInterval = null; + }, 1000); + } + static hide() { + if (LoadingScreen.#orgWebTitle && (document.title = LoadingScreen.#orgWebTitle), LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add("bx-gone"), getPref("ui_loading_screen_game_art") && LoadingScreen.#$bgStyle) { + const $rocketBg = document.querySelector('#game-stream rect[width="800"]'); + $rocketBg && $rocketBg.addEventListener("transitionend", (e) => { + LoadingScreen.#$bgStyle.textContent += "#game-stream{background:#000 !important}"; + }), LoadingScreen.#$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:1 !important}'; + } + setTimeout(LoadingScreen.reset, 2000); + } + static reset() { + LoadingScreen.#$bgStyle && (LoadingScreen.#$bgStyle.textContent = ""), LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add("bx-gone"), LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval), LoadingScreen.#waitTimeInterval = null; + } +} +class GuideMenu { + static #BUTTONS = { + scriptSettings: createButton({ + label: t("better-xcloud"), + style: 64 | 32 | 1, + onClick: (e) => { + window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, (e2) => { + setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50); + }, { once: !0 }), GuideMenu.#closeGuideMenu(); + } + }), + closeApp: AppInterface && createButton({ + icon: BxIcon.POWER, + label: t("close-app"), + title: t("close-app"), + style: 64 | 32 | 2, + onClick: (e) => { + AppInterface.closeApp(); + }, + attributes: { + "data-state": "normal" + } + }), + reloadPage: createButton({ + icon: BxIcon.REFRESH, + label: t("reload-page"), + title: t("reload-page"), + style: 64 | 32, + onClick: (e) => { + if (STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload(); + else window.location.reload(); + GuideMenu.#closeGuideMenu(); + } + }), + backToHome: createButton({ + icon: BxIcon.HOME, + label: t("back-to-home"), + title: t("back-to-home"), + style: 64 | 32, + onClick: (e) => { + confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)), GuideMenu.#closeGuideMenu(); + }, + attributes: { + "data-state": "playing" + } + }) + }; + static #$renderedButtons; + static #closeGuideMenu() { + if (window.BX_EXPOSED.dialogRoutes) { + window.BX_EXPOSED.dialogRoutes.closeAll(); + return; + } + const $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]"); + $btnClose && $btnClose.click(); + } + static #renderButtons() { + if (GuideMenu.#$renderedButtons) return GuideMenu.#$renderedButtons; + const $div = CE("div", { + class: "bx-guide-home-buttons" + }), buttons = [ + GuideMenu.#BUTTONS.scriptSettings, + [ + GuideMenu.#BUTTONS.backToHome, + GuideMenu.#BUTTONS.reloadPage, + GuideMenu.#BUTTONS.closeApp + ] + ]; + for (let $button of buttons) { + if (!$button) continue; + if ($button instanceof HTMLElement) $div.appendChild($button); + else if (Array.isArray($button)) { + const $wrapper = CE("div", {}); + for (let $child of $button) + $child && $wrapper.appendChild($child); + $div.appendChild($wrapper); + } + } + return GuideMenu.#$renderedButtons = $div, $div; + } + static #injectHome($root, isPlaying = !1) { + let $target = null; + if (isPlaying) { + $target = $root.querySelector("a[class*=QuitGameButton]"); + const $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]"); + $btnXcloudHome && ($btnXcloudHome.style.display = "none"); + } else { + const $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]"); + if ($dividers) $target = $dividers[$dividers.length - 1]; + } + if (!$target) return !1; + const $buttons = GuideMenu.#renderButtons(); + $buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons); + } + static async#onShown(e) { + if (e.where === "home") { + const $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]"); + $root && GuideMenu.#injectHome($root, STATES.isPlaying); + } + } + static addEventListeners() { + window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown); + } + static observe($addedElm) { + const className = $addedElm.className; + if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return; + const $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true"); + if ($selectedTab) { + let $elm = $selectedTab, index; + for (index = 0;$elm = $elm?.previousElementSibling; index++) + ; + if (index === 0) BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: "home" }); + } + } +} +var StreamBadgeIcon = { + playtime: BxIcon.PLAYTIME, + video: BxIcon.DISPLAY, + battery: BxIcon.BATTERY, + in: BxIcon.DOWNLOAD, + out: BxIcon.UPLOAD, + server: BxIcon.SERVER, + audio: BxIcon.AUDIO +}; +class StreamBadges { + static instance; + static getInstance() { + if (!StreamBadges.instance) StreamBadges.instance = new StreamBadges; + return StreamBadges.instance; + } + #ipv6 = !1; + #resolution = null; + #video = null; + #audio = null; + #region = ""; + startBatteryLevel = 100; + startTimestamp = 0; + #$container; + #cachedDoms = {}; + #interval; + #REFRESH_INTERVAL = 3000; + setRegion(region) { + this.#region = region; + } + #renderBadge(name, value, color) { + let $badge; + if (this.#cachedDoms[name]) return $badge = this.#cachedDoms[name], $badge.lastElementChild.textContent = value, $badge; + if ($badge = CE("div", { class: "bx-badge", title: t(`badge-${name}`) }, CE("span", { class: "bx-badge-name" }, createSvgIcon(StreamBadgeIcon[name])), CE("span", { class: "bx-badge-value", style: `background-color: ${color}` }, value)), name === "battery") $badge.classList.add("bx-badge-battery"); + return this.#cachedDoms[name] = $badge, $badge; + } + async#updateBadges(forceUpdate = !1) { + if (!this.#$container || !forceUpdate && !this.#$container.isConnected) { + this.#stop(); + return; + } + let now = +new Date; + const diffSeconds = Math.ceil((now - this.startTimestamp) / 1000), playtime = this.#secondsToHm(diffSeconds); + let batteryLevel = "100%", batteryLevelInt = 100, isCharging = !1; + if (STATES.browser.capabilities.batteryApi) try { + const bm = await navigator.getBattery(); + if (isCharging = bm.charging, batteryLevelInt = Math.round(bm.level * 100), batteryLevel = `${batteryLevelInt}%`, batteryLevelInt != this.startBatteryLevel) { + const diffLevel = Math.round(batteryLevelInt - this.startBatteryLevel), sign = diffLevel > 0 ? "+" : ""; + batteryLevel += ` (${sign}${diffLevel}%)`; + } + } catch (e) {} + const stats = await STATES.currentStream.peerConnection?.getStats(); + let totalIn = 0, totalOut = 0; + stats.forEach((stat) => { + if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") totalIn += stat.bytesReceived, totalOut += stat.bytesSent; + }); + const badges = { + in: totalIn ? this.#humanFileSize(totalIn) : null, + out: totalOut ? this.#humanFileSize(totalOut) : null, + playtime: playtime, + battery: batteryLevel + }; + let name; + for (name in badges) { + const value = badges[name]; + if (value === null) continue; + const $elm = this.#cachedDoms[name]; + if ($elm && ($elm.lastElementChild.textContent = value), name === "battery") if (this.startBatteryLevel === 100 && batteryLevelInt === 100) $elm.classList.add("bx-gone"); + else $elm.dataset.charging = isCharging.toString(), $elm.classList.remove("bx-gone"); + } + } + async#start() { + await this.#updateBadges(!0), this.#stop(), this.#interval = window.setInterval(this.#updateBadges.bind(this), this.#REFRESH_INTERVAL); + } + #stop() { + this.#interval && clearInterval(this.#interval), this.#interval = null; + } + #secondsToHm(seconds) { + let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1; + if (m === 60) h += 1, m = 0; + const output = []; + return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); + } + #humanFileSize(size) { + const units = ["B", "KB", "MB", "GB", "TB"], i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); + return (size / Math.pow(1024, i)).toFixed(2) + " " + units[i]; + } + async render() { + if (this.#$container) return this.#start(), this.#$container; + await this.#getServerStats(); + let video = ""; + if (this.#resolution) video = `${this.#resolution.height}p`; + if (this.#video) { + if (video && (video += "/"), video += this.#video.codec, this.#video.profile) { + const profile = this.#video.profile; + let quality = profile; + if (profile.startsWith("4d")) quality = t("visual-quality-high"); + else if (profile.startsWith("42e")) quality = t("visual-quality-normal"); + else if (profile.startsWith("420")) quality = t("visual-quality-low"); + video += ` (${quality})`; + } + } + let audio; + if (this.#audio) { + audio = this.#audio.codec; + const bitrate = this.#audio.bitrate / 1000; + audio += ` (${bitrate} kHz)`; + } + let batteryLevel = ""; + if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%"; + let server = this.#region; + server += "@" + (this.#ipv6 ? "IPv6" : "IPv4"); + const BADGES = [ + ["playtime", "1m", "#ff004d"], + ["battery", batteryLevel, "#00b543"], + ["in", this.#humanFileSize(0), "#29adff"], + ["out", this.#humanFileSize(0), "#ff77a8"], + ["server", server, "#ff6c24"], + video ? ["video", video, "#742f29"] : null, + audio ? ["audio", audio, "#5f574f"] : null + ], $container = CE("div", { class: "bx-badges" }); + return BADGES.forEach((item2) => { + if (!item2) return; + const $badge = this.#renderBadge(...item2); + $container.appendChild($badge); + }), this.#$container = $container, await this.#start(), $container; + } + async#getServerStats() { + const stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}; + let videoCodecId; + const allAudioCodecs = {}; + let audioCodecId; + const allCandidates = {}; + let candidateId; + if (stats.forEach((stat) => { + if (stat.type === "codec") { + const mimeType = stat.mimeType.split("/")[0]; + if (mimeType === "video") allVideoCodecs[stat.id] = stat; + else if (mimeType === "audio") allAudioCodecs[stat.id] = stat; + } else if (stat.type === "inbound-rtp" && stat.packetsReceived > 0) { + if (stat.kind === "video") videoCodecId = stat.codecId; + else if (stat.kind === "audio") audioCodecId = stat.codecId; + } else if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") candidateId = stat.remoteCandidateId; + else if (stat.type === "remote-candidate") allCandidates[stat.id] = stat.address; + }), videoCodecId) { + const videoStat = allVideoCodecs[videoCodecId], video = { + codec: videoStat.mimeType.substring(6) + }; + if (video.codec === "H264") { + const match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine); + video.profile = match ? match[1] : null; + } + this.#video = video; + } + if (audioCodecId) { + const audioStat = allAudioCodecs[audioCodecId]; + this.#audio = { + codec: audioStat.mimeType.substring(6), + bitrate: audioStat.clockRate + }; + } + if (candidateId) BxLogger.info("candidate", candidateId, allCandidates), this.#ipv6 = allCandidates[candidateId].includes(":"); + } + static setupEvents() { + window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { + const $video = e.$video, streamBadges = StreamBadges.getInstance(); + streamBadges.#resolution = { + width: $video.videoWidth, + height: $video.videoHeight + }, streamBadges.startTimestamp = +new Date; + try { + STATES.browser.capabilities.batteryApi && navigator.getBattery().then((bm) => { + streamBadges.startBatteryLevel = Math.round(bm.level * 100); + }); + } catch (e2) {} + }), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, async (e) => { + if (e.where !== "home" || !STATES.isPlaying) return; + const $btnQuit = document.querySelector("#gamepass-dialog-root a[class*=QuitGameButton]"); + if ($btnQuit) $btnQuit.insertAdjacentElement("beforebegin", await StreamBadges.getInstance().render()); + }); + } +} +class XcloudInterceptor { + static async#handleLogin(request, init) { + const bypassServer = getPref("server_bypass_restriction"); + if (bypassServer !== "off") { + const ip = BypassServerIps[bypassServer]; + ip && request.headers.set("X-Forwarded-For", ip); + } + const response = await NATIVE_FETCH(request, init); + if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response; + const obj = await response.clone().json(); + RemotePlayManager.getInstance().xcloudToken = obj.gsToken; + const serverEmojis = { + AustraliaEast: "🇦🇺", + AustraliaSouthEast: "🇦🇺", + BrazilSouth: "🇧🇷", + EastUS: "🇺🇸", + EastUS2: "🇺🇸", + JapanEast: "🇯🇵", + KoreaCentral: "🇰🇷", + MexicoCentral: "🇲🇽", + NorthCentralUs: "🇺🇸", + SouthCentralUS: "🇺🇸", + UKSouth: "🇬🇧", + WestEurope: "🇪🇺", + WestUS: "🇺🇸", + WestUS2: "🇺🇸" + }, serverRegex = /\/\/(\w+)\./; + for (let region of obj.offeringSettings.regions) { + const regionName = region.name; + let shortName = region.name; + if (region.isDefault) STATES.selectedRegion = Object.assign({}, region); + let match = serverRegex.exec(region.baseUri); + if (match) { + if (shortName = match[1], serverEmojis[regionName]) shortName = serverEmojis[regionName] + " " + shortName; + } + region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region); + } + BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY); + const preferredRegion = getPreferredServerRegion(); + if (preferredRegion && preferredRegion in STATES.serverRegions) { + const tmp = Object.assign({}, STATES.serverRegions[preferredRegion]); + tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp; + } + return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response; + } + static async#handlePlay(request, init) { + const PREF_STREAM_TARGET_RESOLUTION = getPref("stream_target_resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream_preferred_locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url); + let badgeRegion = parsedUrl.host.split(".", 1)[0]; + for (let regionName in STATES.serverRegions) { + const region = STATES.serverRegions[regionName]; + if (parsedUrl.origin == region.baseUri) { + badgeRegion = regionName; + break; + } + } + StreamBadges.getInstance().setRegion(badgeRegion); + const body = await request.clone().json(); + if (PREF_STREAM_TARGET_RESOLUTION !== "auto") { + const osName = PREF_STREAM_TARGET_RESOLUTION === "720p" ? "android" : "windows"; + body.settings.osName = osName; + } + if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE; + const newRequest = new Request(request, { + body: JSON.stringify(body) + }); + return NATIVE_FETCH(newRequest); + } + static async#handleWaitTime(request, init) { + const response = await NATIVE_FETCH(request, init); + if (getPref("ui_loading_screen_wait_time")) { + const json = await response.clone().json(); + if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds); + } + return response; + } + static async#handleConfiguration(request, init) { + if (request.method !== "GET") return NATIVE_FETCH(request, init); + if (getPref("stream_touch_controller") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable(); + else TouchController.enable(); + const response = await NATIVE_FETCH(request, init), text = await response.clone().text(); + if (!text.length) return response; + const obj = JSON.parse(text); + let overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {}; + overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0; + let overrideMkb = null; + if (getPref("native_mkb_enabled") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0; + if (getPref("native_mkb_enabled") === "off") overrideMkb = !1; + if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, { + enableMouseInput: overrideMkb, + enableKeyboardInput: overrideMkb + }); + if (TouchController.isEnabled()) overrides.inputConfiguration.enableTouchInput = !0, overrides.inputConfiguration.maxTouchPoints = 10; + if (getPref("audio_mic_on_playing")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0; + return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; + } + static async handle(request, init) { + let url = typeof request === "string" ? request : request.url; + if (url.endsWith("/v2/login/user")) return XcloudInterceptor.#handleLogin(request, init); + else if (url.endsWith("/sessions/cloud/play")) return XcloudInterceptor.#handlePlay(request, init); + else if (url.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.#handleWaitTime(request, init); + else if (url.endsWith("/configuration")) return XcloudInterceptor.#handleConfiguration(request, init); + else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request); + return NATIVE_FETCH(request, init); + } +} +function clearApplicationInsightsBuffers() { + window.sessionStorage.removeItem("AI_buffer"), window.sessionStorage.removeItem("AI_sentBuffer"); +} +function clearDbLogs(dbName, table) { + const request = window.indexedDB.open(dbName); + request.onsuccess = (e) => { + const db = e.target.result; + try { + const objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear(); + objectStoreRequest.onsuccess = function() { + console.log(`[Better xCloud] Cleared ${dbName}.${table}`); + }; + } catch (ex) {} + }; +} +function clearAllLogs() { + clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs"); +} +function updateIceCandidates(candidates, options) { + const pattern = new RegExp(/a=candidate:(?\d+) (?\d+) UDP (?\d+) (?[^\s]+) (?\d+) (?.*)/), lst = []; + for (let item2 of candidates) { + if (item2.candidate == "a=end-of-candidates") continue; + const groups = pattern.exec(item2.candidate).groups; + lst.push(groups); + } + if (options.preferIpv6Server) lst.sort((a, b) => { + const firstIp = a.ip, secondIp = b.ip; + return !firstIp.includes(":") && secondIp.includes(":") ? 1 : -1; + }); + const newCandidates = []; + let foundation = 1; + const newCandidate = (candidate) => { + return { + candidate, + messageType: "iceCandidate", + sdpMLineIndex: "0", + sdpMid: "0" + }; + }; + if (lst.forEach((item2) => { + item2.foundation = foundation, item2.priority = foundation == 1 ? 2130706431 : 1, newCandidates.push(newCandidate(`a=candidate:${item2.foundation} 1 UDP ${item2.priority} ${item2.ip} ${item2.port} ${item2.the_rest}`)), ++foundation; + }), options.consoleAddrs) + for (let ip in options.consoleAddrs) + for (let port of options.consoleAddrs[ip]) + newCandidates.push(newCandidate(`a=candidate:${newCandidates.length + 1} 1 UDP 1 ${ip} ${port} typ host`)); + return newCandidates.push(newCandidate("a=end-of-candidates")), BxLogger.info("ICE Candidates", newCandidates), newCandidates; +} +async function patchIceCandidates(request, consoleAddrs) { + const response = await NATIVE_FETCH(request), text = await response.clone().text(); + if (!text.length) return response; + const options = { + preferIpv6Server: getPref("prefer_ipv6_server"), + consoleAddrs + }, obj = JSON.parse(text); + let exchangeResponse = JSON.parse(obj.exchangeResponse); + return exchangeResponse = updateIceCandidates(exchangeResponse, options), obj.exchangeResponse = JSON.stringify(exchangeResponse), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; +} +function interceptHttpRequests() { + let BLOCKED_URLS = []; + if (getPref("block_tracking")) clearAllLogs(), BLOCKED_URLS = BLOCKED_URLS.concat([ + "https://arc.msn.com", + "https://browser.events.data.microsoft.com", + "https://dc.services.visualstudio.com", + "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io" + ]); + if (getPref("block_social_features")) BLOCKED_URLS = BLOCKED_URLS.concat([ + "https://peoplehub.xboxlive.com/users/me/people/social", + "https://peoplehub.xboxlive.com/users/me/people/recommendations", + "https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox" + ]); + const xhrPrototype = XMLHttpRequest.prototype, nativeXhrOpen = xhrPrototype.open, nativeXhrSend = xhrPrototype.send; + xhrPrototype.open = function(method, url) { + return this._url = url, nativeXhrOpen.apply(this, arguments); + }, xhrPrototype.send = function(...arg) { + for (let blocked of BLOCKED_URLS) + if (this._url.startsWith(blocked)) { + if (blocked === "https://dc.services.visualstudio.com") window.setTimeout(clearAllLogs, 1000); + return !1; + } + return nativeXhrSend.apply(this, arguments); + }; + let gamepassAllGames = []; + window.BX_FETCH = window.fetch = async (request, init) => { + let url = typeof request === "string" ? request : request.url; + for (let blocked of BLOCKED_URLS) { + if (!url.startsWith(blocked)) continue; + return new Response('{"acc":1,"webResult":{}}', { + status: 200, + statusText: "200 OK" + }); + } + if (url.endsWith("/play")) BxEvent.dispatch(window, BxEvent.STREAM_LOADING); + if (url.endsWith("/configuration")) BxEvent.dispatch(window, BxEvent.STREAM_STARTING); + if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try { + const response = await NATIVE_FETCH(request, init), json = await response.json(); + if (json && json.exp && json.exp.treatments) for (let key in FeatureGates) + json.exp.treatments[key] = FeatureGates[key]; + return response.json = () => Promise.resolve(json), response; + } catch (e) { + console.log(e); + } + if (STATES.userAgent.capabilities.touch && url.includes("catalog.gamepass.com/sigls/")) { + const response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); + if (url.includes("29a81209-df6f-41fd-a528-2ae6b91f719c")) for (let i = 1;i < obj.length; i++) + gamepassAllGames.push(obj[i].id); + else if (url.includes("9c86f07a-f3e8-45ad-82a0-a1f759597059")) try { + let customList = TouchController.getCustomList(); + customList = customList.filter((id2) => gamepassAllGames.includes(id2)); + const newCustomList = customList.map((item2) => ({ id: item2 })); + obj.push(...newCustomList); + } catch (e) { + console.log(e); + } + return response.json = () => Promise.resolve(obj), response; + } + if (BX_FLAGS.ForceNativeMkbTitles && url.includes("catalog.gamepass.com/sigls/") && url.includes("8fa264dd-124f-4af3-97e8-596fcdf4b486")) { + const response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); + try { + const newCustomList = BX_FLAGS.ForceNativeMkbTitles.map((item2) => ({ id: item2 })); + obj.push(...newCustomList); + } catch (e) { + console.log(e); + } + return response.json = () => Promise.resolve(obj), response; + } + let requestType; + if (url.includes("/sessions/home") || url.includes("xhome.") || STATES.remotePlay.isPlaying && url.endsWith("/inputconfigs")) requestType = "xhome"; + else requestType = "xcloud"; + if (requestType === "xhome") return XhomeInterceptor.handle(request); + return XcloudInterceptor.handle(request, init); + }; +} +function showGamepadToast(gamepad) { + if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) return; + BxLogger.info("Gamepad", gamepad); + let text = "🎮"; + if (getPref("local_co_op_enabled")) text += ` #${gamepad.index + 1}`; + const gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, ""); + text += ` - ${gamepadId}`; + let status; + if (gamepad.connected) status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status"); + else status = t("disconnected"); + Toast.show(text, status, { instant: !1 }); +} +function addCss() { + let css = `:root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#df5656;--bx-danger-button-disabled-rgb:223,86,86;--bx-fullscreen-text-z-index:99999;--bx-toast-z-index:60000;--bx-dialog-z-index:50000;--bx-dialog-overlay-z-index:40200;--bx-stats-bar-z-index:40100;--bx-mkb-pointer-lock-msg-z-index:40000;--bx-navigation-dialog-z-index:30100;--bx-navigation-dialog-overlay-z-index:30000;--bx-game-bar-z-index:10000;--bx-screenshot-animation-z-index:9000;--bx-wait-time-box-z-index:1000}@font-face{font-family:'promptfont';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf")}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font)}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}select[multiple]{overflow:auto}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}div[class*=SupportedInputsBadge]:not(:has(:nth-child(2))),div[class*=SupportedInputsBadge] svg:first-of-type{display:none}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.549);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb))}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha));backdrop-filter:blur(4px) brightness(1.5)}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-left:10px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:'';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:'🌟' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-dialog-overlay-z-index);background:#000;opacity:50%}.bx-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:20px;border-radius:8px;z-index:var(--bx-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-dialog *:focus{outline:none !important}.bx-dialog h2{display:flex;margin-bottom:12px}.bx-dialog h2 b{flex:1;color:#fff;display:block;font-family:var(--bx-title-font);font-size:26px;font-weight:400;line-height:var(--bx-button-height)}.bx-dialog.bx-binding-dialog h2 b{font-family:var(--bx-promptfont-font) !important}.bx-dialog > div{overflow:auto;padding:2px 0}.bx-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-dialog > button:hover{background-color:#515863}}.bx-dialog > button:focus{background-color:#515863}@media screen and (max-width:450px){.bx-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-focusable::after{border-radius:4px}.bx-settings-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-settings-dialog select option:disabled{display:none}.bx-settings-dialog input[type=checkbox]:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-settings-dialog a:hover,.bx-settings-dialog a:focus{color:#5dc21e}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;padding:10px;margin-left:48px;width:450px;max-width:calc(100vw - tabsWidth);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents > div[data-tab-group=mkb]{display:flex;flex-direction:column;height:100%;overflow:hidden}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:first-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:last-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:first-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:last-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-profile{width:100%;height:36px;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-note{margin-top:10px;font-size:14px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row{display:flex;margin-bottom:10px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row label.bx-prompt{flex:1;font-size:26px;margin-bottom:0}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions{flex:2;position:relative}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select{position:absolute;width:100%;height:100%;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select:last-of-type{opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:10px;border-top-right-radius:10px}.bx-settings-tab-contents > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:10px;border-bottom-right-radius:10px}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:10px}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861}.bx-suggest-toggler label{flex:1;margin-bottom:0;padding:10px;background:#004f87}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:40px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none;background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-container{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;width:420px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:20px}.bx-remote-play-container > .bx-button{display:table;margin:0 0 0 auto}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;padding:4px 0}.bx-remote-play-device-name{font-size:20px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:12px;background:#004c87;color:#fff;display:inline-block;border-radius:14px;padding:2px 10px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}.bx-select{display:flex;align-items:center;flex:0 1 auto}.bx-select select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-select > div,.bx-select button.bx-select-value{min-width:120px;text-align:left;margin:0 8px;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;flex:1}.bx-select > div{display:inline-block}.bx-select > div input{display:inline-block;margin-right:8px}.bx-select > div label{margin-bottom:0;font-size:14px;width:100%}.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:initial}.bx-select button.bx-select-value{border:none;display:inline-flex;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color)}.bx-select button.bx-select-value:hover input,.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}.bx-select button.bx-select-value:hover::after,.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}.bx-select button.bx-button{border:none;height:24px;width:24px;padding:0;line-height:24px;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}.bx-select button.bx-button span{line-height:unset}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}html[data-xds-platform=tv] .bx-guide-home-achievements-progress{flex-direction:column}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress{flex-direction:row}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}html[data-xds-platform=tv] .bx-guide-home-buttons > div{flex-direction:column}html[data-xds-platform=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}html:not([data-xds-platform=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state='normal']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state='playing']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container]{display:flex}div[data-testid=media-container].bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:' ';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#game-stream video{margin:auto;align-self:center;background:#000}#game-stream canvas{position:absolute;align-self:center;margin:auto;left:0;right:0}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper span{display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);font-size:12px;margin:0 4px}.bx-number-stepper button{border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type="range"]{display:block;margin:12px auto 2px;width:180px;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-enabled] button{display:none}#bx-game-bar .bx-game-bar-container div[data-enabled='true'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-enabled='false'] 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:block;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*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.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{display:inline-block}.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.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{margin-right:0;border-right:none}.bx-stats-bar::before{display:none;content:'👀';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-transparent=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;margin-right:8px;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:inherit;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{min-width:60px;display:inline-block;text-align:right;vertical-align:middle}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-stats-bar span:first-of-type{min-width:22px}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-settings select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;text-align:right;border:none;color:#fff}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:50%;transform:translateX(-50%) translateY(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;text-align:center;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:220px;opacity:.9}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > div:first-of-type{display:flex;flex-direction:column;text-align:left}.bx-mkb-pointer-lock-msg p{margin:0}.bx-mkb-pointer-lock-msg p:first-child{font-size:22px;margin-bottom:4px;font-weight:bold}.bx-mkb-pointer-lock-msg p:last-child{font-size:12px;font-style:italic}.bx-mkb-pointer-lock-msg > div:last-of-type{margin-top:10px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='native'] button:first-of-type{margin-bottom:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div{display:flex;flex-flow:row;margin-top:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button{flex:1}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button:first-of-type{margin-right:5px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button:last-of-type{margin-left:5px}.bx-mkb-preset-tools{display:flex;margin-bottom:12px}.bx-mkb-preset-tools select{flex:1}.bx-mkb-preset-tools button{margin-left:6px}.bx-mkb-settings-rows{flex:1;overflow:scroll}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:26px;text-align:center;width:26px;height:32px;line-height:32px}.bx-mkb-key-row button{flex:1;height:32px;line-height:32px;margin:0 0 0 10px;background:transparent;border:none;color:#fff;border-radius:0;border-left:1px solid #373737}.bx-mkb-key-row button:hover{background:transparent;cursor:default}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:16px 0 10px;font-size:12px}.bx-mkb-note:first-of-type{margin-top:0}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}`; + const PREF_HIDE_SECTIONS = getPref("ui_hide_sections"), selectorToHide = []; + if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]"); + if (PREF_HIDE_SECTIONS.includes("all-games")) selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__gridContainer]"), selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__rowHeader]"); + if (PREF_HIDE_SECTIONS.includes("most-popular")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/popular"])'); + if (PREF_HIDE_SECTIONS.includes("touch")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/touch"])'); + if (getPref("block_social_features")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]"); + if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }"; + if (getPref("reduce_animations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}"; + if (getPref("hide_dots_icon")) css += "div[class*=Grip-module__container]{visibility:hidden}@media (hover:hover){button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container]{visibility:visible}}button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container]{visibility:visible}button[class*=GripHandle-module__container][aria-expanded=false]{background-color:transparent !important}div[class*=StreamHUD-module__buttonsContainer]{padding:0 !important}"; + if (css += "div[class*=StreamMenu-module__menu]{min-width:100vw !important}", getPref("stream_simplify_menu")) css += "div[class*=Menu-module__scrollable]{--bxStreamMenuItemSize:80px;--streamMenuItemSize:calc(var(--bxStreamMenuItemSize) + 40px) !important}.bx-badges{top:calc(var(--streamMenuItemSize) - 20px)}body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) - 10px) !important}button[class*=MenuItem-module__container]{min-width:auto !important;min-height:auto !important;width:var(--bxStreamMenuItemSize) !important;height:var(--bxStreamMenuItemSize) !important}div[class*=MenuItem-module__label]{display:none !important}svg[class*=MenuItem-module__icon]{width:36px;height:100% !important;padding:0 !important;margin:0 !important}"; + else css += "body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) + 30px)}body:not([data-media-type=tv]) .bx-badges{top:calc(var(--streamMenuItemSize) + 20px)}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]{min-width:auto !important;width:100px !important}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]:nth-child(n+2){margin-left:10px !important}body:not([data-media-type=tv]) div[class*=MenuItem-module__label]{margin-left:8px !important;margin-right:8px !important}"; + if (getPref("ui_scrollbar_hide")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}"; + const $style = CE("style", {}, css); + document.documentElement.appendChild($style); +} +function preloadFonts() { + const $link = CE("link", { + rel: "preload", + href: "https://redphx.github.io/better-xcloud/fonts/promptfont.otf", + as: "font", + type: "font/otf", + crossorigin: "" + }); + document.querySelector("head")?.appendChild($link); +} +class MouseCursorHider { + static #timeout; + static #cursorVisible = !0; + static show() { + document.body && (document.body.style.cursor = "unset"), MouseCursorHider.#cursorVisible = !0; + } + static hide() { + document.body && (document.body.style.cursor = "none"), MouseCursorHider.#timeout = null, MouseCursorHider.#cursorVisible = !1; + } + static onMouseMove(e) { + !MouseCursorHider.#cursorVisible && MouseCursorHider.show(), MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), MouseCursorHider.#timeout = window.setTimeout(MouseCursorHider.hide, 3000); + } + static start() { + MouseCursorHider.show(), document.addEventListener("mousemove", MouseCursorHider.onMouseMove); + } + static stop() { + MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), document.removeEventListener("mousemove", MouseCursorHider.onMouseMove), MouseCursorHider.show(); + } +} +function patchHistoryMethod(type) { + const orig = window.history[type]; + return function(...args) { + return BxEvent.dispatch(window, BxEvent.POPSTATE, { + arguments: args + }), orig.apply(this, arguments); + }; +} +function onHistoryChanged(e) { + if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return; + window.setTimeout(RemotePlayManager.detect, 10); + const $settings = document.querySelector(".bx-settings-container"); + if ($settings) $settings.classList.add("bx-gone"); + NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); +} +function setCodecPreferences(sdp, preferredCodec) { + const h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || []; + for (let match of matches) { + const id2 = match[1]; + if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id2); + } + if (!preferredCodecIds.length) return sdp; + const lines = sdp.split("\r\n"); + for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex]; + if (!line.startsWith("m=video")) continue; + const tmp = line.trim().split(" "); + let ids = tmp.slice(3); + ids = ids.filter((item2) => !preferredCodecIds.includes(item2)), ids = preferredCodecIds.concat(ids), lines[lineIndex] = tmp.slice(0, 3).concat(ids).join(" "); + break; + } + return lines.join("\r\n"); +} +function patchSdpBitrate(sdp, video, audio) { + const lines = sdp.split("\r\n"), mediaSet = new Set; + !!video && mediaSet.add("video"), !!audio && mediaSet.add("audio"); + const bitrate = { + video, + audio + }; + for (let lineNumber = 0;lineNumber < lines.length; lineNumber++) { + let media = "", line = lines[lineNumber]; + if (!line.startsWith("m=")) continue; + for (let m of mediaSet) + if (line.startsWith(`m=${m}`)) { + media = m, mediaSet.delete(media); + break; + } + if (!media) continue; + const bLine = `b=AS:${bitrate[media]}`; + while (lineNumber++, lineNumber < lines.length) { + if (line = lines[lineNumber], line.startsWith("i=") || line.startsWith("c=")) continue; + if (line.startsWith("b=AS:")) { + lines[lineNumber] = bLine; + break; + } + if (line.startsWith("m=")) { + lines.splice(lineNumber, 0, bLine); + break; + } + } + } + return lines.join("\r\n"); +} +var clarity_boost_default = "attribute vec2 position;\n\nvoid main() {\n gl_Position = vec4(position, 0, 1);\n}\n"; +var clarity_boost_default2 = "const int FILTER_UNSHARP_MASKING = 1;\nconst int FILTER_CAS = 2;\n\nprecision highp float;\nuniform sampler2D data;\nuniform vec2 iResolution;\n\nuniform int filterId;\nuniform float sharpenFactor;\nuniform float brightness;\nuniform float contrast;\nuniform float saturation;\n\nvec3 textureAt(sampler2D tex, vec2 coord) {\n return texture2D(tex, coord / iResolution.xy).rgb;\n}\n\nvec3 clarityBoost(sampler2D tex, vec2 coord)\n{\n // Load a collection of samples in a 3x3 neighorhood, where e is the current pixel.\n // a b c\n // d e f\n // g h i\n vec3 a = textureAt(tex, coord + vec2(-1, 1));\n vec3 b = textureAt(tex, coord + vec2(0, 1));\n vec3 c = textureAt(tex, coord + vec2(1, 1));\n\n vec3 d = textureAt(tex, coord + vec2(-1, 0));\n vec3 e = textureAt(tex, coord);\n vec3 f = textureAt(tex, coord + vec2(1, 0));\n\n vec3 g = textureAt(tex, coord + vec2(-1, -1));\n vec3 h = textureAt(tex, coord + vec2(0, -1));\n vec3 i = textureAt(tex, coord + vec2(1, -1));\n\n if (filterId == FILTER_CAS) {\n // Soft min and max.\n // a b c b\n // d e f * 0.5 + d e f * 0.5\n // g h i h\n // These are 2.0x bigger (factored out the extra multiply).\n vec3 minRgb = min(min(min(d, e), min(f, b)), h);\n vec3 minRgb2 = min(min(a, c), min(g, i));\n minRgb += min(minRgb, minRgb2);\n\n vec3 maxRgb = max(max(max(d, e), max(f, b)), h);\n vec3 maxRgb2 = max(max(a, c), max(g, i));\n maxRgb += max(maxRgb, maxRgb2);\n\n // Smooth minimum distance to signal limit divided by smooth max.\n vec3 reciprocalMaxRgb = 1.0 / maxRgb;\n vec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0);\n\n // Shaping amount of sharpening.\n amplifyRgb = inversesqrt(amplifyRgb);\n\n float contrast = 0.8;\n float peak = -3.0 * contrast + 8.0;\n vec3 weightRgb = -(1.0 / (amplifyRgb * peak));\n\n vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);\n\n // 0 w 0\n // Filter shape: w 1 w\n // 0 w 0\n vec3 window = (b + d) + (f + h);\n vec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);\n\n outColor = mix(e, outColor, sharpenFactor / 2.0);\n\n return outColor;\n } else if (filterId == FILTER_UNSHARP_MASKING) {\n vec3 gaussianBlur = (a * 1.0 + b * 2.0 + c * 1.0 +\n d * 2.0 + e * 4.0 + f * 2.0 +\n g * 1.0 + h * 2.0 + i * 1.0) / 16.0;\n\n // Return edge detection\n return e + (e - gaussianBlur) * sharpenFactor / 3.0;\n }\n\n return e;\n}\n\nvec3 adjustBrightness(vec3 color) {\n return (1.0 + brightness) * color;\n}\n\nvec3 adjustContrast(vec3 color) {\n return 0.5 + (1.0 + contrast) * (color - 0.5);\n}\n\nvec3 adjustSaturation(vec3 color) {\n const vec3 luminosityFactor = vec3(0.2126, 0.7152, 0.0722);\n vec3 grayscale = vec3(dot(color, luminosityFactor));\n\n return mix(grayscale, color, 1.0 + saturation);\n}\n\nvoid main() {\n vec3 color;\n\n if (sharpenFactor > 0.0) {\n color = clarityBoost(data, gl_FragCoord.xy);\n } else {\n color = textureAt(data, gl_FragCoord.xy);\n }\n\n if (saturation != 0.0) {\n color = adjustSaturation(color);\n }\n\n if (contrast != 0.0) {\n color = adjustContrast(color);\n }\n\n if (brightness != 0.0) {\n color = adjustBrightness(color);\n }\n\n gl_FragColor = vec4(color, 1.0);\n}\n"; +var LOG_TAG6 = "WebGL2Player"; +class WebGL2Player { + #$video; + #$canvas; + #gl = null; + #resources = []; + #program = null; + #stopped = !1; + #options = { + filterId: 1, + sharpenFactor: 0, + brightness: 0, + contrast: 0, + saturation: 0 + }; + #animFrameId = null; + constructor($video) { + BxLogger.info(LOG_TAG6, "Initialize"), this.#$video = $video; + const $canvas = document.createElement("canvas"); + $canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.#$canvas = $canvas, this.#setupShaders(), this.#setupRendering(), $video.insertAdjacentElement("afterend", $canvas); + } + setFilter(filterId, update = !0) { + this.#options.filterId = filterId, update && this.updateCanvas(); + } + setSharpness(sharpness, update = !0) { + this.#options.sharpenFactor = sharpness, update && this.updateCanvas(); + } + setBrightness(brightness, update = !0) { + this.#options.brightness = (brightness - 100) / 100, update && this.updateCanvas(); + } + setContrast(contrast, update = !0) { + this.#options.contrast = (contrast - 100) / 100, update && this.updateCanvas(); + } + setSaturation(saturation, update = !0) { + this.#options.saturation = (saturation - 100) / 100, update && this.updateCanvas(); + } + getCanvas() { + return this.#$canvas; + } + updateCanvas() { + const gl = this.#gl, program = this.#program; + gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.#$canvas.width, this.#$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.#options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.#options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.#options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.#options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.#options.saturation); + } + drawFrame() { + const gl = this.#gl, $video = this.#$video; + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, $video), gl.drawArrays(gl.TRIANGLES, 0, 6); + } + #setupRendering() { + let animate; + if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) { + const $video = this.#$video; + animate = () => { + if (this.#stopped) return; + this.drawFrame(), this.#animFrameId = $video.requestVideoFrameCallback(animate); + }, this.#animFrameId = $video.requestVideoFrameCallback(animate); + } else animate = () => { + if (this.#stopped) return; + this.drawFrame(), this.#animFrameId = requestAnimationFrame(animate); + }, this.#animFrameId = requestAnimationFrame(animate); + } + #setupShaders() { + BxLogger.info(LOG_TAG6, "Setting up", getPref("video_power_preference")); + const gl = this.#$canvas.getContext("webgl", { + isBx: !0, + antialias: !0, + alpha: !1, + powerPreference: getPref("video_power_preference") + }); + this.#gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); + const vShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader); + const fShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader); + const program = gl.createProgram(); + if (this.#program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`); + this.updateCanvas(); + const buffer = gl.createBuffer(); + this.#resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + -1, + -1, + 1, + -1, + -1, + 1, + -1, + 1, + 1, + -1, + 1, + 1 + ]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0); + const texture = gl.createTexture(); + this.#resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0); + } + resume() { + this.stop(), this.#stopped = !1, BxLogger.info(LOG_TAG6, "Resume"), this.#$canvas.classList.remove("bx-gone"), this.#setupRendering(); + } + stop() { + if (BxLogger.info(LOG_TAG6, "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(LOG_TAG6, "Destroy"), this.stop(); + const gl = this.#gl; + if (gl) { + gl.getExtension("WEBGL_lose_context")?.loseContext(); + for (let resource of this.#resources) + if (resource instanceof WebGLProgram) gl.useProgram(null), gl.deleteProgram(resource); + else if (resource instanceof WebGLShader) gl.deleteShader(resource); + else if (resource instanceof WebGLTexture) gl.deleteTexture(resource); + else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource); + this.#gl = null; + } + if (this.#$canvas.isConnected) this.#$canvas.parentElement?.removeChild(this.#$canvas); + this.#$canvas.width = 1, this.#$canvas.height = 1; + } +} +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) { + this.#$usmMatrix = this.#$videoCss.querySelector("#bx-filter-usm-matrix"); + return; + } + const $fragment = document.createDocumentFragment(); + this.#$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.#$videoCss); + const $svg = CE("svg", { + id: "bx-video-filters", + xmlns: "http://www.w3.org/2000/svg", + class: "bx-gone" + }, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", { + id: "bx-filter-usm", + xmlns: "http://www.w3.org/2000/svg" + }, this.#$usmMatrix = CE("feConvolveMatrix", { + id: "bx-filter-usm-matrix", + order: "3", + xmlns: "http://www.w3.org/2000/svg" + })))); + $fragment.appendChild($svg), document.documentElement.appendChild($fragment); + } + #getVideoPlayerFilterStyle() { + const filters = [], sharpness = this.#options.sharpness || 0; + if (this.#options.processing === "usm" && sharpness != 0) { + const matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`; + this.#$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)"); + } + const saturation = this.#options.saturation || 100; + if (saturation != 100) filters.push(`saturate(${saturation}%)`); + const contrast = this.#options.contrast || 100; + if (contrast != 100) filters.push(`contrast(${contrast}%)`); + const brightness = this.#options.brightness || 100; + if (brightness != 100) filters.push(`brightness(${brightness}%)`); + return filters.join(" "); + } + #resizePlayer() { + const PREF_RATIO = getPref("video_ratio"), $video = this.#$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport; + let $webGL2Canvas; + if (this.#playerType == "webgl2") $webGL2Canvas = this.#webGL2Player?.getCanvas(); + let targetWidth, targetHeight, targetObjectFit; + if (PREF_RATIO.includes(":")) { + const tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]); + let width = 0, height = 0; + const parentRect = $video.parentElement.getBoundingClientRect(); + if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio; + else width = parentRect.width, height = width / videoRatio; + width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString(), targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill"; + } else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString(); + if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit; + if (isNativeTouchGame && this.#playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions(); + } + setPlayerType(type, refreshPlayer = !1) { + if (this.#playerType !== type) if (type === "webgl2") { + if (!this.#webGL2Player) this.#webGL2Player = new WebGL2Player(this.#$video); + else this.#webGL2Player.resume(); + this.#$videoCss.textContent = "", this.#$video.classList.add("bx-pixel"); + } else this.#webGL2Player?.stop(), this.#$video.classList.remove("bx-pixel"); + this.#playerType = type, refreshPlayer && this.refreshPlayer(); + } + setOptions(options, refreshPlayer = !1) { + this.#options = options, refreshPlayer && this.refreshPlayer(); + } + updateOptions(options, refreshPlayer = !1) { + this.#options = Object.assign(this.#options, options), refreshPlayer && this.refreshPlayer(); + } + getPlayerElement(playerType) { + if (typeof playerType === "undefined") playerType = this.#playerType; + if (playerType === "webgl2") return this.#webGL2Player?.getCanvas(); + return this.#$video; + } + getWebGL2Player() { + return this.#webGL2Player; + } + refreshPlayer() { + if (this.#playerType === "webgl2") { + const options = this.#options, webGL2Player = this.#webGL2Player; + if (options.processing === "usm") webGL2Player.setFilter(1); + else webGL2Player.setFilter(2); + webGL2Player.setSharpness(options.sharpness || 0), webGL2Player.setSaturation(options.saturation || 100), webGL2Player.setContrast(options.contrast || 100), webGL2Player.setBrightness(options.brightness || 100); + } else { + let filters = this.#getVideoPlayerFilterStyle(), videoCss = ""; + if (filters) videoCss += `filter: ${filters} !important;`; + let css = ""; + if (videoCss) css = `#game-stream video { ${videoCss} }`; + this.#$videoCss.textContent = css; + } + this.#resizePlayer(); + } + reloadPlayer() { + this.#cleanUpWebGL2Player(), this.#playerType = "default", this.setPlayerType("webgl2", !1); + } + #cleanUpWebGL2Player() { + this.#webGL2Player?.destroy(), this.#webGL2Player = null; + } + destroy() { + this.#cleanUpWebGL2Player(); + } +} +function patchVideoApi() { + const PREF_SKIP_SPLASH_VIDEO = getPref("skip_splash_video"), showFunc = function() { + if (this.style.visibility = "visible", !this.videoWidth) return; + const playerOptions = { + processing: getPref("video_processing"), + sharpness: getPref("video_sharpness"), + saturation: getPref("video_saturation"), + contrast: getPref("video_contrast"), + brightness: getPref("video_brightness") + }; + STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref("video_player_type"), playerOptions), BxEvent.dispatch(window, BxEvent.STREAM_PLAYING, { + $video: this + }); + }, nativePlay = HTMLMediaElement.prototype.play; + HTMLMediaElement.prototype.nativePlay = nativePlay, HTMLMediaElement.prototype.play = function() { + if (this.className && this.className.startsWith("XboxSplashVideo")) { + if (PREF_SKIP_SPLASH_VIDEO) return this.volume = 0, this.style.display = "none", this.dispatchEvent(new Event("ended")), new Promise(() => {}); + return nativePlay.apply(this); + } + const $parent = this.parentElement; + if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 }); + return nativePlay.apply(this); + }; +} +function patchRtcCodecs() { + if (getPref("stream_codec_profile") === "default") return; + if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1; +} +function patchRtcPeerConnection() { + const nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; + RTCPeerConnection.prototype.createDataChannel = function() { + const dataChannel = nativeCreateDataChannel.apply(this, arguments); + return BxEvent.dispatch(window, BxEvent.DATA_CHANNEL_CREATED, { + dataChannel + }), dataChannel; + }; + const maxVideoBitrate = getPref("bitrate_video_max"), codec = getPref("stream_codec_profile"); + if (codec !== "default" || maxVideoBitrate > 0) { + const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; + RTCPeerConnection.prototype.setLocalDescription = function(description) { + if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec); + try { + if (maxVideoBitrate > 0 && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); + } catch (e) { + BxLogger.error("setLocalDescription", e); + } + return nativeSetLocalDescription.apply(this, arguments); + }; + } + const OrgRTCPeerConnection = window.RTCPeerConnection; + window.RTCPeerConnection = function() { + const conn = new OrgRTCPeerConnection; + return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => { + BxLogger.info("connectionstatechange", conn.connectionState); + }), conn; + }; +} +function patchAudioContext() { + const OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain; + window.AudioContext = function(options) { + if (options && options.latencyHint) options.latencyHint = 0; + const ctx = new OrgAudioContext(options); + return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() { + const gainNode = nativeCreateGain.apply(this); + return gainNode.gain.value = getPref("audio_volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; + }, STATES.currentStream.audioContext = ctx, ctx; + }; +} +function patchMeControl() { + const overrideConfigs = { + enableAADTelemetry: !1, + enableTelemetry: !1, + telEvs: "", + oneDSUrl: "" + }, MSA = { + MeControl: { + API: { + setDisplayMode: () => {}, + setMobileState: () => {}, + addEventListener: () => {}, + removeEventListener: () => {} + } + } + }, MeControl = {}, MsaHandler = { + get(target, prop, receiver) { + return target[prop]; + }, + set(obj, prop, value) { + if (prop === "MeControl" && value.Config) value.Config = Object.assign(value.Config, overrideConfigs); + return obj[prop] = value, !0; + } + }, MeControlHandler = { + get(target, prop, receiver) { + return target[prop]; + }, + set(obj, prop, value) { + if (prop === "Config") value = Object.assign(value, overrideConfigs); + return obj[prop] = value, !0; + } + }; + window.MSA = new Proxy(MSA, MsaHandler), window.MeControl = new Proxy(MeControl, MeControlHandler); +} +function disableAdobeAudienceManager() { + window.adobe = Object.freeze({}); +} +function patchCanvasContext() { + const nativeGetContext = HTMLCanvasElement.prototype.getContext; + HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) { + if (contextType.includes("webgl")) { + if (contextAttributes = contextAttributes || {}, !contextAttributes.isBx) { + if (contextAttributes.antialias = !1, contextAttributes.powerPreference === "high-performance") contextAttributes.powerPreference = "low-power"; + } + } + return nativeGetContext.apply(this, [contextType, contextAttributes]); + }; +} +class ProductDetailsPage { + static $btnShortcut = AppInterface && createButton({ + icon: BxIcon.CREATE_SHORTCUT, + label: t("create-shortcut"), + style: 32, + tabIndex: 0, + onClick: (e) => { + AppInterface.createShortcut(window.location.pathname.substring(6)); + } + }); + static $btnWallpaper = AppInterface && createButton({ + icon: BxIcon.DOWNLOAD, + label: t("wallpaper"), + style: 32, + tabIndex: 0, + onClick: async (e) => { + try { + const matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(window.location.pathname); + if (!matches?.groups) return; + const titleSlug = matches.groups.titleSlug.replaceAll("%" + "7C", "-"), productId = matches.groups.productId; + AppInterface.downloadWallpapers(titleSlug, productId); + } catch (e2) {} + } + }); + static injectTimeoutId = null; + static injectButtons() { + if (!AppInterface) return; + ProductDetailsPage.injectTimeoutId && clearTimeout(ProductDetailsPage.injectTimeoutId), ProductDetailsPage.injectTimeoutId = window.setTimeout(() => { + const $container = document.querySelector("div[class*=ActionButtons-module__container]"); + if ($container && $container.parentElement) $container.parentElement.appendChild(CE("div", { + class: "bx-product-details-buttons" + }, BX_FLAGS.DeviceInfo.deviceType === "android" && ProductDetailsPage.$btnShortcut, ProductDetailsPage.$btnWallpaper)); + }, 500); + } +} +class StreamUiHandler { + static $btnStreamSettings; + static $btnStreamStats; + static $btnRefresh; + static $btnHome; + static observer; + static cloneStreamHudButton($btnOrg, label, svgIcon) { + if (!$btnOrg) return null; + const $container = $btnOrg.cloneNode(!0); + let timeout; + if (STATES.browser.capabilities.touch) { + const onTransitionStart = (e) => { + if (e.propertyName !== "opacity") return; + timeout && clearTimeout(timeout), e.target.style.pointerEvents = "none"; + }, onTransitionEnd = (e) => { + if (e.propertyName !== "opacity") return; + const $streamHud = e.target.closest("#StreamHud"); + if (!$streamHud) return; + if ($streamHud.style.left === "0px") { + const $target = e.target; + timeout && clearTimeout(timeout), timeout = window.setTimeout(() => { + $target.style.pointerEvents = "auto"; + }, 100); + } + }; + $container.addEventListener("transitionstart", onTransitionStart), $container.addEventListener("transitionend", onTransitionEnd); + } + const $button = $container.querySelector("button"); + if (!$button) return null; + $button.setAttribute("title", label); + const $orgSvg = $button.querySelector("svg"); + if (!$orgSvg) return null; + const $svg = createSvgIcon(svgIcon); + return $svg.style.fill = "none", $svg.setAttribute("class", $orgSvg.getAttribute("class") || ""), $svg.ariaHidden = "true", $orgSvg.replaceWith($svg), $container; + } + static cloneCloseButton($btnOrg, icon, className, onChange) { + if (!$btnOrg) return null; + const $btn = $btnOrg.cloneNode(!0), $svg = createSvgIcon(icon); + return $svg.setAttribute("class", $btn.firstElementChild.getAttribute("class") || ""), $svg.style.fill = "none", $btn.classList.add(className), $btn.removeChild($btn.firstElementChild), $btn.appendChild($svg), $btn.addEventListener("click", onChange), $btn; + } + static async handleStreamMenu() { + const $btnCloseHud = document.querySelector("button[class*=StreamMenu-module__backButton]"); + if (!$btnCloseHud) return; + let { $btnRefresh, $btnHome } = StreamUiHandler; + if (typeof $btnRefresh === "undefined") $btnRefresh = StreamUiHandler.cloneCloseButton($btnCloseHud, BxIcon.REFRESH, "bx-stream-refresh-button", () => { + confirm(t("confirm-reload-stream")) && window.location.reload(); + }); + if (typeof $btnHome === "undefined") $btnHome = StreamUiHandler.cloneCloseButton($btnCloseHud, BxIcon.HOME, "bx-stream-home-button", () => { + confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)); + }); + if ($btnRefresh && $btnHome) $btnCloseHud.insertAdjacentElement("afterend", $btnRefresh), $btnRefresh.insertAdjacentElement("afterend", $btnHome); + document.querySelector("div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]")?.appendChild(await StreamBadges.getInstance().render()); + } + static handleSystemMenu($streamHud) { + const $gripHandle = $streamHud.querySelector("button[class^=GripHandle]"); + if (!$gripHandle) return; + const $orgButton = $streamHud.querySelector("div[class^=HUDButton]"); + if (!$orgButton) return; + const hideGripHandle = () => { + if (!$gripHandle) return; + $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(), $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(); + }; + let $btnStreamSettings = StreamUiHandler.$btnStreamSettings; + if (typeof $btnStreamSettings === "undefined") $btnStreamSettings = StreamUiHandler.cloneStreamHudButton($orgButton, t("better-xcloud"), BxIcon.BETTER_XCLOUD), $btnStreamSettings?.addEventListener("click", (e) => { + hideGripHandle(), e.preventDefault(), SettingsNavigationDialog.getInstance().show(); + }), StreamUiHandler.$btnStreamSettings = $btnStreamSettings; + const streamStats = StreamStats.getInstance(); + let $btnStreamStats = StreamUiHandler.$btnStreamStats; + if (typeof $btnStreamStats === "undefined") $btnStreamStats = StreamUiHandler.cloneStreamHudButton($orgButton, t("stream-stats"), BxIcon.STREAM_STATS), $btnStreamStats?.addEventListener("click", (e) => { + hideGripHandle(), e.preventDefault(), streamStats.toggle(); + const btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing(); + $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn); + }), StreamUiHandler.$btnStreamStats = $btnStreamStats; + const $btnParent = $orgButton.parentElement; + if ($btnStreamSettings && $btnStreamStats) { + const btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing(); + $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn), $btnParent.insertBefore($btnStreamStats, $btnParent.lastElementChild), $btnParent.insertBefore($btnStreamSettings, $btnStreamStats); + } + const $dotsButton = $btnParent.lastElementChild; + $dotsButton.parentElement.insertBefore($dotsButton, $dotsButton.parentElement.firstElementChild); + } + static reset() { + StreamUiHandler.$btnStreamSettings = void 0, StreamUiHandler.$btnStreamStats = void 0, StreamUiHandler.$btnRefresh = void 0, StreamUiHandler.$btnHome = void 0, StreamUiHandler.observer && StreamUiHandler.observer.disconnect(), StreamUiHandler.observer = void 0; + } + static observe() { + StreamUiHandler.reset(); + const $screen = document.querySelector("#PageContent section[class*=PureScreens]"); + if (!$screen) return; + const observer = new MutationObserver((mutationList) => { + mutationList.forEach((item2) => { + if (item2.type !== "childList") return; + item2.addedNodes.forEach(async ($node) => { + if (!$node || $node.nodeType !== Node.ELEMENT_NODE) return; + let $elm = $node; + if (!($elm instanceof HTMLElement)) return; + const className = $elm.className || ""; + if (className.includes("PureErrorPage")) { + BxEvent.dispatch(window, BxEvent.STREAM_ERROR_PAGE); + return; + } + if (className.startsWith("StreamMenu-module__container")) { + StreamUiHandler.handleStreamMenu(); + return; + } + if (className.startsWith("Overlay-module_") || className.startsWith("InProgressScreen")) $elm = $elm.querySelector("#StreamHud"); + if (!$elm || ($elm.id || "") !== "StreamHud") return; + StreamUiHandler.handleSystemMenu($elm); + }); + }); + }); + observer.observe($screen, { subtree: !0, childList: !0 }), StreamUiHandler.observer = observer; + } +} +class XboxApi { + static CACHED_TITLES = {}; + static async getProductTitle(xboxTitleId) { + if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId]; + try { + const url = `https://displaycatalog.mp.microsoft.com/v7.0/products/lookup?market=US&languages=en&value=${xboxTitleId}&alternateId=XboxTitleId&fieldsTemplate=browse`, productTitle = (await (await NATIVE_FETCH(url)).json()).Products[0].LocalizedProperties[0].ProductTitle; + return XboxApi.CACHED_TITLES[xboxTitleId] = productTitle, productTitle; + } catch (e) {} + return null; + } +} +function unload() { + if (!STATES.isPlaying) return; + STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().onStoppedPlaying(); +} +function observeRootDialog($root) { + let beingShown = !1; + new MutationObserver((mutationList) => { + for (let mutation of mutationList) { + if (mutation.type !== "childList") continue; + if (BX_FLAGS.Debug && BxLogger.warning("RootDialog", "added", mutation.addedNodes), mutation.addedNodes.length === 1) { + const $addedElm = mutation.addedNodes[0]; + if ($addedElm instanceof HTMLElement && $addedElm.className) { + if ($root.querySelector("div[class*=GuideDialog]")) GuideMenu.observe($addedElm); + } + } + const shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0); + if (shown !== beingShown) beingShown = shown, BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED); + } + }).observe($root, { subtree: !0, childList: !0 }); +} +function waitForRootDialog() { + const observer = new MutationObserver((mutationList) => { + for (let mutation of mutationList) { + if (mutation.type !== "childList") continue; + const $target = mutation.target; + if ($target.id && $target.id === "gamepass-dialog-root") { + observer.disconnect(), observeRootDialog($target); + break; + } + } + }); + observer.observe(document.documentElement, { subtree: !0, childList: !0 }); +} +function main() { + if (patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), getPref("audio_enable_volume_control") && patchAudioContext(), getPref("block_tracking")) patchMeControl(), disableAdobeAudienceManager(); + if (waitForRootDialog(), addCss(), Toast.setup(), GuideMenu.addEventListeners(), StreamBadges.setupEvents(), StreamStats.setupEvents(), getPref("controller_show_connection_status")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); +} +if (window.location.pathname.includes("/auth/msa")) { + const nativePushState = window.history.pushState; + throw window.history.pushState = function(...args) { + const url = args[2]; + if (url && (url.startsWith("/play") || url.substring(6).startsWith("/play"))) { + console.log("Redirecting to xbox.com/play"), window.stop(), window.location.href = "https://www.xbox.com" + url; + return; + } + return nativePushState.apply(this, arguments); + }, new Error("[Better xCloud] Refreshing the page after logging in"); +} +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) getPref("xhome_enabled") && RemotePlayManager.getInstance().initialize(); + else window.setTimeout(HeaderSection.watchHeader, 2000); + if (getPref("ui_hide_sections").includes("friends")) { + const $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]"); + $parent && ($parent.style.display = "none"); + } + preloadFonts(); +}); +window.BX_EXPOSED = BxExposed; +window.addEventListener(BxEvent.POPSTATE, onHistoryChanged); +window.addEventListener("popstate", onHistoryChanged); +window.history.pushState = patchHistoryMethod("pushState"); +window.history.replaceState = patchHistoryMethod("replaceState"); +window.addEventListener(BxEvent.XCLOUD_SERVERS_UNAVAILABLE, (e) => { + STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), SettingsNavigationDialog.getInstance().show(); +}); +window.addEventListener(BxEvent.XCLOUD_SERVERS_READY, (e) => { + STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000); +}); +window.addEventListener(BxEvent.STREAM_LOADING, (e) => { + if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title); + else STATES.currentStream.titleSlug = "remote-play"; +}); +getPref("ui_loading_screen_game_art") && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup); +window.addEventListener(BxEvent.STREAM_STARTING, (e) => { + if (LoadingScreen.hide(), !getPref("mkb_enabled") && getPref("mkb_hide_idle_cursor")) MouseCursorHider.start(), MouseCursorHider.hide(); +}); +window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { + STATES.isPlaying = !0, StreamUiHandler.observe(), updateVideoPlayer(); +}); +window.addEventListener(BxEvent.STREAM_ERROR_PAGE, (e) => { + BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); +}); +window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, (e) => { + if (e.component === "product-details") ProductDetailsPage.injectButtons(); +}); +window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { + const dataChannel = e.dataChannel; + if (!dataChannel || dataChannel.label !== "message") return; + dataChannel.addEventListener("message", async (msg) => { + if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return; + if (msg.data.includes("/titleinfo")) { + const json = JSON.parse(JSON.parse(msg.data).content), xboxTitleId = parseInt(json.titleid, 16); + if (STATES.currentStream.xboxTitleId = xboxTitleId, STATES.remotePlay.isPlaying) { + if (STATES.currentStream.titleSlug = "remote-play", json.focused) { + const productTitle = await XboxApi.getProductTitle(xboxTitleId); + if (productTitle) STATES.currentStream.titleSlug = productTitleToSlug(productTitle); + } + } + } + }); +}); +window.addEventListener(BxEvent.STREAM_STOPPED, unload); +window.addEventListener("pagehide", (e) => { + BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); +}); +main(); diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index 083bdc6..4d63f19 100644 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Better xCloud // @namespace https://github.com/redphx -// @version 5.7.7 +// @version 5.7.8-beta // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -120,7 +120,7 @@ function deepClone(obj) { if (!obj) return {}; return JSON.parse(JSON.stringify(obj)); } -var SCRIPT_VERSION = "5.7.7", AppInterface = window.AppInterface; +var SCRIPT_VERSION = "5.7.8-beta", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; UserAgent.init(); var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = { supportedRegion: !0, @@ -995,6 +995,7 @@ class BaseSettingsStore { let settingId; for (settingId in definitions) { const setting = definitions[settingId]; + if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants]; setting.ready && setting.ready.call(this, setting); } this.definitions = definitions, this._settings = null; @@ -1016,7 +1017,9 @@ class BaseSettingsStore { debugger; return; } - if (checkUnsupported && this.definitions[key].unsupported) return this.definitions[key].default; + const definition = this.definitions[key]; + if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return definition.default; + if (checkUnsupported && definition.unsupported) return definition.default; if (!(key in this.settings)) this.settings[key] = this.validateValue(key, null); return this.settings[key]; } @@ -1183,6 +1186,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { default: !1 }, screenshot_apply_filters: { + requiredVariants: "full", label: t("screenshot-apply-filters"), default: !1 }, @@ -1195,12 +1199,14 @@ class GlobalSettingsStorage extends BaseSettingsStore { default: !1 }, stream_combine_sources: { + requiredVariants: "full", label: t("combine-audio-video-streams"), default: !1, experimental: !0, note: t("combine-audio-video-streams-summary") }, stream_touch_controller: { + requiredVariants: "full", label: t("tc-availability"), default: "all", options: { @@ -1214,11 +1220,13 @@ class GlobalSettingsStorage extends BaseSettingsStore { } }, stream_touch_controller_auto_off: { + requiredVariants: "full", label: t("tc-auto-off"), default: !1, unsupported: !STATES.userAgent.capabilities.touch }, stream_touch_controller_default_opacity: { + requiredVariants: "full", type: "number-stepper", label: t("tc-default-opacity"), default: 100, @@ -1233,6 +1241,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { unsupported: !STATES.userAgent.capabilities.touch }, stream_touch_controller_style_standard: { + requiredVariants: "full", label: t("tc-standard-layout-style"), default: "default", options: { @@ -1243,6 +1252,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { unsupported: !STATES.userAgent.capabilities.touch }, stream_touch_controller_style_custom: { + requiredVariants: "full", label: t("tc-custom-layout-style"), default: "default", options: { @@ -1256,14 +1266,17 @@ class GlobalSettingsStorage extends BaseSettingsStore { default: !1 }, mkb_hide_idle_cursor: { + requiredVariants: "full", label: t("hide-idle-cursor"), default: !1 }, stream_disable_feedback_dialog: { + requiredVariants: "full", label: t("disable-post-stream-feedback-dialog"), default: !1 }, bitrate_video_max: { + requiredVariants: "full", type: "number-stepper", label: t("bitrate-video-maximum"), note: "⚠️ " + t("unexpected-behavior"), @@ -1283,6 +1296,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { } }, game_bar_position: { + requiredVariants: "full", label: t("position"), default: "bottom-left", options: { @@ -1292,6 +1306,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { } }, local_co_op_enabled: { + requiredVariants: "full", label: t("enable-local-co-op-support"), default: !1, note: CE("a", { @@ -1304,13 +1319,16 @@ class GlobalSettingsStorage extends BaseSettingsStore { default: !0 }, controller_enable_shortcuts: { + requiredVariants: "full", default: !1 }, controller_enable_vibration: { + requiredVariants: "full", label: t("controller-vibration"), default: !0 }, controller_device_vibration: { + requiredVariants: "full", label: t("device-vibration"), default: "off", options: { @@ -1320,6 +1338,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { } }, controller_vibration_intensity: { + requiredVariants: "full", label: t("vibration-intensity"), type: "number-stepper", default: 100, @@ -1332,6 +1351,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { } }, mkb_enabled: { + requiredVariants: "full", label: t("enable-mkb"), default: !1, unsupported: (() => { @@ -1349,6 +1369,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { } }, native_mkb_enabled: { + requiredVariants: "full", label: t("native-mkb"), default: "default", options: { @@ -1363,6 +1384,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { } }, native_mkb_scroll_x_sensitivity: { + requiredVariants: "full", label: t("horizontal-scroll-sensitivity"), type: "number-stepper", default: 0, @@ -1378,6 +1400,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { } }, native_mkb_scroll_y_sensitivity: { + requiredVariants: "full", label: t("vertical-scroll-sensitivity"), type: "number-stepper", default: 0, @@ -1393,9 +1416,11 @@ class GlobalSettingsStorage extends BaseSettingsStore { } }, mkb_default_preset_id: { + requiredVariants: "full", default: 0 }, mkb_absolute_mouse: { + requiredVariants: "full", default: !1 }, reduce_animations: { @@ -1403,6 +1428,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { default: !1 }, ui_loading_screen_game_art: { + requiredVariants: "full", label: t("show-game-art"), default: !0 }, @@ -1424,6 +1450,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { default: BX_FLAGS.DeviceInfo.deviceType !== "unknown" }, ui_layout: { + requiredVariants: "full", label: t("layout"), default: "default", options: { @@ -1437,10 +1464,12 @@ class GlobalSettingsStorage extends BaseSettingsStore { default: !1 }, ui_home_context_menu_disabled: { + requiredVariants: "full", label: t("disable-home-context-menu"), default: STATES.browser.capabilities.touch }, ui_hide_sections: { + requiredVariants: "full", label: t("hide-sections"), default: [], multipleOptions: { @@ -1456,6 +1485,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { } }, ui_game_card_show_wait_time: { + requiredVariants: "full", label: t("show-wait-time-in-game-card"), default: !1 }, @@ -1585,6 +1615,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { default: !1 }, audio_enable_volume_control: { + requiredVariants: "full", label: t("enable-volume-control"), default: !1 }, @@ -1662,10 +1693,12 @@ class GlobalSettingsStorage extends BaseSettingsStore { default: !1 }, xhome_enabled: { + requiredVariants: "full", label: t("enable-remote-play-feature"), default: !1 }, xhome_resolution: { + requiredVariants: "full", default: "1080p", options: { "1080p": "1080p", @@ -1673,6 +1706,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { } }, game_fortnite_force_console: { + requiredVariants: "full", label: "🎮 " + t("fortnite-force-console-version"), default: !1, note: t("fortnite-allow-stw-mode") @@ -1699,7 +1733,7 @@ class Screenshot { if ($canvas) $canvas.width = width, $canvas.height = height; } static updateCanvasFilters(filters) { - Screenshot.#canvasContext.filter = filters; + Screenshot.#canvasContext && (Screenshot.#canvasContext.filter = filters); } static #onAnimationEnd(e) { e.target.classList.remove("bx-taking-screenshot"); @@ -4065,12 +4099,14 @@ class SettingsNavigationDialog extends NavigationDialog { "stream_combine_sources" ] }, { + requiredVariants: "full", group: "co-op", label: t("local-co-op"), items: [ "local_co_op_enabled" ] }, { + requiredVariants: "full", group: "mkb", label: t("mouse-and-keyboard"), items: [ @@ -4079,6 +4115,7 @@ class SettingsNavigationDialog extends NavigationDialog { "mkb_hide_idle_cursor" ] }, { + requiredVariants: "full", group: "touch-control", label: t("touch-controller"), note: !STATES.userAgent.capabilities.touch ? "⚠️ " + t("device-unsupported-touch") : null, @@ -4107,6 +4144,7 @@ class SettingsNavigationDialog extends NavigationDialog { "ui_hide_sections" ] }, { + requiredVariants: "full", group: "game-bar", label: t("game-bar"), items: [ @@ -4193,6 +4231,7 @@ class SettingsNavigationDialog extends NavigationDialog { ] }]; TAB_DISPLAY_ITEMS = [{ + requiredVariants: "full", group: "audio", label: t("audio"), helpUrl: "https://better-xcloud.github.io/ingame-features/#audio", @@ -4309,6 +4348,7 @@ class SettingsNavigationDialog extends NavigationDialog { content: MkbRemapper.INSTANCE.render() }]; TAB_NATIVE_MKB_ITEMS = [{ + requiredVariants: "full", group: "native-mkb", label: t("native-mkb"), items: [{ @@ -4324,6 +4364,7 @@ class SettingsNavigationDialog extends NavigationDialog { }] }]; TAB_SHORTCUTS_ITEMS = [{ + requiredVariants: "full", group: "controller-shortcuts", label: t("controller-shortcuts"), content: ControllerShortcut.renderSettings() @@ -4383,22 +4424,26 @@ class SettingsNavigationDialog extends NavigationDialog { { icon: BxIcon.CONTROLLER, group: "controller", - items: this.TAB_CONTROLLER_ITEMS + items: this.TAB_CONTROLLER_ITEMS, + requiredVariants: "full" }, getPref("mkb_enabled") && { icon: BxIcon.VIRTUAL_CONTROLLER, group: "mkb", - items: this.TAB_VIRTUAL_CONTROLLER_ITEMS + items: this.TAB_VIRTUAL_CONTROLLER_ITEMS, + requiredVariants: "full" }, AppInterface && getPref("native_mkb_enabled") === "on" && { icon: BxIcon.NATIVE_MKB, group: "native-mkb", - items: this.TAB_NATIVE_MKB_ITEMS + items: this.TAB_NATIVE_MKB_ITEMS, + requiredVariants: "full" }, { icon: BxIcon.COMMAND, group: "shortcuts", - items: this.TAB_SHORTCUTS_ITEMS + items: this.TAB_SHORTCUTS_ITEMS, + requiredVariants: "full" }, { icon: BxIcon.STREAM_STATS, @@ -4457,6 +4502,10 @@ class SettingsNavigationDialog extends NavigationDialog { if (!(prefKey in this.suggestedSettings.default)) this.suggestedSettings.default[prefKey] = getPrefDefinition(prefKey).default; } } + isSupportedVariant(requiredVariants) { + if (typeof requiredVariants === "undefined") return !0; + return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT); + } async renderSuggestions(e) { const $btnSuggest = e.target.closest("div"); $btnSuggest.toggleAttribute("bx-open"); @@ -4645,6 +4694,7 @@ class SettingsNavigationDialog extends NavigationDialog { } let prefDefinition = null; if (pref) prefDefinition = getPrefDefinition(pref); + if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return; let label = prefDefinition?.label || setting.label, note = prefDefinition?.note || setting.note; const experimental = prefDefinition?.experimental || setting.experimental; if (settingTabContent.label && setting.pref) { @@ -4718,6 +4768,7 @@ class SettingsNavigationDialog extends NavigationDialog { }); for (let settingTab of this.SETTINGS_UI) { if (!settingTab) continue; + if (!this.isSupportedVariant(settingTab.requiredVariants)) continue; if (settingTab.group !== "global" && !this.renderFullSettings) continue; const $svg = this.renderTab(settingTab); $tabs.appendChild($svg); @@ -4727,13 +4778,17 @@ class SettingsNavigationDialog extends NavigationDialog { }); for (let settingTabContent of settingTab.items) { if (settingTabContent === !1) continue; + if (!this.isSupportedVariant(settingTabContent.requiredVariants)) continue; if (!this.renderFullSettings && settingTab.group === "global" && settingTabContent.group !== "general" && settingTabContent.group !== "footer") continue; let label = settingTabContent.label; - if (label === t("better-xcloud")) label += " " + SCRIPT_VERSION, label = createButton({ + if (label === t("better-xcloud")) { + if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)"; + label = createButton({ label, url: "https://github.com/redphx/better-xcloud/releases", style: 1024 | 8 | 32 }); + } if (label) { const $title = CE("h2", { _nearby: { @@ -5908,6 +5963,211 @@ class LoadingScreen { LoadingScreen.#$bgStyle && (LoadingScreen.#$bgStyle.textContent = ""), LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add("bx-gone"), LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval), LoadingScreen.#waitTimeInterval = null; } } +class TrueAchievements { + static $link = createButton({ + label: t("true-achievements"), + url: "#", + icon: BxIcon.TRUE_ACHIEVEMENTS, + style: 32 | 4 | 64 | 2048, + onClick: TrueAchievements.onClick + }); + static $button = createButton({ + label: t("true-achievements"), + title: t("true-achievements"), + icon: BxIcon.TRUE_ACHIEVEMENTS, + style: 32, + onClick: TrueAchievements.onClick + }); + static onClick(e) { + e.preventDefault(); + const dataset = TrueAchievements.$link.dataset; + TrueAchievements.open(!0, dataset.xboxTitleId, dataset.id), window.BX_EXPOSED.dialogRoutes?.closeAll(); + } + static $hiddenLink = CE("a", { + target: "_blank" + }); + static updateIds(xboxTitleId, id2) { + const { $link, $button } = TrueAchievements; + if (clearDataSet($link), clearDataSet($button), xboxTitleId) $link.dataset.xboxTitleId = xboxTitleId, $button.dataset.xboxTitleId = xboxTitleId; + if (id2) $link.dataset.id = id2, $button.dataset.id = id2; + } + static injectAchievementsProgress($elm) { + if (SCRIPT_VARIANT !== "full") return; + const $parent = $elm.parentElement, $div = CE("div", { + class: "bx-guide-home-achievements-progress" + }, $elm); + let xboxTitleId; + try { + const $container = $parent.closest("div[class*=AchievementsPreview-module__container]"); + if ($container) xboxTitleId = getReactProps($container).children.props.data.data.xboxTitleId; + } catch (e) {} + if (!xboxTitleId) xboxTitleId = TrueAchievements.getStreamXboxTitleId(); + if (typeof xboxTitleId !== "undefined") xboxTitleId = xboxTitleId.toString(); + if (TrueAchievements.updateIds(xboxTitleId), document.documentElement.dataset.xdsPlatform === "tv") $div.appendChild(TrueAchievements.$link); + else $div.appendChild(TrueAchievements.$button); + $parent.appendChild($div); + } + static injectAchievementDetailPage($parent) { + if (SCRIPT_VARIANT !== "full") return; + const props = getReactProps($parent); + if (!props) return; + try { + const achievementList = props.children.props.data.data, $header = $parent.querySelector("div[class*=AchievementDetailHeader]"), achievementName = getReactProps($header).children[0].props.achievementName; + let id2, xboxTitleId; + for (let achiev of achievementList) + if (achiev.name === achievementName) { + id2 = achiev.id, xboxTitleId = achiev.title.id; + break; + } + if (id2) TrueAchievements.updateIds(xboxTitleId, id2), $parent.appendChild(TrueAchievements.$link); + } catch (e) {} + } + static getStreamXboxTitleId() { + return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId; + } + static open(override, xboxTitleId, id2) { + if (!xboxTitleId || xboxTitleId === "undefined") xboxTitleId = TrueAchievements.getStreamXboxTitleId(); + if (AppInterface && AppInterface.openTrueAchievementsLink) { + AppInterface.openTrueAchievementsLink(override, xboxTitleId?.toString(), id2?.toString()); + return; + } + let url = "https://www.trueachievements.com"; + if (xboxTitleId) { + if (url += `/deeplink/${xboxTitleId}`, id2) url += `/${id2}`; + } + TrueAchievements.$hiddenLink.href = url, TrueAchievements.$hiddenLink.click(); + } +} +class GuideMenu { + static #BUTTONS = { + scriptSettings: createButton({ + label: t("better-xcloud"), + style: 64 | 32 | 1, + onClick: (e) => { + window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, (e2) => { + setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50); + }, { once: !0 }), GuideMenu.#closeGuideMenu(); + } + }), + closeApp: AppInterface && createButton({ + icon: BxIcon.POWER, + label: t("close-app"), + title: t("close-app"), + style: 64 | 32 | 2, + onClick: (e) => { + AppInterface.closeApp(); + }, + attributes: { + "data-state": "normal" + } + }), + reloadPage: createButton({ + icon: BxIcon.REFRESH, + label: t("reload-page"), + title: t("reload-page"), + style: 64 | 32, + onClick: (e) => { + if (STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload(); + else window.location.reload(); + GuideMenu.#closeGuideMenu(); + } + }), + backToHome: createButton({ + icon: BxIcon.HOME, + label: t("back-to-home"), + title: t("back-to-home"), + style: 64 | 32, + onClick: (e) => { + confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)), GuideMenu.#closeGuideMenu(); + }, + attributes: { + "data-state": "playing" + } + }) + }; + static #$renderedButtons; + static #closeGuideMenu() { + if (window.BX_EXPOSED.dialogRoutes) { + window.BX_EXPOSED.dialogRoutes.closeAll(); + return; + } + const $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]"); + $btnClose && $btnClose.click(); + } + static #renderButtons() { + if (GuideMenu.#$renderedButtons) return GuideMenu.#$renderedButtons; + const $div = CE("div", { + class: "bx-guide-home-buttons" + }), buttons = [ + GuideMenu.#BUTTONS.scriptSettings, + [ + GuideMenu.#BUTTONS.backToHome, + GuideMenu.#BUTTONS.reloadPage, + GuideMenu.#BUTTONS.closeApp + ] + ]; + for (let $button of buttons) { + if (!$button) continue; + if ($button instanceof HTMLElement) $div.appendChild($button); + else if (Array.isArray($button)) { + const $wrapper = CE("div", {}); + for (let $child of $button) + $child && $wrapper.appendChild($child); + $div.appendChild($wrapper); + } + } + return GuideMenu.#$renderedButtons = $div, $div; + } + static #injectHome($root, isPlaying = !1) { + { + const $achievementsProgress = $root.querySelector("button[class*=AchievementsButton-module__progressBarContainer]"); + if ($achievementsProgress) TrueAchievements.injectAchievementsProgress($achievementsProgress); + } + let $target = null; + if (isPlaying) { + $target = $root.querySelector("a[class*=QuitGameButton]"); + const $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]"); + $btnXcloudHome && ($btnXcloudHome.style.display = "none"); + } else { + const $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]"); + if ($dividers) $target = $dividers[$dividers.length - 1]; + } + if (!$target) return !1; + const $buttons = GuideMenu.#renderButtons(); + $buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons); + } + static async#onShown(e) { + if (e.where === "home") { + const $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]"); + $root && GuideMenu.#injectHome($root, STATES.isPlaying); + } + } + static addEventListeners() { + window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown); + } + static observe($addedElm) { + const className = $addedElm.className; + if (className.includes("AchievementsButton-module__progressBarContainer")) { + TrueAchievements.injectAchievementsProgress($addedElm); + return; + } + if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return; + { + const $achievDetailPage = $addedElm.querySelector("div[class*=AchievementDetailPage]"); + if ($achievDetailPage) { + TrueAchievements.injectAchievementDetailPage($achievDetailPage); + return; + } + } + const $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true"); + if ($selectedTab) { + let $elm = $selectedTab, index; + for (index = 0;$elm = $elm?.previousElementSibling; index++) + ; + if (index === 0) BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: "home" }); + } + } +} var StreamBadgeIcon = { playtime: BxIcon.PLAYTIME, video: BxIcon.DISPLAY, @@ -6929,79 +7189,6 @@ class MicrophoneAction extends BaseGameBarAction { this.visible = !1, this.$content.classList.add("bx-gone"), this.$content.setAttribute("data-enabled", "false"); } } -class TrueAchievements { - static $link = createButton({ - label: t("true-achievements"), - url: "#", - icon: BxIcon.TRUE_ACHIEVEMENTS, - style: 32 | 4 | 64 | 2048, - onClick: TrueAchievements.onClick - }); - static $button = createButton({ - label: t("true-achievements"), - title: t("true-achievements"), - icon: BxIcon.TRUE_ACHIEVEMENTS, - style: 32, - onClick: TrueAchievements.onClick - }); - static onClick(e) { - e.preventDefault(); - const dataset = TrueAchievements.$link.dataset; - TrueAchievements.open(!0, dataset.xboxTitleId, dataset.id), window.BX_EXPOSED.dialogRoutes.closeAll(); - } - static $hiddenLink = CE("a", { - target: "_blank" - }); - static updateIds(xboxTitleId, id2) { - const { $link, $button } = TrueAchievements; - if (clearDataSet($link), clearDataSet($button), xboxTitleId) $link.dataset.xboxTitleId = xboxTitleId, $button.dataset.xboxTitleId = xboxTitleId; - if (id2) $link.dataset.id = id2, $button.dataset.id = id2; - } - static injectAchievementsProgress($elm) { - const $parent = $elm.parentElement, $div = CE("div", { - class: "bx-guide-home-achievements-progress" - }, $elm); - let xboxTitleId; - try { - const $container = $parent.closest("div[class*=AchievementsPreview-module__container]"); - if ($container) xboxTitleId = getReactProps($container).children.props.data.data.xboxTitleId; - } catch (e) {} - if (!xboxTitleId) xboxTitleId = TrueAchievements.getStreamXboxTitleId(); - if (typeof xboxTitleId !== "undefined") xboxTitleId = xboxTitleId.toString(); - if (TrueAchievements.updateIds(xboxTitleId), document.documentElement.dataset.xdsPlatform === "tv") $div.appendChild(TrueAchievements.$link); - else $div.appendChild(TrueAchievements.$button); - $parent.appendChild($div); - } - static injectAchievementDetailPage($parent) { - const props = getReactProps($parent); - if (!props) return; - try { - const achievementList = props.children.props.data.data, $header = $parent.querySelector("div[class*=AchievementDetailHeader]"), achievementName = getReactProps($header).children[0].props.achievementName; - let id2, xboxTitleId; - for (let achiev of achievementList) - if (achiev.name === achievementName) { - id2 = achiev.id, xboxTitleId = achiev.title.id; - break; - } - if (id2) TrueAchievements.updateIds(xboxTitleId, id2), $parent.appendChild(TrueAchievements.$link); - } catch (e) {} - } - static getStreamXboxTitleId() { - return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId; - } - static open(override, xboxTitleId, id2) { - if (!xboxTitleId || xboxTitleId === "undefined") xboxTitleId = TrueAchievements.getStreamXboxTitleId(); - if (AppInterface && AppInterface.openTrueAchievementsLink) { - AppInterface.openTrueAchievementsLink(override, xboxTitleId?.toString(), id2?.toString()); - return; - } - let url = "https://www.trueachievements.com"; - if (xboxTitleId) { - if (url += `/deeplink/${xboxTitleId}`, id2) url += `/${id2}`; - } - TrueAchievements.$hiddenLink.href = url, TrueAchievements.$hiddenLink.click(); - } -} class TrueAchievementsAction extends BaseGameBarAction { $content; constructor() { @@ -7113,124 +7300,6 @@ class GameBar { action.reset(); } } -class GuideMenu { - static #BUTTONS = { - scriptSettings: createButton({ - label: t("better-xcloud"), - style: 64 | 32 | 1, - onClick: (e) => { - window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, (e2) => { - setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50); - }, { once: !0 }), window.BX_EXPOSED.dialogRoutes.closeAll(); - } - }), - closeApp: AppInterface && createButton({ - icon: BxIcon.POWER, - label: t("close-app"), - title: t("close-app"), - style: 64 | 32 | 2, - onClick: (e) => { - AppInterface.closeApp(); - }, - attributes: { - "data-state": "normal" - } - }), - reloadPage: createButton({ - icon: BxIcon.REFRESH, - label: t("reload-page"), - title: t("reload-page"), - style: 64 | 32, - onClick: (e) => { - if (STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload(); - else window.location.reload(); - window.BX_EXPOSED.dialogRoutes.closeAll(); - } - }), - backToHome: createButton({ - icon: BxIcon.HOME, - label: t("back-to-home"), - title: t("back-to-home"), - style: 64 | 32, - onClick: (e) => { - confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)), window.BX_EXPOSED.dialogRoutes.closeAll(); - }, - attributes: { - "data-state": "playing" - } - }) - }; - static #$renderedButtons; - static #renderButtons() { - if (GuideMenu.#$renderedButtons) return GuideMenu.#$renderedButtons; - const $div = CE("div", { - class: "bx-guide-home-buttons" - }), buttons = [ - GuideMenu.#BUTTONS.scriptSettings, - [ - GuideMenu.#BUTTONS.backToHome, - GuideMenu.#BUTTONS.reloadPage, - GuideMenu.#BUTTONS.closeApp - ] - ]; - for (let $button of buttons) { - if (!$button) continue; - if ($button instanceof HTMLElement) $div.appendChild($button); - else if (Array.isArray($button)) { - const $wrapper = CE("div", {}); - for (let $child of $button) - $child && $wrapper.appendChild($child); - $div.appendChild($wrapper); - } - } - return GuideMenu.#$renderedButtons = $div, $div; - } - static #injectHome($root, isPlaying = !1) { - const $achievementsProgress = $root.querySelector("button[class*=AchievementsButton-module__progressBarContainer]"); - if ($achievementsProgress) TrueAchievements.injectAchievementsProgress($achievementsProgress); - let $target = null; - if (isPlaying) { - $target = $root.querySelector("a[class*=QuitGameButton]"); - const $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]"); - $btnXcloudHome && ($btnXcloudHome.style.display = "none"); - } else { - const $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]"); - if ($dividers) $target = $dividers[$dividers.length - 1]; - } - if (!$target) return !1; - const $buttons = GuideMenu.#renderButtons(); - $buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons); - } - static async#onShown(e) { - if (e.where === "home") { - const $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]"); - $root && GuideMenu.#injectHome($root, STATES.isPlaying); - } - } - static addEventListeners() { - window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown); - } - static observe($addedElm) { - const className = $addedElm.className; - if (className.includes("AchievementsButton-module__progressBarContainer")) { - TrueAchievements.injectAchievementsProgress($addedElm); - return; - } - if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return; - const $achievDetailPage = $addedElm.querySelector("div[class*=AchievementDetailPage]"); - if ($achievDetailPage) { - TrueAchievements.injectAchievementDetailPage($achievDetailPage); - return; - } - const $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true"); - if ($selectedTab) { - let $elm = $selectedTab, index; - for (index = 0;$elm = $elm?.previousElementSibling; index++) - ; - if (index === 0) BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: "home" }); - } - } -} class XcloudApi { static instance; static getInstance() { @@ -7532,12 +7601,11 @@ function waitForRootDialog() { observer.observe(document.documentElement, { subtree: !0, childList: !0 }); } function main() { - if (waitForRootDialog(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getPref("audio_enable_volume_control") && patchAudioContext(), getPref("block_tracking")) patchMeControl(), disableAdobeAudienceManager(); - if (STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), overridePreloadState(), VibrationManager.initialSetup(), BX_FLAGS.CheckForUpdate && checkForUpdate(), addCss(), Toast.setup(), getPref("game_bar_position") !== "off" && GameBar.getInstance(), Screenshot.setup(), GuideMenu.addEventListeners(), StreamBadges.setupEvents(), StreamStats.setupEvents(), EmulatedMkbHandler.setupEvents(), Patcher.init(), disablePwa(), getPref("controller_show_connection_status")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); - if (getPref("xhome_enabled")) RemotePlayManager.detect(); + if (patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getPref("audio_enable_volume_control") && patchAudioContext(), getPref("block_tracking")) patchMeControl(), disableAdobeAudienceManager(); + if (waitForRootDialog(), addCss(), Toast.setup(), GuideMenu.addEventListeners(), StreamBadges.setupEvents(), StreamStats.setupEvents(), getPref("game_bar_position") !== "off" && GameBar.getInstance(), Screenshot.setup(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), overridePreloadState(), VibrationManager.initialSetup(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getPref("xhome_enabled")) RemotePlayManager.detect(); if (getPref("stream_touch_controller") === "all") TouchController.setup(); if (getPref("mkb_enabled") && AppInterface) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString()); - getPref("ui_game_card_show_wait_time") && GameTile.setup(); + if (getPref("ui_game_card_show_wait_time") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getPref("controller_show_connection_status")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); } if (window.location.pathname.includes("/auth/msa")) { const nativePushState = window.history.pushState; @@ -7606,8 +7674,11 @@ window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { const gameBar = GameBar.getInstance(); gameBar.reset(), gameBar.enable(), gameBar.showBar(); } - const $video = e.$video; - Screenshot.updateCanvasSize($video.videoWidth, $video.videoHeight), updateVideoPlayer(); + { + const $video = e.$video; + Screenshot.updateCanvasSize($video.videoWidth, $video.videoHeight); + } + updateVideoPlayer(); }); window.addEventListener(BxEvent.STREAM_ERROR_PAGE, (e) => { BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);