From 361e494e111bed3f78506f2ba88f266765569fbd Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:11:49 +0700 Subject: [PATCH] Update better-xcloud.user.js --- dist/better-xcloud.user.js | 7536 ++++++++++++++++++++---------------- 1 file changed, 4206 insertions(+), 3330 deletions(-) diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index 9b46375..a7526b0 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.9.8-beta +// @version 6.0.0-beta // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -46,7 +46,7 @@ if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) { if (match) CHROMIUM_VERSION = match[1]; } class UserAgent { - static STORAGE_KEY = "better_xcloud_user_agent"; + static STORAGE_KEY = "BetterXcloud.UserAgent"; static #config; static #isMobile = null; static #isSafari = null; @@ -107,9 +107,9 @@ class UserAgent { }); } } -var SCRIPT_VERSION = "5.9.8-beta", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; +var SCRIPT_VERSION = "6.0.0-beta", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; UserAgent.init(); -var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = { +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: {}, @@ -119,14 +119,17 @@ var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.inclu browser: { capabilities: { touch: browserHasTouchSupport, - batteryApi: "getBattery" in window.navigator + batteryApi: "getBattery" in window.navigator, + deviceVibration: !!window.navigator.vibrate, + mkb: AppInterface || !UserAgent.getDefault().toLowerCase().match(/(android|iphone|ipad)/), + emulatedNativeMkb: !!AppInterface } }, userAgent: { isTv, capabilities: { touch: userAgentHasTouchSupport, - mkb: supportMkb + mkb: AppInterface || !userAgent.match(/(android|iphone|ipad)/) } }, currentStream: {}, @@ -134,13 +137,13 @@ var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.inclu pointerServerPort: 9269 }, STORAGE = {}; function deepClone(obj) { - if ("structuredClone" in window) return structuredClone(obj); if (!obj) return {}; + if ("structuredClone" in window) return structuredClone(obj); return JSON.parse(JSON.stringify(obj)); } 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"; + 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.MKB_UPDATED = "bx-mkb-updated", BxEvent.KEYBOARD_SHORTCUTS_UPDATED = "bx-keyboard-shortcuts-updated", 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.DEVICE_VIBRATION_CHANGED = "bx-device-vibration-changed", 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.VIDEO_VISIBILITY_CHANGED = "bx-video-visibility-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) { @@ -155,109 +158,69 @@ var BxEvent; 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; -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" +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", "⇁"] }; -function createElement(elmName, props = {}, ..._) { - let $elm, 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]); +class GhPagesUtils { + static fetchLatestCommit() { + NATIVE_FETCH("https://api.github.com/repos/redphx/better-xcloud/branches/gh-pages", { + method: "GET", + headers: { + Accept: "application/vnd.github.v3+json" + } + }).then((response) => response.json()).then((data) => { + let latestCommitHash = data.commit.sha; + window.localStorage.setItem("BetterXcloud.GhPages.CommitHash", latestCommitHash); + }).catch((error) => { + BxLogger.error("GhPagesUtils", "Error fetching the latest commit:", error); + }); } - for (let i = 2, size = arguments.length;i < size; i++) { - let arg = arguments[i]; - if (arg instanceof Node) $elm.appendChild(arg); - else if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.appendChild(document.createTextNode(arg)); + static getUrl(path) { + if (path[0] === "/") alert('`path` must not starts with "/"'); + let prefix = "https://raw.githubusercontent.com/redphx/better-xcloud", latestCommitHash = window.localStorage.getItem("BetterXcloud.GhPages.CommitHash"); + if (latestCommitHash) return `${prefix}/${latestCommitHash}/${path}`; + else return `${prefix}/refs/heads/gh-pages/${path}`; } - return $elm; -} -var CE = createElement, domParser = new DOMParser; -function createSvgIcon(icon) { - return domParser.parseFromString(icon.toString(), "image/svg+xml").documentElement; -} -var ButtonStyleIndices = Object.keys(ButtonStyleClass).map((i) => parseInt(i)); -function createButton(options) { - let $btn; - if (options.url) $btn = CE("a", { class: "bx-button" }), $btn.href = options.url, $btn.target = "_blank"; - else $btn = CE("button", { class: "bx-button", type: "button" }); - let style = options.style || 0; - if (style) { - let index; - for (index of ButtonStyleIndices) - style & index && $btn.classList.add(ButtonStyleClass[index]); + static getNativeMkbCustomList() { + let key = "BetterXcloud.GhPages.ForceNativeMkb"; + NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => { + if (json.$schemaVersion !== 1) return; + window.localStorage.setItem(key, JSON.stringify(json)); + }); + let info = JSON.parse(window.localStorage.getItem(key) || "{}"); + if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), {}; + return info.data; + } + static getTouchControlCustomList() { + let key = "BetterXcloud.GhPages.CustomTouchLayouts"; + return NATIVE_FETCH(GhPagesUtils.getUrl("touch-layouts/ids.json")).then((response) => response.json()).then((json) => { + if (Array.isArray(json)) window.localStorage.setItem(key, JSON.stringify(json)); + }), JSON.parse(window.localStorage.getItem(key) || "[]"); } - 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; -} -function getReactProps($elm) { - for (let key in $elm) - if (key.startsWith("__reactProps")) return $elm[key]; - return null; -} -function escapeHtml(html) { - let text = document.createTextNode(html), $span = document.createElement("span"); - return $span.appendChild(text), $span.innerHTML; -} -function isElementVisible($elm) { - let rect = $elm.getBoundingClientRect(); - return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height; -} -var CTN = document.createTextNode.bind(document); -window.BX_CE = createElement; -function removeChildElements($parent) { - while ($parent.firstElementChild) - $parent.firstElementChild.remove(); -} -function clearDataSet($elm) { - Object.keys($elm.dataset).forEach((key) => { - delete $elm.dataset[key]; - }); -} -var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"]; -function humanFileSize(size) { - let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); - return (size / Math.pow(1024, i)).toFixed(2) + " " + FILE_SIZE_UNITS[i]; -} -function secondsToHm(seconds) { - let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1; - if (m === 60) h += 1, m = 0; - let output = []; - return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); -} -function secondsToHms(seconds) { - let h = Math.floor(seconds / 3600); - seconds %= 3600; - let m = Math.floor(seconds / 60), s = seconds % 60, output = []; - if (h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), s > 0 || output.length === 0) output.push(`${s}s`); - return output.join(" "); } var SUPPORTED_LANGUAGES = { "en-US": "English (US)", @@ -312,6 +275,9 @@ var SUPPORTED_LANGUAGES = { "clarity-boost": "Clarity boost", "clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON", clear: "Clear", + "clear-data": "Clear data", + "clear-data-confirm": "Do you want to clear all Better xCloud settings and data?", + "clear-data-success": "Data cleared! Refresh the page to apply the changes.", clock: "Clock", close: "Close", "close-app": "Close app", @@ -332,6 +298,7 @@ var SUPPORTED_LANGUAGES = { "controller-friendly-ui": "Controller-friendly UI", "controller-shortcuts": "Controller shortcuts", "controller-shortcuts-connect-note": "Connect a controller to use this feature", + "controller-shortcuts-in-game": "In-game controller shortcuts", "controller-shortcuts-xbox-note": "Button to open the Guide menu", "controller-vibration": "Controller vibration", copy: "Copy", @@ -346,6 +313,7 @@ var SUPPORTED_LANGUAGES = { "device-vibration": "Device vibration", "device-vibration-not-using-gamepad": "On when not using gamepad", disable: "Disable", + "disable-byog-feature": 'Disable "Stream your own game" feature', "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", @@ -367,6 +335,7 @@ var SUPPORTED_LANGUAGES = { experimental: "Experimental", export: "Export", fast: "Fast", + "force-native-mkb-games": "Force native Mouse & Keyboard for these games", "fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile', "fortnite-force-console-version": "Fortnite: force console version", "game-bar": "Game Bar", @@ -392,7 +361,9 @@ var SUPPORTED_LANGUAGES = { "install-android": "Better xCloud app for Android", japan: "Japan", jitter: "Jitter", + "keyboard-key": "Keyboard key", "keyboard-shortcuts": "Keyboard shortcuts", + "keyboard-shortcuts-in-game": "In-game keyboard shortcuts", korea: "Korea", language: "Language", large: "Large", @@ -402,6 +373,7 @@ var SUPPORTED_LANGUAGES = { "loading-screen": "Loading screen", "local-co-op": "Local co-op", "lowest-quality": "Lowest quality", + manage: "Manage", "map-mouse-to": "Map mouse to", "max-fps": "Max FPS", "may-not-work-properly": "May not work properly!", @@ -409,17 +381,18 @@ var SUPPORTED_LANGUAGES = { 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", + "mkb-disclaimer": "This could be viewed as cheating when playing online", + "modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.", "mouse-and-keyboard": "Mouse & Keyboard", + "mouse-click": "Mouse click", "mouse-wheel": "Mouse wheel", - "msfs2020-force-native-mkb": "MSFS2020: force native M&KB support", muted: "Muted", name: "Name", "native-mkb": "Native Mouse & Keyboard", new: "New", "new-version-available": [ e => `Version ${e.version} available`, - , + e => `Versió ${e.version} disponible`, , e => `Version ${e.version} verfügbar`, e => `Versi ${e.version} tersedia`, @@ -439,8 +412,10 @@ var SUPPORTED_LANGUAGES = { e => `已可更新為 ${e.version} 版` ], "no-consoles-found": "No consoles found", + "no-controllers-connected": "No controllers connected", normal: "Normal", off: "Off", + official: "Official", on: "On", "only-supports-some-games": "Only supports some games", opacity: "Opacity", @@ -519,6 +494,7 @@ var SUPPORTED_LANGUAGES = { screen: "Screen", "screenshot-apply-filters": "Apply video filters to screenshots", "section-all-games": "All games", + "section-byog": "Stream your own game", "section-most-popular": "Most popular", "section-native-mkb": "Play with mouse & keyboard", "section-news": "News", @@ -612,6 +588,8 @@ var SUPPORTED_LANGUAGES = { unknown: "Unknown", unlimited: "Unlimited", unmuted: "Unmuted", + unofficial: "Unofficial", + "unofficial-game-list": "Unofficial game list", "unsharp-masking": "Unsharp masking", upload: "Upload", uploaded: "Uploaded", @@ -631,59 +609,60 @@ var SUPPORTED_LANGUAGES = { volume: "Volume", "wait-time-countdown": "Countdown", "wait-time-estimated": "Estimated finish time", + "waiting-for-input": "Waiting for input...", wallpaper: "Wallpaper", webgl2: "WebGL2" }; class Translations { - static #EN_US = "en-US"; - static #KEY_LOCALE = "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 EN_US = "en-US"; + static KEY_LOCALE = "BetterXcloud.Locale"; + static KEY_TRANSLATIONS = "BetterXcloud.Locale.Translations"; + static selectedLocaleIndex = -1; + static selectedLocale = "en-US"; + static supportedLocales = Object.keys(SUPPORTED_LANGUAGES); + static foreignTranslations = {}; + static enUsIndex = Translations.supportedLocales.indexOf(Translations.EN_US); static async init() { - Translations.#enUsIndex = Translations.#supportedLocales.indexOf(Translations.#EN_US), Translations.refreshLocale(), await Translations.#loadTranslations(); + Translations.refreshLocale(), await Translations.loadTranslations(); } static refreshLocale(newLocale) { let locale; - if (newLocale) localStorage.setItem(Translations.#KEY_LOCALE, newLocale), locale = newLocale; - else locale = localStorage.getItem(Translations.#KEY_LOCALE); - let supportedLocales = Translations.#supportedLocales; + if (newLocale) localStorage.setItem(Translations.KEY_LOCALE, newLocale), locale = newLocale; + else locale = localStorage.getItem(Translations.KEY_LOCALE); + let supportedLocales = Translations.supportedLocales; if (!locale) { - if (locale = window.navigator.language || Translations.#EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.#EN_US; - localStorage.setItem(Translations.#KEY_LOCALE, locale); + if (locale = window.navigator.language || Translations.EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.EN_US; + localStorage.setItem(Translations.KEY_LOCALE, locale); } - Translations.#selectedLocale = locale, Translations.#selectedLocaleIndex = supportedLocales.indexOf(locale); + Translations.selectedLocale = locale, Translations.selectedLocaleIndex = supportedLocales.indexOf(locale); } static get(key, values) { let text = null; - if (Translations.#foreignTranslations && Translations.#selectedLocale !== Translations.#EN_US) text = Translations.#foreignTranslations[key]; + if (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); + 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; + static async loadTranslations() { + if (Translations.selectedLocale === Translations.EN_US) return; try { - Translations.#foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.#KEY_TRANSLATIONS)); + Translations.foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.KEY_TRANSLATIONS)); } catch (e) {} - if (!Translations.#foreignTranslations) await this.downloadTranslations(Translations.#selectedLocale); + if (!Translations.foreignTranslations) await this.downloadTranslations(Translations.selectedLocale); } static async updateTranslations(async = !1) { - if (Translations.#selectedLocale === Translations.#EN_US) { - localStorage.removeItem(Translations.#KEY_TRANSLATIONS); + if (Translations.selectedLocale === Translations.EN_US) { + localStorage.removeItem(Translations.KEY_TRANSLATIONS); return; } - if (async) Translations.downloadTranslationsAsync(Translations.#selectedLocale); - else await Translations.downloadTranslations(Translations.#selectedLocale); + if (async) Translations.downloadTranslationsAsync(Translations.selectedLocale); + else await Translations.downloadTranslations(Translations.selectedLocale); } static async downloadTranslations(locale) { try { - let 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; + let translations = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`))).json(); + if (localStorage.getItem(Translations.KEY_LOCALE) === locale) window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations; return !0; } catch (e) { debugger; @@ -691,16 +670,231 @@ class Translations { 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; + NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`)).then((resp) => resp.json()).then((translations) => { + window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations; }); } static switchLocale(locale) { - localStorage.setItem(Translations.#KEY_LOCALE, locale); + localStorage.setItem(Translations.KEY_LOCALE, locale); } } -var t = Translations.get; +var t = Translations.get, ut = (text) => { + return BxLogger.warning("Untranslated text", text), text; +}; Translations.init(); +class NavigationUtils { + static setNearby($elm, nearby) { + $elm.nearby = $elm.nearby || {}; + let key; + for (key in nearby) + $elm.nearby[key] = nearby[key]; + } +} +var setNearby = NavigationUtils.setNearby; +var ButtonStyleClass = { + 1: "bx-primary", + 2: "bx-warning", + 4: "bx-danger", + 8: "bx-ghost", + 16: "bx-frosted", + 32: "bx-drop-shadow", + 64: "bx-focusable", + 128: "bx-full-width", + 256: "bx-full-height", + 512: "bx-tall", + 1024: "bx-circular", + 2048: "bx-normal-case", + 4096: "bx-normal-link" +}; +function createElement(elmName, props = {}, ..._) { + let $elm, 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; + if (props._on) { + for (let name in props._on) + $elm.addEventListener(name, props._on[name]); + delete props._on; + } + if (props._dataset) { + for (let name in props._dataset) + $elm.dataset[name] = props._dataset[name]; + delete props._dataset; + } + for (let key in props) { + if ($elm.hasOwnProperty(key)) continue; + let value = props[key]; + if (hasNs) $elm.setAttributeNS(null, key, value); + else $elm.setAttribute(key, value); + } + for (let i = 2, size = arguments.length;i < size; i++) { + let arg = arguments[i]; + if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.append(arg); + } + return $elm; +} +var domParser = new DOMParser; +function createSvgIcon(icon) { + return domParser.parseFromString(icon.toString(), "image/svg+xml").documentElement; +} +var ButtonStyleIndices = Object.keys(ButtonStyleClass).map((i) => parseInt(i)); +function createButton(options) { + let $btn; + if (options.url) $btn = CE("a", { + class: "bx-button", + href: options.url, + target: "_blank" + }); + else $btn = CE("button", { + class: "bx-button", + type: "button" + }), options.disabled && ($btn.disabled = !0); + let style = options.style || 0; + if (style) { + let index; + for (index of ButtonStyleIndices) + style & index && $btn.classList.add(ButtonStyleClass[index]); + } + if (options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", {}, options.label)), options.title && $btn.setAttribute("title", options.title), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0, options.secondaryText) $btn.classList.add("bx-button-multi-lines"), $btn.appendChild(CE("span", {}, options.secondaryText)); + for (let key in options.attributes) + if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]); + return $btn; +} +function createSettingRow(label, $control, options = {}) { + let $label, $row = CE("label", { class: "bx-settings-row" }, $label = CE("span", { class: "bx-settings-label" }, label, options.$note), $control), $link = $label.querySelector("a"); + if ($link) $link.classList.add("bx-focusable"), setNearby($label, { + focus: $link + }); + if (setNearby($row, { + orientation: options.multiLines ? "vertical" : "horizontal" + }), options.multiLines) + $row.dataset.multiLines = "true"; + if ($control instanceof HTMLElement && $control.id) $row.htmlFor = $control.id; + return $row; +} +function getReactProps($elm) { + for (let key in $elm) + if (key.startsWith("__reactProps")) return $elm[key]; + return null; +} +function escapeHtml(html) { + let text = document.createTextNode(html), $span = document.createElement("span"); + return $span.appendChild(text), $span.innerHTML; +} +function isElementVisible($elm) { + let rect = $elm.getBoundingClientRect(); + return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height; +} +function removeChildElements($parent) { + if ($parent instanceof HTMLDivElement && $parent.classList.contains("bx-select")) $parent = $parent.querySelector("select"); + while ($parent.firstElementChild) + $parent.firstElementChild.remove(); +} +function clearDataSet($elm) { + Object.keys($elm.dataset).forEach((key) => { + delete $elm.dataset[key]; + }); +} +function renderPresetsList($select, allPresets, selectedValue, addOffValue = !1) { + if (removeChildElements($select), addOffValue) { + let $option = CE("option", { value: 0 }, t("off")); + $select.appendChild($option); + } + let groups = { + default: t("default"), + custom: t("custom") + }, key; + for (key in groups) { + let $optGroup = CE("optgroup", { label: groups[key] }); + for (let id of allPresets[key]) { + let record = allPresets.data[id], $option = CE("option", { value: record.id }, record.name); + $optGroup.appendChild($option); + } + if ($optGroup.hasChildNodes()) $select.appendChild($optGroup); + } + if (selectedValue) $select.value = selectedValue.toString(), BxEvent.dispatch($select, "input", { manualTrigger: !0 }); +} +var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"]; +function humanFileSize(size) { + let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); + return (size / Math.pow(1024, i)).toFixed(2) + " " + FILE_SIZE_UNITS[i]; +} +function secondsToHm(seconds) { + let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1; + if (m === 60) h += 1, m = 0; + let output = []; + return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); +} +function secondsToHms(seconds) { + let h = Math.floor(seconds / 3600); + seconds %= 3600; + let m = Math.floor(seconds / 60), s = seconds % 60, output = []; + if (h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), s > 0 || output.length === 0) output.push(`${s}s`); + return output.join(" "); +} +var CE = createElement; +window.BX_CE = createElement; +class Toast { + static instance; + static getInstance = () => Toast.instance ?? (Toast.instance = new Toast); + LOG_TAG = "Toast"; + $wrapper; + $msg; + $status; + stack = []; + isShowing = !1; + timeoutId; + DURATION = 3000; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"), this.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, this.$msg = CE("span", { class: "bx-toast-msg" }), this.$status = CE("span", { class: "bx-toast-status" })), this.$wrapper.addEventListener("transitionend", (e) => { + let classList = this.$wrapper.classList; + if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), this.showNext(); + }), document.documentElement.appendChild(this.$wrapper); + } + show(msg, status, options = {}) { + options = options || {}; + let args = Array.from(arguments); + if (options.instant) this.stack = [args], this.showNext(); + else this.stack.push(args), !this.isShowing && this.showNext(); + } + showNext() { + if (!this.stack.length) { + this.isShowing = !1; + return; + } + this.isShowing = !0, this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, this.DURATION); + let [msg, status, options] = this.stack.shift(); + if (options && options.html) this.$msg.innerHTML = msg; + else this.$msg.textContent = msg; + if (status) this.$status.classList.remove("bx-gone"), this.$status.textContent = status; + else this.$status.classList.add("bx-gone"); + let classList = this.$wrapper.classList; + classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); + } + hide = () => { + this.timeoutId = null; + let classList = this.$wrapper.classList; + classList.remove("bx-show"), classList.add("bx-hide"); + }; + static show(msg, status, options = {}) { + Toast.getInstance().show(msg, status, options); + } + static showNext() { + Toast.getInstance().showNext(); + } +} +class MicrophoneShortcut { + static toggle(showToast = !0) { + if (!window.BX_EXPOSED.streamSession) return !1; + let enableMic = window.BX_EXPOSED.streamSession._microphoneState === "Enabled" ? !1 : !0; + try { + return window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic), showToast && Toast.show(t("microphone"), t(enableMic ? "unmuted" : "muted"), { instant: !0 }), enableMic; + } catch (e) { + console.log(e); + } + return !1; + } +} var BypassServers = { br: t("brazil"), jp: t("japan"), @@ -714,175 +908,6 @@ var BypassServers = { pl: "45.134.212.66", us: "143.244.47.65" }; -class SettingElement { - static #renderOptions(key, setting, currentValue, onChange) { - let $control = CE("select", { - tabindex: 0 - }), $parent; - if (setting.optionsGroup) $parent = CE("optgroup", { - label: setting.optionsGroup - }), $control.appendChild($parent); - else $parent = $control; - for (let value in setting.options) { - let label = setting.options[value], $option = CE("option", { value }, label); - $parent.appendChild($option); - } - return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { - let target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value; - !e.ignoreOnChange && onChange(e, value); - }), $control.setValue = (value) => { - $control.value = value; - }, $control; - } - static #renderMultipleOptions(key, setting, currentValue, onChange, params = {}) { - let $control = CE("select", { - multiple: !0, - tabindex: 0 - }); - if (params && params.size) $control.setAttribute("size", params.size.toString()); - for (let value in setting.multipleOptions) { - let label = setting.multipleOptions[value], $option = CE("option", { value }, label); - $option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) { - e.preventDefault(); - let target = e.target; - target.selected = !target.selected; - let $parent = target.parentElement; - $parent.focus(), BxEvent.dispatch($parent, "input"); - }), $control.appendChild($option); - } - return $control.addEventListener("mousedown", function(e) { - let self = this, orgScrollTop = self.scrollTop; - window.setTimeout(() => self.scrollTop = orgScrollTop, 0); - }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => { - let target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value); - !e.ignoreOnChange && onChange(e, values); - }), $control; - } - static #renderNumber(key, setting, currentValue, onChange) { - let $control = CE("input", { - tabindex: 0, - type: "number", - min: setting.min, - max: setting.max - }); - return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { - let 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) { - let $control = CE("input", { type: "checkbox", tabindex: 0 }); - return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => { - !e.ignoreOnChange && onChange(e, e.target.checked); - }), $control.setValue = (value) => { - $control.checked = !!value; - }, $control; - } - static #renderNumberStepper(key, setting, value, onChange, options = {}) { - options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider; - let $text, $btnDec, $btnInc, $range = null, controlValue = value, MIN = options.reverse ? -setting.max : setting.min, MAX = options.reverse ? -setting.min : setting.max, STEPS = Math.max(setting.steps || 1, 1), intervalId, isHolding = !1, clearIntervalId = () => { - intervalId && clearInterval(intervalId), intervalId = null; - }, 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 = () => { - if ($btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX), controlValue === MIN || controlValue === MAX) clearIntervalId(); - }, $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_inp_setting_${key}`, - type: "range", - min: MIN, - max: MAX, - value: options.reverse ? -value : value, - step: STEPS, - tabindex: 0 - }), options.hideSlider && $range.classList.add("bx-gone"), $range.addEventListener("input", (e) => { - if (value = parseInt(e.target.value), options.reverse) value *= -1; - if (controlValue === value) return; - controlValue = options.reverse ? -value : 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) { - let markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId }); - if ($range.setAttribute("list", markersId), options.exactTicks) { - let start = Math.max(Math.floor(setting.min / options.exactTicks), 1) * options.exactTicks; - if (start === setting.min) start += options.exactTicks; - for (let i = start;i < setting.max; i += options.exactTicks) - $markers.appendChild(CE("option", { - value: options.reverse ? -i : i - })); - } else for (let i = MIN + options.ticks;i < MAX; i += options.ticks) - $markers.appendChild(CE("option", { value: i })); - $wrapper.appendChild($markers); - } - updateButtonsVisibility(); - let buttonPressed = (e, $btn) => { - 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()), onChange && onChange(e, value2); - }, onClick = (e) => { - if (e.preventDefault(), isHolding) return; - let $btn = e.target.closest("button"); - $btn && buttonPressed(e, $btn), clearIntervalId(), isHolding = !1; - }, onPointerDown = (e) => { - clearIntervalId(); - let $btn = e.target.closest("button"); - if (!$btn) return; - isHolding = !0, e.preventDefault(), intervalId = window.setInterval((e2) => { - buttonPressed(e2, $btn); - }, 200), window.addEventListener("pointerup", onPointerUp, { once: !0 }), window.addEventListener("pointercancel", onPointerUp, { once: !0 }); - }, onPointerUp = (e) => { - clearIntervalId(), isHolding = !1; - }, onContextMenu = (e) => e.preventDefault(); - return $wrapper.setValue = (value2) => { - $text.textContent = renderTextValue(value2), $range.value = options.reverse ? -value2 : value2; - }, $wrapper.addEventListener("click", onClick), $wrapper.addEventListener("pointerdown", onPointerDown), $wrapper.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, { - focus: options.hideSlider ? $btnInc : $range - }), $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) { - let 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 = {}) { - let definition = storage.getDefinition(key), 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; @@ -901,6 +926,8 @@ class BaseSettingsStore { get settings() { if (this._settings) return this._settings; let settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}"); + for (let key in settings) + settings[key] = this.validateValue("get", key, settings[key]); return this._settings = settings, settings; } getDefinition(key) { @@ -911,18 +938,15 @@ class BaseSettingsStore { return this.definitions[key]; } getSetting(key, checkUnsupported = !0) { - if (typeof key === "undefined") { - debugger; - return; - } let 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); + if (checkUnsupported && definition.unsupported) if ("unsupportedValue" in definition) return definition.unsupportedValue; + else return definition.default; + if (!(key in this.settings)) this.settings[key] = this.validateValue("get", key, null); return this.settings[key]; } setSetting(key, value, emitEvent = !1) { - return value = this.validateValue(key, value), this.settings[key] = value, this.saveSettings(), emitEvent && BxEvent.dispatch(window, BxEvent.SETTINGS_CHANGED, { + return value = this.validateValue("set", key, value), this.settings[key] = this.validateValue("get", key, value), this.saveSettings(), emitEvent && BxEvent.dispatch(window, BxEvent.SETTINGS_CHANGED, { storageKey: this.storageKey, settingKey: key, settingValue: value @@ -931,14 +955,16 @@ class BaseSettingsStore { saveSettings() { this.storage.setItem(this.storageKey, JSON.stringify(this.settings)); } - validateValue(key, value) { + validateValue(action, key, value) { let def = this.definitions[key]; if (!def) return value; if (typeof value === "undefined" || value === null) value = def.default; + if (def.transformValue && action === "get") value = def.transformValue.get.call(def, value); if ("min" in def) value = Math.max(def.min, value); if ("max" in def) value = Math.min(def.max, value); - if ("options" in def && !(value in def.options)) value = def.default; - else if ("multipleOptions" in def) { + if ("options" in def) { + if (!(value in def.options)) value = def.default; + } else if ("multipleOptions" in def) { if (value.length) { let validOptions = Object.keys(def.multipleOptions); value.forEach((item2, idx) => { @@ -947,6 +973,7 @@ class BaseSettingsStore { } if (!value.length) value = def.default; } + if (def.transformValue && action === "set") value = def.transformValue.set.call(def, value); return value; } getLabel(key) { @@ -954,10 +981,11 @@ class BaseSettingsStore { } getValueText(key, value) { let definition = this.definitions[key]; - if (definition.type === "number-stepper") { + if ("min" in definition) { let params = definition.params; if (params.customTextValue) { - let text = params.customTextValue(value); + if (definition.transformValue) value = definition.transformValue.get.call(definition, value); + let text = params.customTextValue(value, definition.min, definition.max); if (text) return text; } return value.toString(); @@ -968,6 +996,1034 @@ class BaseSettingsStore { return value.toString(); } } +class LocalDb { + static instance; + static getInstance = () => LocalDb.instance ?? (LocalDb.instance = new LocalDb); + static DB_NAME = "BetterXcloud"; + static DB_VERSION = 3; + static TABLE_VIRTUAL_CONTROLLERS = "virtual_controllers"; + static TABLE_CONTROLLER_SHORTCUTS = "controller_shortcuts"; + static TABLE_CONTROLLER_SETTINGS = "controller_settings"; + static TABLE_KEYBOARD_SHORTCUTS = "keyboard_shortcuts"; + db; + open() { + return new Promise((resolve, reject) => { + if (this.db) { + resolve(this.db); + return; + } + let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION); + request.onupgradeneeded = (e) => { + let db = e.target.result; + if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined"); + if (!db.objectStoreNames.contains(LocalDb.TABLE_VIRTUAL_CONTROLLERS)) db.createObjectStore(LocalDb.TABLE_VIRTUAL_CONTROLLERS, { + keyPath: "id", + autoIncrement: !0 + }); + if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SHORTCUTS, { + keyPath: "id", + autoIncrement: !0 + }); + if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SETTINGS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SETTINGS, { + keyPath: "id" + }); + if (!db.objectStoreNames.contains(LocalDb.TABLE_KEYBOARD_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_KEYBOARD_SHORTCUTS, { + keyPath: "id", + autoIncrement: !0 + }); + }, request.onerror = (e) => { + console.log(e), alert(e.target.error.message), reject && reject(); + }, request.onsuccess = (e) => { + this.db = e.target.result, resolve(this.db); + }; + }); + } +} +class BaseLocalTable { + tableName; + constructor(tableName) { + this.tableName = tableName; + } + async prepareTable(type = "readonly") { + return (await LocalDb.getInstance().open()).transaction(this.tableName, type).objectStore(this.tableName); + } + call(method) { + return new Promise((resolve) => { + let request = method.call(null, ...Array.from(arguments).slice(1)); + request.onsuccess = (e) => { + resolve(e.target.result); + }; + }); + } + async count() { + let table = await this.prepareTable(); + return this.call(table.count.bind(table)); + } + async add(data) { + let table = await this.prepareTable("readwrite"); + return this.call(table.add.bind(table), ...arguments); + } + async put(data) { + let table = await this.prepareTable("readwrite"); + return this.call(table.put.bind(table), ...arguments); + } + async delete(id) { + let table = await this.prepareTable("readwrite"); + return this.call(table.delete.bind(table), ...arguments); + } + async get(id) { + let table = await this.prepareTable(); + return this.call(table.get.bind(table), ...arguments); + } + async getAll() { + let table = await this.prepareTable(), all = await this.call(table.getAll.bind(table), ...arguments), results = {}; + return all.forEach((item2) => { + results[item2.id] = item2; + }), results; + } +} +class BasePresetsTable extends BaseLocalTable { + async newPreset(name, data) { + let newRecord = { name, data }; + return await this.add(newRecord); + } + async updatePreset(preset) { + return await this.put(preset); + } + async deletePreset(id) { + return this.delete(id); + } + async getPreset(id) { + if (id === 0) return null; + if (id < 0) return this.DEFAULT_PRESETS[id]; + let preset = await this.get(id); + if (!preset) preset = this.DEFAULT_PRESETS[this.DEFAULT_PRESET_ID]; + return preset; + } + async getPresets() { + let all = deepClone(this.DEFAULT_PRESETS), presets = { + default: Object.keys(this.DEFAULT_PRESETS).map((key) => parseInt(key)), + custom: [], + data: {} + }; + if (await this.count() > 0) { + let items = await this.getAll(), id; + for (id in items) { + let item2 = items[id]; + presets.custom.push(item2.id), all[item2.id] = item2; + } + } + return presets.data = all, presets; + } + async getPresetsData() { + let presetsData = {}; + for (let id in this.DEFAULT_PRESETS) { + let preset = this.DEFAULT_PRESETS[id]; + presetsData[id] = deepClone(preset.data); + } + if (await this.count() > 0) { + let items = await this.getAll(), id; + for (id in items) { + let item2 = items[id]; + presetsData[item2.id] = item2.data; + } + } + return presetsData; + } +} +class MkbMappingPresetsTable extends BasePresetsTable { + static instance; + static getInstance = () => MkbMappingPresetsTable.instance ?? (MkbMappingPresetsTable.instance = new MkbMappingPresetsTable); + LOG_TAG = "MkbMappingPresetsTable"; + TABLE_PRESETS = LocalDb.TABLE_VIRTUAL_CONTROLLERS; + DEFAULT_PRESETS = { + [-1]: { + id: -1, + name: "Shooter", + data: { + mapping: { + 16: ["Backquote"], + 12: ["ArrowUp"], + 13: ["ArrowDown"], + 14: ["ArrowLeft"], + 15: ["ArrowRight"], + 100: ["KeyW"], + 101: ["KeyS"], + 102: ["KeyA"], + 103: ["KeyD"], + 200: ["KeyI"], + 201: ["KeyK"], + 202: ["KeyJ"], + 203: ["KeyL"], + 0: ["Space", "KeyE"], + 2: ["KeyR"], + 1: ["ControlLeft", "Backspace"], + 3: ["KeyV"], + 9: ["Enter"], + 8: ["Tab"], + 4: ["KeyC", "KeyG"], + 5: ["KeyQ"], + 7: ["Mouse0"], + 6: ["Mouse2"], + 10: ["ShiftLeft"], + 11: ["KeyF"] + }, + mouse: { + mapTo: 2, + sensitivityX: 100, + sensitivityY: 100, + deadzoneCounterweight: 20 + } + } + } + }; + DEFAULT_PRESET_ID = -1; + constructor() { + super(LocalDb.TABLE_VIRTUAL_CONTROLLERS); + BxLogger.info(this.LOG_TAG, "constructor()"); + } +} +class KeyboardShortcutsTable extends BasePresetsTable { + static instance; + static getInstance = () => KeyboardShortcutsTable.instance ?? (KeyboardShortcutsTable.instance = new KeyboardShortcutsTable); + LOG_TAG = "KeyboardShortcutsTable"; + TABLE_PRESETS = LocalDb.TABLE_KEYBOARD_SHORTCUTS; + DEFAULT_PRESETS = { + [-1]: { + id: -1, + name: t("default"), + data: { + mapping: { + "mkb-toggle": { + code: "F8" + }, + "stream-screenshot-capture": { + code: "Slash" + } + } + } + } + }; + DEFAULT_PRESET_ID = -1; + constructor() { + super(LocalDb.TABLE_KEYBOARD_SHORTCUTS); + BxLogger.info(this.LOG_TAG, "constructor()"); + } +} +function getSupportedCodecProfiles() { + let options = { + default: t("default") + }; + if (!("getCapabilities" in RTCRtpReceiver)) return options; + let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1, codecs = RTCRtpReceiver.getCapabilities("video").codecs; + for (let codec of codecs) { + if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue; + let fmtp = codec.sdpFmtpLine.toLowerCase(); + if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0; + else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0; + else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0; + } + if (hasLowCodec) if (!hasNormalCodec && !hasHighCodec) options["default"] = `${t("visual-quality-low")} (${t("default")})`; + else options["low"] = t("visual-quality-low"); + if (hasNormalCodec) if (!hasLowCodec && !hasHighCodec) options["default"] = `${t("visual-quality-normal")} (${t("default")})`; + else options["normal"] = t("visual-quality-normal"); + if (hasHighCodec) if (!hasLowCodec && !hasNormalCodec) options["default"] = `${t("visual-quality-high")} (${t("default")})`; + else options["high"] = t("visual-quality-high"); + return options; +} +class GlobalSettingsStorage extends BaseSettingsStore { + static DEFINITIONS = { + versionLastCheck: { + default: 0 + }, + versionLatest: { + default: "" + }, + versionCurrent: { + default: "" + }, + bxLocale: { + label: t("language"), + default: localStorage.getItem("BetterXcloud.Locale") || "en-US", + options: SUPPORTED_LANGUAGES + }, + serverRegion: { + label: t("region"), + note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")), + default: "default" + }, + serverBypassRestriction: { + 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) + }, + streamLocale: { + label: t("preferred-game-language"), + default: "default", + options: { + default: t("default"), + "ar-SA": "العربية", + "bg-BG": "Български", + "cs-CZ": "čeština", + "da-DK": "dansk", + "de-DE": "Deutsch", + "el-GR": "Ελληνικά", + "en-GB": "English (UK)", + "en-US": "English (US)", + "es-ES": "español (España)", + "es-MX": "español (Latinoamérica)", + "fi-FI": "suomi", + "fr-FR": "français", + "he-IL": "עברית", + "hu-HU": "magyar", + "it-IT": "italiano", + "ja-JP": "日本語", + "ko-KR": "한국어", + "nb-NO": "norsk bokmål", + "nl-NL": "Nederlands", + "pl-PL": "polski", + "pt-BR": "português (Brasil)", + "pt-PT": "português (Portugal)", + "ro-RO": "Română", + "ru-RU": "русский", + "sk-SK": "slovenčina", + "sv-SE": "svenska", + "th-TH": "ไทย", + "tr-TR": "Türkçe", + "zh-CN": "中文(简体)", + "zh-TW": "中文 (繁體)" + } + }, + streamResolution: { + label: t("target-resolution"), + default: "auto", + options: { + auto: t("default"), + "720p": "720p", + "1080p": "1080p" + }, + suggest: { + lowest: "720p", + highest: "1080p" + } + }, + streamCodecProfile: { + label: t("visual-quality"), + default: "default", + options: getSupportedCodecProfiles(), + ready: (setting) => { + let options = setting.options, keys = Object.keys(options); + if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature"); + setting.suggest = { + lowest: keys.length === 1 ? keys[0] : keys[1], + highest: keys[keys.length - 1] + }; + } + }, + serverPreferIpv6: { + label: t("prefer-ipv6-server"), + default: !1 + }, + screenshotApplyFilters: { + requiredVariants: "full", + label: t("screenshot-apply-filters"), + default: !1 + }, + uiSkipSplashVideo: { + label: t("skip-splash-video"), + default: !1 + }, + uiHideSystemMenuIcon: { + label: t("hide-system-menu-icon"), + default: !1 + }, + streamCombineSources: { + requiredVariants: "full", + label: t("combine-audio-video-streams"), + default: !1, + experimental: !0, + note: t("combine-audio-video-streams-summary") + }, + touchControllerMode: { + requiredVariants: "full", + label: t("tc-availability"), + default: "all", + options: { + default: t("default"), + off: t("off"), + all: t("tc-all-games") + }, + unsupported: !STATES.userAgent.capabilities.touch, + unsupportedValue: "default" + }, + touchControllerAutoOff: { + requiredVariants: "full", + label: t("tc-auto-off"), + default: !1, + unsupported: !STATES.userAgent.capabilities.touch + }, + touchControllerDefaultOpacity: { + requiredVariants: "full", + label: t("tc-default-opacity"), + default: 100, + min: 10, + max: 100, + params: { + steps: 10, + suffix: "%", + ticks: 10, + hideSlider: !0 + }, + unsupported: !STATES.userAgent.capabilities.touch + }, + touchControllerStyleStandard: { + 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 + }, + touchControllerStyleCustom: { + requiredVariants: "full", + label: t("tc-custom-layout-style"), + default: "default", + options: { + default: t("default"), + muted: t("tc-muted-colors") + }, + unsupported: !STATES.userAgent.capabilities.touch + }, + uiSimplifyStreamMenu: { + label: t("simplify-stream-menu"), + default: !1 + }, + mkbHideIdleCursor: { + requiredVariants: "full", + label: t("hide-idle-cursor"), + default: !1 + }, + uiDisableFeedbackDialog: { + requiredVariants: "full", + label: t("disable-post-stream-feedback-dialog"), + default: !1 + }, + streamMaxVideoBitrate: { + requiredVariants: "full", + label: t("bitrate-video-maximum"), + note: "⚠️ " + t("unexpected-behavior"), + default: 0, + min: 102400, + max: 15360000, + transformValue: { + get(value) { + return value === 0 ? this.max : value; + }, + set(value) { + return value === this.max ? 0 : value; + } + }, + params: { + steps: 102400, + exactTicks: 5120000, + customTextValue: (value, min, max) => { + if (value = parseInt(value), value === max) return t("unlimited"); + else return (value / 1024000).toFixed(1) + " Mb/s"; + } + }, + suggest: { + highest: 0 + } + }, + gameBarPosition: { + requiredVariants: "full", + label: t("position"), + default: "bottom-left", + options: { + off: t("off"), + "bottom-left": t("bottom-left"), + "bottom-right": t("bottom-right") + } + }, + localCoOpEnabled: { + 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")) + }, + uiShowControllerStatus: { + label: t("show-controller-connection-status"), + default: !0 + }, + deviceVibrationMode: { + requiredVariants: "full", + label: t("device-vibration"), + default: "off", + options: { + off: t("off"), + on: t("on"), + auto: t("device-vibration-not-using-gamepad") + } + }, + deviceVibrationIntensity: { + requiredVariants: "full", + label: t("vibration-intensity"), + default: 50, + min: 10, + max: 100, + params: { + steps: 10, + suffix: "%", + exactTicks: 20 + } + }, + controllerPollingRate: { + requiredVariants: "full", + label: t("polling-rate"), + default: 4, + min: 4, + max: 60, + params: { + steps: 4, + exactTicks: 20, + reverse: !0, + customTextValue(value) { + value = parseInt(value); + let text = +(1000 / value).toFixed(2) + " Hz"; + if (value === 4) text = `${text} (${t("default")})`; + return text; + } + } + }, + mkbEnabled: { + requiredVariants: "full", + label: t("enable-mkb"), + default: !1, + unsupported: !STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb, + ready: (setting) => { + let note, url; + if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657"; + else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer"; + setting.unsupportedNote = () => CE("a", { + href: url, + target: "_blank" + }, "⚠️ " + note); + } + }, + nativeMkbMode: { + requiredVariants: "full", + label: t("native-mkb"), + default: "default", + options: { + default: t("default"), + off: t("off"), + on: t("on") + }, + ready: (setting) => { + if (STATES.browser.capabilities.emulatedNativeMkb) ; + else if (UserAgent.isMobile()) setting.unsupported = !0, setting.unsupportedValue = "off", delete setting.options["default"], delete setting.options["on"]; + else delete setting.options["on"]; + } + }, + forceNativeMkbGames: { + label: t("force-native-mkb-games"), + default: [], + ready: (setting) => { + setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(), setting.unsupported = !AppInterface && UserAgent.isMobile(); + } + }, + nativeMkbScrollXSensitivity: { + requiredVariants: "full", + label: t("horizontal-scroll-sensitivity"), + default: 0, + min: 0, + max: 1e4, + params: { + steps: 10, + exactTicks: 2000, + customTextValue: (value) => { + if (!value) return t("default"); + return (value / 100).toFixed(1) + "x"; + } + } + }, + nativeMkbScrollYSensitivity: { + requiredVariants: "full", + label: t("vertical-scroll-sensitivity"), + default: 0, + min: 0, + max: 1e4, + params: { + steps: 10, + exactTicks: 2000, + customTextValue: (value) => { + if (!value) return t("default"); + return (value / 100).toFixed(1) + "x"; + } + } + }, + mkbMappingPresetIdP1: { + requiredVariants: "full", + default: -1 + }, + mkbSlotP1: { + requiredVariants: "full", + default: 1, + min: 1, + max: 4, + params: { + hideSlider: !0 + } + }, + mkbMappingPresetIdP2: { + requiredVariants: "full", + default: 0 + }, + mkbSlotP2: { + requiredVariants: "full", + default: 0, + min: 0, + max: 4, + params: { + hideSlider: !0, + customTextValue(value) { + return value = parseInt(value), value === 0 ? t("off") : value.toString(); + } + } + }, + keyboardShortcutsInGamePresetId: { + requiredVariants: "full", + default: -1 + }, + uiReduceAnimations: { + label: t("reduce-animations"), + default: !1 + }, + loadingScreenGameArt: { + requiredVariants: "full", + label: t("show-game-art"), + default: !0 + }, + loadingScreenShowWaitTime: { + label: t("show-wait-time"), + default: !0 + }, + loadingScreenRocket: { + label: t("rocket-animation"), + default: "show", + options: { + show: t("rocket-always-show"), + "hide-queue": t("rocket-hide-queue"), + hide: t("rocket-always-hide") + } + }, + uiControllerFriendly: { + label: t("controller-friendly-ui"), + default: BX_FLAGS.DeviceInfo.deviceType !== "unknown" + }, + uiLayout: { + requiredVariants: "full", + label: t("layout"), + default: "default", + options: { + default: t("default"), + normal: t("normal"), + tv: t("smart-tv") + } + }, + uiHideScrollbar: { + label: t("hide-scrollbar"), + default: !1 + }, + uiHideSections: { + requiredVariants: "full", + label: t("hide-sections"), + default: [], + multipleOptions: { + news: t("section-news"), + friends: t("section-play-with-friends"), + "native-mkb": t("section-native-mkb"), + touch: t("section-touch"), + "most-popular": t("section-most-popular"), + "all-games": t("section-all-games") + }, + params: { + size: 0 + } + }, + byogDisabled: { + label: t("disable-byog-feature"), + default: !1 + }, + uiGameCardShowWaitTime: { + requiredVariants: "full", + label: t("show-wait-time-in-game-card"), + default: !0 + }, + blockSocialFeatures: { + label: t("disable-social-features"), + default: !1 + }, + blockTracking: { + label: t("disable-xcloud-analytics"), + default: !1 + }, + userAgentProfile: { + 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") + } + }, + videoPlayerType: { + label: t("renderer"), + default: "default", + options: { + default: t("default"), + webgl2: t("webgl2") + }, + suggest: { + lowest: "default", + highest: "webgl2" + } + }, + videoProcessing: { + label: t("clarity-boost"), + default: "usm", + options: { + usm: t("unsharp-masking"), + cas: t("amd-fidelity-cas") + }, + suggest: { + lowest: "usm", + highest: "cas" + } + }, + videoPowerPreference: { + label: t("renderer-configuration"), + default: "default", + options: { + default: t("default"), + "low-power": t("battery-saving"), + "high-performance": t("high-performance") + }, + suggest: { + highest: "low-power" + } + }, + videoMaxFps: { + label: t("max-fps"), + default: 60, + min: 10, + max: 60, + params: { + steps: 10, + exactTicks: 10, + customTextValue: (value) => { + return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps"; + } + } + }, + videoSharpness: { + label: t("sharpness"), + default: 0, + min: 0, + max: 10, + params: { + exactTicks: 2, + customTextValue: (value) => { + return value = parseInt(value), value === 0 ? t("off") : value.toString(); + } + }, + suggest: { + lowest: 0, + highest: 2 + } + }, + videoRatio: { + 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") + } + }, + videoSaturation: { + label: t("saturation"), + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + videoContrast: { + label: t("contrast"), + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + videoBrightness: { + label: t("brightness"), + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + audioMicOnPlaying: { + label: t("enable-mic-on-startup"), + default: !1 + }, + audioEnableVolumeControl: { + requiredVariants: "full", + label: t("enable-volume-control"), + default: !1 + }, + audioVolume: { + label: t("volume"), + default: 100, + min: 0, + max: 600, + params: { + steps: 10, + suffix: "%", + ticks: 100 + } + }, + statsItems: { + label: t("stats"), + default: ["ping", "fps", "btr", "dt", "pl", "fl"], + multipleOptions: { + time: t("clock"), + play: t("playtime"), + batt: t("battery"), + ping: t("stat-ping"), + jit: t("jitter"), + fps: t("stat-fps"), + btr: t("stat-bitrate"), + dt: t("stat-decode-time"), + pl: t("stat-packets-lost"), + fl: t("stat-frames-lost"), + dl: t("downloaded"), + ul: t("uploaded") + }, + params: { + size: 0 + }, + ready: (setting) => { + let multipleOptions = setting.multipleOptions; + if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"]; + for (let key in multipleOptions) + multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key]; + } + }, + statsShowWhenPlaying: { + label: t("show-stats-on-startup"), + default: !1 + }, + statsQuickGlance: { + label: "👀 " + t("enable-quick-glance-mode"), + default: !0 + }, + statsPosition: { + label: t("position"), + default: "top-right", + options: { + "top-left": t("top-left"), + "top-center": t("top-center"), + "top-right": t("top-right") + } + }, + statsTextSize: { + label: t("text-size"), + default: "0.9rem", + options: { + "0.9rem": t("small"), + "1.0rem": t("normal"), + "1.1rem": t("large") + } + }, + statsTransparent: { + label: t("transparent-background"), + default: !1 + }, + statsOpacity: { + label: t("opacity"), + default: 80, + min: 50, + max: 100, + params: { + steps: 10, + suffix: "%", + ticks: 10 + } + }, + statsConditionalFormatting: { + label: t("conditional-formatting"), + default: !1 + }, + xhomeEnabled: { + requiredVariants: "full", + label: t("enable-remote-play-feature"), + default: !1 + }, + xhomeStreamResolution: { + requiredVariants: "full", + default: "1080p", + options: { + "1080p": "1080p", + "720p": "720p" + } + }, + gameFortniteForceConsole: { + requiredVariants: "full", + label: "🎮 " + t("fortnite-force-console-version"), + default: !1, + note: t("fortnite-allow-stw-mode") + } + }; + constructor() { + super("BetterXcloud", GlobalSettingsStorage.DEFINITIONS); + } +} +var globalSettings = new GlobalSettingsStorage, getPrefDefinition = globalSettings.getDefinition.bind(globalSettings), getPref = globalSettings.getSetting.bind(globalSettings), setPref = globalSettings.setSetting.bind(globalSettings); +STORAGE.Global = globalSettings; +function checkForUpdate() { + if (SCRIPT_VERSION.includes("beta")) return; + let CHECK_INTERVAL_SECONDS = 7200, currentVersion = getPref("versionCurrent"), lastCheck = getPref("versionLastCheck"), now = Math.round(+new Date / 1000); + if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) return; + setPref("versionLastCheck", now), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => { + setPref("versionLatest", json.tag_name.substring(1)), setPref("versionCurrent", SCRIPT_VERSION); + }), Translations.updateTranslations(currentVersion === SCRIPT_VERSION); +} +function disablePwa() { + if (!(window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase()) return; + if (!!AppInterface || UserAgent.isSafariMobile()) Object.defineProperty(window.navigator, "standalone", { + value: !0 + }); +} +function hashCode(str) { + let hash = 0; + for (let i = 0, len = str.length;i < len; i++) { + let chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr, hash |= 0; + } + return hash; +} +function renderString(str, obj) { + return str.replace(/\$\{.+?\}/g, (match) => { + let 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(); +} +function parseDetailsPath(path) { + let matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(path); + if (!matches?.groups) return; + let titleSlug = matches.groups.titleSlug.replaceAll("|", "-"), productId = matches.groups.productId; + return { titleSlug, productId }; +} +function clearAllData() { + for (let i = 0;i < localStorage.length; i++) { + let key = localStorage.key(i); + if (!key) continue; + if (key.startsWith("BetterXcloud") || key.startsWith("better_xcloud")) localStorage.removeItem(key); + } + try { + indexedDB.deleteDatabase(LocalDb.DB_NAME); + } catch (e) {} + alert(t("clear-data-success")); +} +class SoundShortcut { + static adjustGainNodeVolume(amount) { + if (!getPref("audioEnableVolumeControl")) return 0; + let currentValue = getPref("audioVolume"), 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("audioVolume", 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("audioEnableVolumeControl") && STATES.currentStream.audioGainNode) { + let gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audioVolume"), targetValue; + if (settingValue === 0) targetValue = 100, setPref("audioVolume", 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 = document.querySelector("div[data-testid=media-container] audio") ?? document.querySelector("div[data-testid=media-container] video"); + if ($media) { + $media.muted = !$media.muted; + let status = $media.muted ? t("muted") : t("unmuted"); + Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { + speakerState: $media.muted ? 1 : 0 + }); + } + } +} +class StreamUiShortcut { + static showHideStreamMenu() { + window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu(); + } +} class StreamStatsCollector { static instance; static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector); @@ -994,7 +2050,7 @@ class StreamStatsCollector { fps: { current: 0, toString() { - let maxFps = getPref("video_max_fps"); + let maxFps = getPref("videoMaxFps"); return maxFps < 60 ? `${maxFps}/${this.current}` : this.current.toString(); } }, @@ -1079,29 +2135,29 @@ class StreamStatsCollector { if (!stats) return; stats.forEach((stat) => { if (stat.type === "inbound-rtp" && stat.kind === "video") { - let fps = this.currentStats.fps; + let fps = this.currentStats["fps"]; fps.current = stat.framesPerSecond || 0; - let pl = this.currentStats.pl; + let pl = this.currentStats["pl"]; pl.dropped = Math.max(0, stat.packetsLost), pl.received = stat.packetsReceived; - let fl = this.currentStats.fl; + let fl = this.currentStats["fl"]; if (fl.dropped = stat.framesDropped, fl.received = stat.framesReceived, !this.lastVideoStat) { this.lastVideoStat = stat; return; } - let lastStat = this.lastVideoStat, jit = this.currentStats.jit, bufferDelayDiff = stat.jitterBufferDelay - lastStat.jitterBufferDelay, emittedCountDiff = stat.jitterBufferEmittedCount - lastStat.jitterBufferEmittedCount; + let lastStat = this.lastVideoStat, jit = this.currentStats["jit"], bufferDelayDiff = stat.jitterBufferDelay - lastStat.jitterBufferDelay, emittedCountDiff = stat.jitterBufferEmittedCount - lastStat.jitterBufferEmittedCount; if (emittedCountDiff > 0) jit.current = bufferDelayDiff / emittedCountDiff * 1000; - let btr = this.currentStats.btr, timeDiff = stat.timestamp - lastStat.timestamp; + let btr = this.currentStats["btr"], timeDiff = stat.timestamp - lastStat.timestamp; btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000; - let dt = this.currentStats.dt; + let dt = this.currentStats["dt"]; dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime; let framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded; dt.current = dt.total / framesDecodedDiff * 1000, this.lastVideoStat = stat; } else if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") { - let ping = this.currentStats.ping; + let ping = this.currentStats["ping"]; ping.current = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1; - let dl = this.currentStats.dl; + let dl = this.currentStats["dl"]; dl.total = stat.bytesReceived; - let ul = this.currentStats.ul; + let ul = this.currentStats["ul"]; ul.total = stat.bytesSent; } }); @@ -1110,20 +2166,20 @@ class StreamStatsCollector { let bm = await navigator.getBattery(); isCharging = bm.charging, batteryLevel = Math.round(bm.level * 100); } catch (e) {} - let battery = this.currentStats.batt; + let battery = this.currentStats["batt"]; battery.current = batteryLevel, battery.isCharging = isCharging; - let playTime = this.currentStats.play, now = +new Date; + let playTime = this.currentStats["play"], now = +new Date; playTime.seconds = Math.ceil((now - playTime.startTime) / 1000); } getStat(kind) { return this.currentStats[kind]; } reset() { - let playTime = this.currentStats.play; + let playTime = this.currentStats["play"]; playTime.seconds = 0, playTime.startTime = +new Date; try { STATES.browser.capabilities.batteryApi && navigator.getBattery().then((bm) => { - this.currentStats.batt.start = Math.round(bm.level * 100); + this.currentStats["batt"].start = Math.round(bm.level * 100); }); } catch (e) {} } @@ -1133,771 +2189,6 @@ class StreamStatsCollector { }); } } -function getSupportedCodecProfiles() { - let options = { - default: t("default") - }; - if (!("getCapabilities" in RTCRtpReceiver)) return options; - let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1, codecs = RTCRtpReceiver.getCapabilities("video").codecs; - for (let codec of codecs) { - if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue; - let fmtp = codec.sdpFmtpLine.toLowerCase(); - if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0; - else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0; - else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0; - } - if (hasLowCodec) if (!hasNormalCodec && !hasHighCodec) options.default = `${t("visual-quality-low")} (${t("default")})`; - else options.low = t("visual-quality-low"); - if (hasNormalCodec) if (!hasLowCodec && !hasHighCodec) options.default = `${t("visual-quality-normal")} (${t("default")})`; - else options.normal = t("visual-quality-normal"); - if (hasHighCodec) if (!hasLowCodec && !hasNormalCodec) options.default = `${t("visual-quality-high")} (${t("default")})`; - else options.high = t("visual-quality-high"); - return options; -} -class GlobalSettingsStorage extends 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"), - note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")), - 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": "العربية", - "bg-BG": "Български", - "cs-CZ": "čeština", - "da-DK": "dansk", - "de-DE": "Deutsch", - "el-GR": "Ελληνικά", - "en-GB": "English (UK)", - "en-US": "English (US)", - "es-ES": "español (España)", - "es-MX": "español (Latinoamérica)", - "fi-FI": "suomi", - "fr-FR": "français", - "he-IL": "עברית", - "hu-HU": "magyar", - "it-IT": "italiano", - "ja-JP": "日本語", - "ko-KR": "한국어", - "nb-NO": "norsk bokmål", - "nl-NL": "Nederlands", - "pl-PL": "polski", - "pt-BR": "português (Brasil)", - "pt-PT": "português (Portugal)", - "ro-RO": "Română", - "ru-RU": "русский", - "sk-SK": "slovenčina", - "sv-SE": "svenska", - "th-TH": "ไทย", - "tr-TR": "Türkçe", - "zh-CN": "中文(简体)", - "zh-TW": "中文 (繁體)" - } - }, - stream_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) => { - let options = setting.options, keys = Object.keys(options); - if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature"); - setting.suggest = { - lowest: keys.length === 1 ? keys[0] : keys[1], - highest: keys[keys.length - 1] - }; - } - }, - 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_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 - } - }, - controller_polling_rate: { - requiredVariants: "full", - label: t("polling-rate"), - type: "number-stepper", - default: 4, - min: 4, - max: 60, - steps: 4, - params: { - exactTicks: 20, - reverse: !0, - customTextValue(value) { - value = parseInt(value); - let text = +(1000 / value).toFixed(2) + " Hz"; - if (value === 4) text = `${text} (${t("default")})`; - return text; - } - } - }, - mkb_enabled: { - requiredVariants: "full", - label: t("enable-mkb"), - default: !1, - unsupported: !STATES.userAgent.capabilities.mkb, - ready: (setting) => { - let note, url; - if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657"; - else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer"; - setting.unsupportedNote = () => CE("a", { - href: url, - target: "_blank" - }, "⚠️ " + note); - } - }, - native_mkb_enabled: { - requiredVariants: "full", - label: t("native-mkb"), - default: "default", - options: { - default: t("default"), - on: t("on"), - off: t("off") - }, - ready: (setting) => { - if (AppInterface) ; - else if (UserAgent.isMobile()) setting.unsupported = !0, setting.default = "off", delete setting.options.default, delete setting.options.on; - else delete setting.options.on; - } - }, - native_mkb_scroll_x_sensitivity: { - requiredVariants: "full", - label: t("horizontal-scroll-sensitivity"), - type: "number-stepper", - default: 0, - min: 0, - max: 1e4, - steps: 10, - params: { - exactTicks: 2000, - customTextValue: (value) => { - if (!value) return t("default"); - return (value / 100).toFixed(1) + "x"; - } - } - }, - native_mkb_scroll_y_sensitivity: { - requiredVariants: "full", - label: t("vertical-scroll-sensitivity"), - type: "number-stepper", - default: 0, - min: 0, - max: 1e4, - steps: 10, - params: { - exactTicks: 2000, - customTextValue: (value) => { - if (!value) return t("default"); - return (value / 100).toFixed(1) + "x"; - } - } - }, - mkb_default_preset_id: { - requiredVariants: "full", - default: 0 - }, - mkb_absolute_mouse: { - requiredVariants: "full", - default: !1 - }, - reduce_animations: { - label: t("reduce-animations"), - default: !1 - }, - ui_loading_screen_game_art: { - requiredVariants: "full", - label: t("show-game-art"), - default: !0 - }, - ui_loading_screen_wait_time: { - label: t("show-wait-time"), - default: !0 - }, - ui_loading_screen_rocket: { - label: t("rocket-animation"), - default: "show", - options: { - show: t("rocket-always-show"), - "hide-queue": t("rocket-hide-queue"), - hide: t("rocket-always-hide") - } - }, - ui_controller_friendly: { - label: t("controller-friendly-ui"), - default: BX_FLAGS.DeviceInfo.deviceType !== "unknown" - }, - ui_layout: { - requiredVariants: "full", - label: t("layout"), - default: "default", - options: { - default: t("default"), - normal: t("normal"), - tv: t("smart-tv") - } - }, - ui_scrollbar_hide: { - label: t("hide-scrollbar"), - default: !1 - }, - ui_hide_sections: { - requiredVariants: "full", - label: t("hide-sections"), - default: [], - multipleOptions: { - news: t("section-news"), - friends: t("section-play-with-friends"), - "native-mkb": t("section-native-mkb"), - touch: t("section-touch"), - "most-popular": t("section-most-popular"), - "all-games": t("section-all-games") - }, - params: { - size: 6 - } - }, - ui_game_card_show_wait_time: { - requiredVariants: "full", - label: t("show-wait-time-in-game-card"), - default: !1 - }, - block_social_features: { - label: t("disable-social-features"), - default: !1 - }, - block_tracking: { - label: t("disable-xcloud-analytics"), - default: !1 - }, - user_agent_profile: { - label: t("user-agent-profile"), - note: "⚠️ " + t("unexpected-behavior"), - default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default", - options: { - default: t("default"), - "windows-edge": "Edge + Windows", - "macos-safari": "Safari + macOS", - "vr-oculus": "Android TV", - "smarttv-generic": "Smart TV", - "smarttv-tizen": "Samsung Smart TV", - custom: t("custom") - } - }, - video_player_type: { - label: t("renderer"), - default: "default", - options: { - default: t("default"), - webgl2: t("webgl2") - }, - suggest: { - lowest: "default", - highest: "webgl2" - } - }, - video_processing: { - label: t("clarity-boost"), - default: "usm", - options: { - usm: t("unsharp-masking"), - cas: t("amd-fidelity-cas") - }, - suggest: { - lowest: "usm", - highest: "cas" - } - }, - video_power_preference: { - label: t("renderer-configuration"), - default: "default", - options: { - default: t("default"), - "low-power": t("battery-saving"), - "high-performance": t("high-performance") - }, - suggest: { - highest: "low-power" - } - }, - video_max_fps: { - label: t("max-fps"), - type: "number-stepper", - default: 60, - min: 10, - max: 60, - steps: 10, - params: { - exactTicks: 10, - customTextValue: (value) => { - return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps"; - } - } - }, - video_sharpness: { - label: t("sharpness"), - type: "number-stepper", - default: 0, - min: 0, - max: 10, - params: { - exactTicks: 2, - customTextValue: (value) => { - return value = parseInt(value), value === 0 ? t("off") : value.toString(); - } - }, - suggest: { - lowest: 0, - highest: 2 - } - }, - video_ratio: { - label: t("aspect-ratio"), - note: t("aspect-ratio-note"), - default: "16:9", - options: { - "16:9": "16:9", - "18:9": "18:9", - "21:9": "21:9", - "16:10": "16:10", - "4:3": "4:3", - fill: t("stretch") - } - }, - video_saturation: { - label: t("saturation"), - type: "number-stepper", - default: 100, - min: 50, - max: 150, - params: { - suffix: "%", - ticks: 25 - } - }, - video_contrast: { - label: t("contrast"), - type: "number-stepper", - default: 100, - min: 50, - max: 150, - params: { - suffix: "%", - ticks: 25 - } - }, - video_brightness: { - label: t("brightness"), - type: "number-stepper", - default: 100, - min: 50, - max: 150, - params: { - suffix: "%", - ticks: 25 - } - }, - audio_mic_on_playing: { - label: t("enable-mic-on-startup"), - default: !1 - }, - audio_enable_volume_control: { - requiredVariants: "full", - label: t("enable-volume-control"), - default: !1 - }, - audio_volume: { - label: t("volume"), - type: "number-stepper", - default: 100, - min: 0, - max: 600, - steps: 10, - params: { - suffix: "%", - ticks: 100 - } - }, - stats_items: { - label: t("stats"), - default: ["ping", "fps", "btr", "dt", "pl", "fl"], - multipleOptions: { - time: `TIME: ${t("clock")}`, - play: `PLAY: ${t("playtime")}`, - batt: `BATT: ${t("battery")}`, - ping: `PING: ${t("stat-ping")}`, - jit: `JIT: ${t("jitter")}`, - fps: `FPS: ${t("stat-fps")}`, - btr: `BTR: ${t("stat-bitrate")}`, - dt: `DT: ${t("stat-decode-time")}`, - pl: `PL: ${t("stat-packets-lost")}`, - fl: `FL: ${t("stat-frames-lost")}`, - dl: `DL: ${t("downloaded")}`, - ul: `UL: ${t("uploaded")}` - }, - params: { - size: 6 - }, - ready: (setting) => { - let multipleOptions = setting.multipleOptions; - if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"]; - } - }, - stats_show_when_playing: { - label: t("show-stats-on-startup"), - default: !1 - }, - stats_quick_glance: { - label: "👀 " + t("enable-quick-glance-mode"), - default: !0 - }, - stats_position: { - label: t("position"), - default: "top-right", - options: { - "top-left": t("top-left"), - "top-center": t("top-center"), - "top-right": t("top-right") - } - }, - stats_text_size: { - label: t("text-size"), - default: "0.9rem", - options: { - "0.9rem": t("small"), - "1.0rem": t("normal"), - "1.1rem": t("large") - } - }, - stats_transparent: { - label: t("transparent-background"), - default: !1 - }, - stats_opacity: { - label: t("opacity"), - type: "number-stepper", - default: 80, - min: 50, - max: 100, - steps: 10, - params: { - suffix: "%", - ticks: 10 - } - }, - stats_conditional_formatting: { - label: t("conditional-formatting"), - default: !1 - }, - xhome_enabled: { - requiredVariants: "full", - label: t("enable-remote-play-feature"), - default: !1 - }, - xhome_resolution: { - requiredVariants: "full", - default: "1080p", - options: { - "1080p": "1080p", - "720p": "720p" - } - }, - game_fortnite_force_console: { - requiredVariants: "full", - label: "🎮 " + t("fortnite-force-console-version"), - default: !1, - note: t("fortnite-allow-stw-mode") - }, - game_msfs2020_force_native_mkb: { - requiredVariants: "full", - label: "✈️ " + t("msfs2020-force-native-mkb"), - default: !1, - note: t("may-not-work-properly") - } - }; - constructor() { - super("better_xcloud", GlobalSettingsStorage.DEFINITIONS); - } -} -var globalSettings = new GlobalSettingsStorage, getPrefDefinition = globalSettings.getDefinition.bind(globalSettings), getPref = globalSettings.getSetting.bind(globalSettings), setPref = globalSettings.setSetting.bind(globalSettings); -STORAGE.Global = globalSettings; -class ScreenshotManager { - static instance; - static getInstance = () => ScreenshotManager.instance ?? (ScreenshotManager.instance = new ScreenshotManager); - LOG_TAG = "ScreenshotManager"; - $download; - $canvas; - canvasContext; - constructor() { - BxLogger.info(this.LOG_TAG, "constructor()"), this.$download = CE("a"), this.$canvas = CE("canvas", { class: "bx-gone" }), this.canvasContext = this.$canvas.getContext("2d", { - alpha: !1, - willReadFrequently: !1 - }); - } - updateCanvasSize(width, height) { - this.$canvas.width = width, this.$canvas.height = height; - } - updateCanvasFilters(filters) { - this.canvasContext.filter = filters; - } - onAnimationEnd(e) { - e.target.classList.remove("bx-taking-screenshot"); - } - takeScreenshot(callback) { - let currentStream = STATES.currentStream, streamPlayer = currentStream.streamPlayer, $canvas = this.$canvas; - if (!streamPlayer || !$canvas) return; - let $player; - if (getPref("screenshot_apply_filters")) $player = streamPlayer.getPlayerElement(); - else $player = streamPlayer.getPlayerElement("default"); - if (!$player || !$player.isConnected) return; - $player.parentElement.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $player.parentElement.classList.add("bx-taking-screenshot"); - let canvasContext = this.canvasContext; - if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().forceDrawFrame(); - if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) { - let data = $canvas.toDataURL("image/png").split(";base64,")[1]; - AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback(); - return; - } - $canvas.toBlob((blob) => { - if (!blob) return; - let now = +new Date, $download = this.$download; - $download.download = `${currentStream.titleSlug}-${now}.png`, $download.href = URL.createObjectURL(blob), $download.click(), URL.revokeObjectURL($download.href), $download.href = "", $download.download = "", canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback(); - }, "image/png"); - } -} -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 StreamStats { static instance; static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats); @@ -1961,7 +2252,7 @@ class StreamStats { } async start(glancing = !1) { if (!this.isHidden() || glancing && this.isGlancing()) return; - this.intervalId && clearInterval(this.intervalId), await this.update(!0), this.$container.classList.remove("bx-gone"), this.$container.dataset.display = glancing ? "glancing" : "fixed", this.intervalId = window.setInterval(this.update.bind(this), this.REFRESH_INTERVAL); + this.intervalId && clearInterval(this.intervalId), await this.update(!0), this.$container.classList.remove("bx-gone"), this.$container.dataset.display = glancing ? "glancing" : "fixed", this.intervalId = window.setInterval(this.update, this.REFRESH_INTERVAL); } async stop(glancing = !1) { if (glancing && !this.isGlancing()) return; @@ -1996,12 +2287,12 @@ class StreamStats { quickGlanceStop() { this.quickGlanceObserver && this.quickGlanceObserver.disconnect(), this.quickGlanceObserver = null; } - async update(forceUpdate = !1) { + update = async (forceUpdate = !1) => { if (!forceUpdate && this.isHidden() || !STATES.currentStream.peerConnection) { this.destroy(); return; } - let PREF_STATS_CONDITIONAL_FORMATTING = getPref("stats_conditional_formatting"), grade = "", statsCollector = StreamStatsCollector.getInstance(); + let PREF_STATS_CONDITIONAL_FORMATTING = getPref("statsConditionalFormatting"), grade = "", statsCollector = StreamStatsCollector.getInstance(); await statsCollector.collect(); let statKey; for (statKey in this.stats) { @@ -2010,13 +2301,13 @@ class StreamStats { if ($element.textContent = value.toString(), PREF_STATS_CONDITIONAL_FORMATTING && "grades" in value) grade = statsCollector.calculateGrade(value.current, value.grades); if ($element.dataset.grade !== grade) $element.dataset.grade = grade; } - } + }; refreshStyles() { - let PREF_ITEMS = getPref("stats_items"), $container = this.$container; - $container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getPref("stats_position"), $container.dataset.transparent = getPref("stats_transparent"), $container.style.opacity = getPref("stats_opacity") + "%", $container.style.fontSize = getPref("stats_text_size"); + let PREF_ITEMS = getPref("statsItems"), $container = this.$container; + $container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getPref("statsPosition"), $container.dataset.transparent = getPref("statsTransparent"), $container.style.opacity = getPref("statsOpacity") + "%", $container.style.fontSize = getPref("statsTextSize"); } hideSettingsUi() { - if (this.isGlancing() && !getPref("stats_quick_glance")) this.stop(); + if (this.isGlancing() && !getPref("statsQuickGlance")) this.stop(); } async render() { this.$container = CE("div", { class: "bx-stats-bar bx-gone" }); @@ -2032,7 +2323,7 @@ class StreamStats { } static setupEvents() { window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { - let PREF_STATS_QUICK_GLANCE = getPref("stats_quick_glance"), PREF_STATS_SHOW_WHEN_PLAYING = getPref("stats_show_when_playing"), streamStats = StreamStats.getInstance(); + let PREF_STATS_QUICK_GLANCE = getPref("statsQuickGlance"), PREF_STATS_SHOW_WHEN_PLAYING = getPref("statsShowWhenPlaying"), 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); }); @@ -2041,247 +2332,12 @@ class StreamStats { StreamStats.getInstance().refreshStyles(); } } -class Toast { - static instance; - static getInstance = () => Toast.instance ?? (Toast.instance = new Toast); - LOG_TAG = "Toast"; - $wrapper; - $msg; - $status; - stack = []; - isShowing = !1; - timeoutId; - DURATION = 3000; - constructor() { - BxLogger.info(this.LOG_TAG, "constructor()"), this.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, this.$msg = CE("span", { class: "bx-toast-msg" }), this.$status = CE("span", { class: "bx-toast-status" })), this.$wrapper.addEventListener("transitionend", (e) => { - let classList = this.$wrapper.classList; - if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), this.showNext(); - }), document.documentElement.appendChild(this.$wrapper); - } - show(msg, status, options = {}) { - options = options || {}; - let args = Array.from(arguments); - if (options.instant) this.stack = [args], this.showNext(); - else this.stack.push(args), !this.isShowing && this.showNext(); - } - showNext() { - if (!this.stack.length) { - this.isShowing = !1; - return; - } - this.isShowing = !0, this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide.bind(this), this.DURATION); - let [msg, status, options] = this.stack.shift(); - if (options && options.html) this.$msg.innerHTML = msg; - else this.$msg.textContent = msg; - if (status) this.$status.classList.remove("bx-gone"), this.$status.textContent = status; - else this.$status.classList.add("bx-gone"); - let classList = this.$wrapper.classList; - classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); - } - hide() { - this.timeoutId = null; - let classList = this.$wrapper.classList; - classList.remove("bx-show"), classList.add("bx-hide"); - } - static show(msg, status, options = {}) { - Toast.getInstance().show(msg, status, options); - } - static showNext() { - Toast.getInstance().showNext(); - } -} -class MicrophoneShortcut { - static toggle(showToast = !0) { - if (!window.BX_EXPOSED.streamSession) return !1; - let enableMic = window.BX_EXPOSED.streamSession._microphoneState === "Enabled" ? !1 : !0; - try { - return window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic), showToast && Toast.show(t("microphone"), t(enableMic ? "unmuted" : "muted"), { instant: !0 }), enableMic; - } catch (e) { - console.log(e); - } - return !1; - } -} -class StreamUiShortcut { - static showHideStreamMenu() { - window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu(); - } -} -function checkForUpdate() { - if (SCRIPT_VERSION.includes("beta")) return; - let CHECK_INTERVAL_SECONDS = 7200, currentVersion = getPref("version_current"), lastCheck = getPref("version_last_check"), now = Math.round(+new Date / 1000); - if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) return; - setPref("version_last_check", now), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => { - setPref("version_latest", json.tag_name.substring(1)), setPref("version_current", SCRIPT_VERSION); - }), Translations.updateTranslations(currentVersion === SCRIPT_VERSION); -} -function disablePwa() { - if (!(window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase()) return; - if (!!AppInterface || UserAgent.isSafariMobile()) Object.defineProperty(window.navigator, "standalone", { - value: !0 - }); -} -function hashCode(str) { - let hash = 0; - for (let i = 0, len = str.length;i < len; i++) { - let chr = str.charCodeAt(i); - hash = (hash << 5) - hash + chr, hash |= 0; - } - return hash; -} -function renderString(str, obj) { - return str.replace(/\$\{.+?\}/g, (match) => { - let 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(); -} -function parseDetailsPath(path) { - let matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(path); - if (!matches?.groups) return; - let titleSlug = matches.groups.titleSlug.replaceAll("|", "-"), productId = matches.groups.productId; - return { titleSlug, productId }; -} -class SoundShortcut { - static adjustGainNodeVolume(amount) { - if (!getPref("audio_enable_volume_control")) return 0; - let currentValue = getPref("audio_volume"), nearestValue; - if (amount > 0) nearestValue = ceilToNearest(currentValue, amount); - else nearestValue = floorToNearest(currentValue, -1 * amount); - let newValue; - if (currentValue !== nearestValue) newValue = nearestValue; - else newValue = currentValue + amount; - return newValue = setPref("audio_volume", newValue, !0), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue; - } - static setGainNodeVolume(value) { - STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); - } - static muteUnmute() { - if (getPref("audio_enable_volume_control") && STATES.currentStream.audioGainNode) { - let gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio_volume"), targetValue; - if (settingValue === 0) targetValue = 100, setPref("audio_volume", targetValue, !0); - else if (gainValue === 0) targetValue = settingValue; - else targetValue = 0; - let status; - if (targetValue === 0) status = t("muted"); - else status = targetValue + "%"; - SoundShortcut.setGainNodeVolume(targetValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { - speakerState: targetValue === 0 ? 1 : 0 - }); - return; - } - let $media = document.querySelector("div[data-testid=media-container] audio") ?? document.querySelector("div[data-testid=media-container] video"); - if ($media) { - $media.muted = !$media.muted; - let status = $media.muted ? t("muted") : t("unmuted"); - Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { - speakerState: $media.muted ? 1 : 0 - }); - } - } -} -class BxSelectElement { - static wrap($select) { - $select.removeAttribute("tabindex"); - let $btnPrev = createButton({ - label: "<", - style: 32 - }), $btnNext = createButton({ - label: ">", - style: 32 - }), isMultiple = $select.multiple, $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) => { - let $option = getOptionAtIndex(visibleIndex); - $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); - }); - else $content = CE("div", {}, $label = CE("label", { for: $select.id + "_checkbox" }, "")); - let getOptionAtIndex = (index) => { - return Array.from($select.querySelectorAll("option"))[index]; - }, render = (e) => { - if (e && e.manualTrigger) visibleIndex = $select.selectedIndex; - visibleIndex = normalizeIndex(visibleIndex); - let $option = getOptionAtIndex(visibleIndex), content = ""; - if ($option) if (content = $option.textContent || "", content && $option.parentElement.tagName === "OPTGROUP") { - $label.innerHTML = ""; - let 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); - let 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; - let goNext = e.target.closest("button") === $btnNext, currentIndex = visibleIndex, 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(); - let $div = CE("div", { - class: "bx-select", - _nearby: { - orientation: "horizontal", - focus: $btnNext - } - }, $select, $btnPrev, $content, $btnNext); - return Object.defineProperty($div, "value", { - get() { - return $select.value; - }, - set(value) { - $div.setValue(value); - } - }), $div.addEventListener = function() { - $select.addEventListener.apply($select, arguments); - }, $div.removeEventListener = function() { - $select.removeEventListener.apply($select, arguments); - }, $div.dispatchEvent = function() { - return $select.dispatchEvent.apply($select, arguments); - }, $div.setValue = (value) => { - if ("setValue" in $select) $select.setValue(value); - else $select.value = value; - }, $div; - } -} function onChangeVideoPlayerType() { - let playerType = getPref("video_player_type"), $videoProcessing = document.getElementById(`bx_setting_${"video_processing"}`), $videoSharpness = document.getElementById(`bx_setting_${"video_sharpness"}`), $videoPowerPreference = document.getElementById(`bx_setting_${"video_power_preference"}`), $videoMaxFps = document.getElementById(`bx_setting_${"video_max_fps"}`); + let playerType = getPref("videoPlayerType"), $videoProcessing = document.getElementById(`bx_setting_${"videoProcessing"}`), $videoSharpness = document.getElementById(`bx_setting_${"videoSharpness"}`), $videoPowerPreference = document.getElementById(`bx_setting_${"videoPowerPreference"}`), $videoMaxFps = document.getElementById(`bx_setting_${"videoMaxFps"}`); if (!$videoProcessing) return; let isDisabled = !1, $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); - else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; + else if ($videoProcessing.value = "usm", setPref("videoProcessing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; $videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), updateVideoPlayer(); } function limitVideoPlayerFps(targetFps) { @@ -2290,117 +2346,36 @@ function limitVideoPlayerFps(targetFps) { function updateVideoPlayer() { let streamPlayer = STATES.currentStream.streamPlayer; if (!streamPlayer) return; - limitVideoPlayerFps(getPref("video_max_fps")); + limitVideoPlayerFps(getPref("videoMaxFps")); let options = { - processing: getPref("video_processing"), - sharpness: getPref("video_sharpness"), - saturation: getPref("video_saturation"), - contrast: getPref("video_contrast"), - brightness: getPref("video_brightness") + processing: getPref("videoProcessing"), + sharpness: getPref("videoSharpness"), + saturation: getPref("videoSaturation"), + contrast: getPref("videoContrast"), + brightness: getPref("videoBrightness") }; - streamPlayer.setPlayerType(getPref("video_player_type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); + streamPlayer.setPlayerType(getPref("videoPlayerType")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); } window.addEventListener("resize", updateVideoPlayer); -class MkbPreset { - static MOUSE_SETTINGS = { - map_to: { - label: t("map-mouse-to"), - type: "options", - default: MouseMapTo[2], - options: { - [MouseMapTo[2]]: t("right-stick"), - [MouseMapTo[1]]: t("left-stick"), - [MouseMapTo[0]]: t("off") - } - }, - sensitivity_y: { - label: t("horizontal-sensitivity"), - type: "number-stepper", - default: 50, - min: 1, - max: 300, - params: { - suffix: "%", - exactTicks: 50 - } - }, - sensitivity_x: { - label: t("vertical-sensitivity"), - type: "number-stepper", - default: 50, - min: 1, - max: 300, - params: { - suffix: "%", - exactTicks: 50 - } - }, - deadzone_counterweight: { - label: t("deadzone-counterweight"), - type: "number-stepper", - default: 20, - min: 1, - max: 50, - params: { - suffix: "%", - exactTicks: 10 - } - } - }; - static DEFAULT_PRESET = { - mapping: { - 12: ["ArrowUp"], - 13: ["ArrowDown"], - 14: ["ArrowLeft"], - 15: ["ArrowRight"], - 100: ["KeyW"], - 101: ["KeyS"], - 102: ["KeyA"], - 103: ["KeyD"], - 200: ["KeyI"], - 201: ["KeyK"], - 202: ["KeyJ"], - 203: ["KeyL"], - 0: ["Space", "KeyE"], - 2: ["KeyR"], - 1: ["ControlLeft", "Backspace"], - 3: ["KeyV"], - 9: ["Enter"], - 8: ["Tab"], - 4: ["KeyC", "KeyG"], - 5: ["KeyQ"], - 16: ["Backquote"], - 7: ["Mouse0"], - 6: ["Mouse2"], - 10: ["ShiftLeft"], - 11: ["KeyF"] - }, - mouse: { - map_to: MouseMapTo[2], - sensitivity_x: 100, - sensitivity_y: 100, - deadzone_counterweight: 20 - } - }; - static convert(preset) { - let 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); - let mouse = obj.mouse; - mouse["sensitivity_x"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["sensitivity_y"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["deadzone_counterweight"] *= EmulatedMkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT; - let mouseMapTo = MouseMapTo[mouse["map_to"]]; - if (typeof mouseMapTo !== "undefined") mouse["map_to"] = mouseMapTo; - else mouse["map_to"] = MkbPreset.MOUSE_SETTINGS["map_to"].default; - return obj; - } -} class KeyHelper { - static #NON_PRINTABLE_KEYS = { + static NON_PRINTABLE_KEYS = { Backquote: "`", + Minus: "-", + Equal: "=", + BracketLeft: "[", + BracketRight: "]", + Backslash: "\\", + Semicolon: ";", + Quote: "'", + Comma: ",", + Period: ".", + Slash: "/", + NumpadMultiply: "Numpad *", + NumpadAdd: "Numpad +", + NumpadSubtract: "Numpad -", + NumpadDecimal: "Numpad .", + NumpadDivide: "Numpad /", + NumpadEqual: "Numpad =", Mouse0: "Left Click", Mouse2: "Right Click", Mouse1: "Middle Click", @@ -2410,19 +2385,43 @@ class KeyHelper { ScrollRight: "Scroll Right" }; static getKeyFromEvent(e) { - let code, name; - if (e instanceof KeyboardEvent) code = e.code || e.key; + let code = null, modifiers; + if (e instanceof KeyboardEvent) code = e.code || e.key, modifiers = 0, modifiers ^= e.ctrlKey ? 1 : 0, modifiers ^= e.shiftKey ? 2 : 0, modifiers ^= e.altKey ? 4 : 0; else if (e instanceof WheelEvent) { if (e.deltaY < 0) code = "ScrollUp"; else if (e.deltaY > 0) code = "ScrollDown"; else if (e.deltaX < 0) code = "ScrollLeft"; else if (e.deltaX > 0) code = "ScrollRight"; } else if (e instanceof MouseEvent) code = "Mouse" + e.button; - if (code) name = KeyHelper.codeToKeyName(code); - return code ? { code, name } : null; + if (code) { + let results = { code }; + if (modifiers) results.modifiers = modifiers; + return results; + } + return null; } - static codeToKeyName(code) { - return KeyHelper.#NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code; + static getFullKeyCodeFromEvent(e) { + let key = KeyHelper.getKeyFromEvent(e); + return key ? `${key.code}:${key.modifiers || 0}` : ""; + } + static parseFullKeyCode(str) { + if (!str) return null; + let tmp = str.split(":"), code = tmp[0], modifiers = parseInt(tmp[1]); + return { + code, + modifiers + }; + } + static codeToKeyName(key) { + let { code, modifiers } = key, text = [KeyHelper.NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code]; + if (modifiers && modifiers !== 0) { + if (!code.startsWith("Control") && !code.startsWith("Shift") && !code.startsWith("Alt")) { + if (modifiers & 2) text.unshift("Shift"); + if (modifiers & 4) text.unshift("Alt"); + if (modifiers & 1) text.unshift("Ctrl"); + } + } + return text.join(" + "); } } class PointerClient { @@ -2500,98 +2499,308 @@ class MouseDataProvider { constructor(handler) { this.mkbHandler = handler; } + init() {} + destroy() {} } class MkbHandler {} -class NativeMkbHandler extends MkbHandler { +class ControllerShortcutsTable extends BasePresetsTable { static instance; - static getInstance = () => NativeMkbHandler.instance ?? (NativeMkbHandler.instance = new NativeMkbHandler); - LOG_TAG = "NativeMkbHandler"; - #pointerClient; - #enabled = !1; - #mouseButtonsPressed = 0; - #mouseWheelX = 0; - #mouseWheelY = 0; - #mouseVerticalMultiply = 0; - #mouseHorizontalMultiply = 0; - #inputSink; - #$message; + static getInstance = () => ControllerShortcutsTable.instance ?? (ControllerShortcutsTable.instance = new ControllerShortcutsTable); + LOG_TAG = "ControllerShortcutsTable"; + TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_SHORTCUTS; + DEFAULT_PRESETS = { + [-1]: { + id: -1, + name: "Type A", + data: { + mapping: { + 3: AppInterface ? "device-volume-inc" : "stream-volume-inc", + 0: AppInterface ? "device-volume-dec" : "stream-volume-dec", + 2: "stream-stats-toggle", + 1: AppInterface ? "device-sound-toggle" : "stream-sound-toggle", + 5: "stream-screenshot-capture", + 9: "stream-menu-show" + } + } + }, + [-2]: { + id: -2, + name: "Type B", + data: { + mapping: { + 12: AppInterface ? "device-volume-inc" : "stream-volume-inc", + 13: AppInterface ? "device-volume-dec" : "stream-volume-dec", + 15: "stream-stats-toggle", + 14: AppInterface ? "device-sound-toggle" : "stream-sound-toggle", + 4: "stream-screenshot-capture", + 8: "stream-menu-show" + } + } + } + }; + DEFAULT_PRESET_ID = -1; constructor() { - super(); + super(LocalDb.TABLE_CONTROLLER_SHORTCUTS); BxLogger.info(this.LOG_TAG, "constructor()"); } - #onKeyboardEvent(e) { +} +class ControllerSettingsTable extends BaseLocalTable { + static instance; + static getInstance = () => ControllerSettingsTable.instance ?? (ControllerSettingsTable.instance = new ControllerSettingsTable(LocalDb.TABLE_CONTROLLER_SETTINGS)); + static DEFAULT_DATA = { + shortcutPresetId: -1, + vibrationIntensity: 50 + }; + async getControllerData(id) { + let setting = await this.get(id); + if (!setting) return deepClone(ControllerSettingsTable.DEFAULT_DATA); + return setting.data; + } + async getControllersData() { + let all = await this.getAll(), results = {}; + for (let key in all) { + let settings = all[key].data; + settings.vibrationIntensity /= 100, results[key] = settings; + } + return results; + } +} +function showGamepadToast(gamepad) { + if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; + BxLogger.info("Gamepad", gamepad); + let text = "🎮"; + if (getPref("localCoOpEnabled")) text += ` #${gamepad.index + 1}`; + let gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, ""); + text += ` - ${gamepadId}`; + let status; + if (gamepad.connected) status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status"); + else status = t("disconnected"); + Toast.show(text, status, { instant: !1 }); +} +function getUniqueGamepadNames() { + let gamepads = window.navigator.getGamepads(), names = []; + for (let gamepad of gamepads) + if (gamepad?.connected && gamepad.id !== VIRTUAL_GAMEPAD_ID) !names.includes(gamepad.id) && names.push(gamepad.id); + return names; +} +function hasGamepad() { + let gamepads = window.navigator.getGamepads(); + for (let gamepad of gamepads) + if (gamepad?.connected) return !0; + return !1; +} +class StreamSettings { + static settings = { + settings: {}, + xCloudPollingMode: "all", + deviceVibrationIntensity: 0, + controllerPollingRate: 4, + controllers: {}, + mkbPreset: null, + keyboardShortcuts: {} + }; + static getPref(key) { + return getPref(key); + } + static async refreshControllerSettings() { + let settings = StreamSettings.settings, controllers = {}, settingsTable = ControllerSettingsTable.getInstance(), shortcutsTable = ControllerShortcutsTable.getInstance(), gamepads = window.navigator.getGamepads(); + for (let gamepad of gamepads) { + if (!gamepad?.connected) continue; + if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; + let settingsData = await settingsTable.getControllerData(gamepad.id), shortcutsMapping, preset = await shortcutsTable.getPreset(settingsData.shortcutPresetId); + if (!preset) shortcutsMapping = null; + else shortcutsMapping = preset.data.mapping; + controllers[gamepad.id] = { + vibrationIntensity: settingsData.vibrationIntensity, + shortcuts: shortcutsMapping + }; + } + settings.controllers = controllers, settings.controllerPollingRate = StreamSettings.getPref("controllerPollingRate"), await StreamSettings.refreshDeviceVibration(); + } + static async refreshDeviceVibration() { + if (!STATES.browser.capabilities.deviceVibration) return; + let mode = StreamSettings.getPref("deviceVibrationMode"), intensity = 0; + if (mode === "on" || mode === "auto" && !hasGamepad()) intensity = StreamSettings.getPref("deviceVibrationIntensity") / 100; + StreamSettings.settings.deviceVibrationIntensity = intensity, BxEvent.dispatch(window, BxEvent.DEVICE_VIBRATION_CHANGED); + } + static async refreshMkbSettings() { + let settings = StreamSettings.settings, presetId = StreamSettings.getPref("mkbMappingPresetIdP1"), orgPreset = await MkbMappingPresetsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data, converted = { + mapping: {}, + mouse: Object.assign({}, orgPresetData.mouse) + }, key; + for (key in orgPresetData.mapping) { + let buttonIndex = parseInt(key); + if (!orgPresetData.mapping[buttonIndex]) continue; + for (let keyName of orgPresetData.mapping[buttonIndex]) + if (typeof keyName === "string") converted.mapping[keyName] = buttonIndex; + } + let mouse = converted.mouse; + mouse["sensitivityX"] *= 0.001, mouse["sensitivityY"] *= 0.001, mouse["deadzoneCounterweight"] *= 0.01, settings.mkbPreset = converted, setPref("mkbMappingPresetIdP1", orgPreset.id), BxEvent.dispatch(window, BxEvent.MKB_UPDATED); + } + static async refreshKeyboardShortcuts() { + let settings = StreamSettings.settings, presetId = StreamSettings.getPref("keyboardShortcutsInGamePresetId"), orgPreset = await KeyboardShortcutsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data.mapping, converted = {}, action; + for (action in orgPresetData) { + let info = orgPresetData[action], key = `${info.code}:${info.modifiers || 0}`; + converted[key] = action; + } + settings.keyboardShortcuts = converted, setPref("keyboardShortcutsInGamePresetId", orgPreset.id), BxEvent.dispatch(window, BxEvent.KEYBOARD_SHORTCUTS_UPDATED); + } + static async refreshAllSettings() { + window.BX_STREAM_SETTINGS = StreamSettings.settings, await StreamSettings.refreshControllerSettings(), await StreamSettings.refreshMkbSettings(), await StreamSettings.refreshKeyboardShortcuts(); + } + static findKeyboardShortcut(targetAction) { + let shortcuts = StreamSettings.settings.keyboardShortcuts; + for (let codeStr in shortcuts) + if (shortcuts[codeStr] === targetAction) return KeyHelper.parseFullKeyCode(codeStr); + return null; + } + static setup() { + let listener = () => { + StreamSettings.refreshControllerSettings(); + }; + window.addEventListener("gamepadconnected", listener), window.addEventListener("gamepaddisconnected", listener), StreamSettings.refreshAllSettings(); + } +} +class MkbPopup { + static instance; + static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup); + popupType; + $popup; + $title; + $btnActivate; + mkbHandler; + constructor() { + this.render(), window.addEventListener(BxEvent.KEYBOARD_SHORTCUTS_UPDATED, (e) => { + let $newButton = this.createActivateButton(); + this.$btnActivate.replaceWith($newButton), this.$btnActivate = $newButton; + }); + } + attachMkbHandler(handler) { + this.mkbHandler = handler, this.popupType = handler instanceof NativeMkbHandler ? "native" : "virtual", this.$popup.dataset.type = this.popupType, this.$title.innerText = t(this.popupType === "native" ? "native-mkb" : "virtual-controller"); + } + toggleVisibility(show) { + this.$popup.classList.toggle("bx-gone", !show), show && this.moveOffscreen(!1); + } + moveOffscreen(doMove) { + this.$popup.classList.toggle("bx-offscreen", doMove); + } + createActivateButton() { + let options = { + style: 1 | 512 | 128, + label: t("activate"), + onClick: this.onActivate + }, shortcutKey = StreamSettings.findKeyboardShortcut("mkb-toggle"); + if (shortcutKey) options.secondaryText = t("press-key-to-toggle-mkb", { key: KeyHelper.codeToKeyName(shortcutKey) }); + return createButton(options); + } + onActivate = (e) => { + e.preventDefault(), this.mkbHandler.toggle(!0); + }; + render() { + this.$popup = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, this.$title = CE("p"), this.$btnActivate = this.createActivateButton(), CE("div", {}, createButton({ + label: t("ignore"), + style: 8, + onClick: (e) => { + e.preventDefault(), this.mkbHandler.toggle(!1), this.mkbHandler.waitForMouseData(!1); + } + }), createButton({ + label: t("manage"), + style: 64, + onClick: () => { + let dialog = SettingsDialog.getInstance(); + dialog.focusTab("mkb"), dialog.show(); + } + }))), document.documentElement.appendChild(this.$popup); + } + reset() { + this.toggleVisibility(!0), this.moveOffscreen(!1); + } +} +class NativeMkbHandler extends MkbHandler { + static instance; + static getInstance() { + if (typeof NativeMkbHandler.instance === "undefined") if (NativeMkbHandler.isAllowed()) NativeMkbHandler.instance = new NativeMkbHandler; + else NativeMkbHandler.instance = null; + return NativeMkbHandler.instance; + } + LOG_TAG = "NativeMkbHandler"; + static isAllowed = () => { + return STATES.browser.capabilities.emulatedNativeMkb && getPref("nativeMkbMode") === "on"; + }; + pointerClient; + enabled = !1; + mouseButtonsPressed = 0; + mouseWheelX = 0; + mouseWheelY = 0; + mouseVerticalMultiply = 0; + mouseHorizontalMultiply = 0; + inputSink; + popup; + constructor() { + super(); + BxLogger.info(this.LOG_TAG, "constructor()"), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this); + } + onKeyboardEvent(e) { if (e.type === "keyup" && e.code === "F8") { e.preventDefault(), this.toggle(); return; } } - #onPointerLockRequested(e) { + onPointerLockRequested(e) { AppInterface.requestPointerCapture(), this.start(); } - #onPointerLockExited(e) { + 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"); + onPollingModeChanged = (e) => { + let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none"; + this.popup.moveOffscreen(move); }; - #onDialogShown = () => { + 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); + this.onKeyboardEvent(event); break; case BxEvent.XCLOUD_DIALOG_SHOWN: - this.#onDialogShown(); + this.onDialogShown(); break; case BxEvent.POINTER_LOCK_REQUESTED: - this.#onPointerLockRequested(event); + this.onPointerLockRequested(event); break; case BxEvent.POINTER_LOCK_EXITED: - this.#onPointerLockExited(event); + this.onPointerLockExited(event); break; case BxEvent.XCLOUD_POLLING_MODE_CHANGED: - this.#onPollingModeChanged(event); + this.onPollingModeChanged(event); break; } } init() { - this.#pointerClient = PointerClient.getInstance(), this.#inputSink = window.BX_EXPOSED.inputSink, this.#updateInputConfigurationAsync(!1); + this.pointerClient = PointerClient.getInstance(), this.inputSink = window.BX_EXPOSED.inputSink, this.updateInputConfigurationAsync(!1); try { - this.#pointerClient.start(STATES.pointerServerPort, this); + 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"); + this.mouseVerticalMultiply = getPref("nativeMkbScrollYSensitivity"), this.mouseHorizontalMultiply = getPref("nativeMkbScrollXSensitivity"), 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); + let shortcutKey = StreamSettings.findKeyboardShortcut("mkb-toggle"); + if (shortcutKey) { + let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` }); + Toast.show(msg, t("native-mkb"), { html: !0 }); + } + this.waitForMouseData(!1); } toggle(force) { let setEnable; if (typeof force !== "undefined") setEnable = force; - else setEnable = !this.#enabled; + else setEnable = !this.enabled; if (setEnable) document.documentElement.requestPointerLock(); else document.exitPointerLock(); } - #updateInputConfigurationAsync(enabled) { + updateInputConfigurationAsync(enabled) { window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({ enableKeyboardInput: enabled, enableMouseInput: enabled, @@ -2600,62 +2809,64 @@ class NativeMkbHandler extends MkbHandler { }); } 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 }); + this.resetMouseInput(), this.enabled = !0, this.updateInputConfigurationAsync(!0), window.BX_EXPOSED.stopTakRendering = !0, this.waitForMouseData(!1), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 }); } stop() { - this.#resetMouseInput(), this.#enabled = !1, this.#updateInputConfigurationAsync(!1), this.#$message?.classList.remove("bx-gone"); + this.resetMouseInput(), this.enabled = !1, this.updateInputConfigurationAsync(!1), this.waitForMouseData(!0); } 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"); + 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.waitForMouseData(!1); } handleMouseMove(data) { - this.#sendMouseInput({ + this.sendMouseInput({ X: data.movementX, Y: data.movementY, - Buttons: this.#mouseButtonsPressed, - WheelX: this.#mouseWheelX, - WheelY: this.#mouseWheelY + Buttons: this.mouseButtonsPressed, + WheelX: this.mouseWheelX, + WheelY: this.mouseWheelY }); } handleMouseClick(data) { let { pointerButton, pressed } = data; - if (pressed) this.#mouseButtonsPressed |= pointerButton; - else this.#mouseButtonsPressed ^= pointerButton; - this.#mouseButtonsPressed = Math.max(0, this.#mouseButtonsPressed), this.#sendMouseInput({ + 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 + Buttons: this.mouseButtonsPressed, + WheelX: this.mouseWheelX, + WheelY: this.mouseWheelY }); } handleMouseWheel(data) { let { 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({ + 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 + Buttons: this.mouseButtonsPressed, + WheelX: this.mouseWheelX, + WheelY: this.mouseWheelY }), !0; } setVerticalScrollMultiplier(vertical) { - this.#mouseVerticalMultiply = vertical; + this.mouseVerticalMultiply = vertical; } setHorizontalScrollMultiplier(horizontal) { - this.#mouseHorizontalMultiply = horizontal; + this.mouseHorizontalMultiply = horizontal; + } + waitForMouseData(showPopup) { + this.popup.toggleVisibility(showPopup); } - waitForMouseData(enabled) {} isEnabled() { - return this.#enabled; + return this.enabled; } - #sendMouseInput(data) { - data.Type = 0, this.#inputSink?.onMouseInput(data); + sendMouseInput(data) { + data.Type = 0, this.inputSink?.onMouseInput(data); } - #resetMouseInput() { - this.#mouseButtonsPressed = 0, this.#mouseWheelX = 0, this.#mouseWheelY = 0, this.#sendMouseInput({ + resetMouseInput() { + this.mouseButtonsPressed = 0, this.mouseWheelX = 0, this.mouseWheelY = 0, this.sendMouseInput({ X: 0, Y: 0, Buttons: 0, @@ -2664,160 +2875,54 @@ class NativeMkbHandler extends MkbHandler { }); } } -class LocalDb { - static DB_NAME = "BetterXcloud"; - static DB_VERSION = 2; - db; - open() { - return new Promise((resolve, reject) => { - if (this.db) { - resolve(); - return; - } - let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION); - request.onupgradeneeded = this.onUpgradeNeeded.bind(this), 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) { - let table = this.db.transaction(name, type || "readonly").objectStore(name); - return new Promise((resolve) => resolve(table)); - } - call(method) { - let table = arguments[1]; - return new Promise((resolve) => { - let 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); - } -} -class MkbPresetsDb extends LocalDb { - static instance; - static getInstance = () => MkbPresetsDb.instance ?? (MkbPresetsDb.instance = new MkbPresetsDb); - LOG_TAG = "MkbPresetsDb"; - TABLE_PRESETS = "mkb_presets"; - constructor() { - super(); - BxLogger.info(this.LOG_TAG, "constructor()"); - } - createTable(db) { - db.createObjectStore(this.TABLE_PRESETS, { - keyPath: "id", - autoIncrement: !0 - }).createIndex("name_idx", "name"); - } - onUpgradeNeeded(e) { - let db = e.target.result; - if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined"); - if (!db.objectStoreNames.contains(this.TABLE_PRESETS)) this.createTable(db); - } - async presetsTable() { - return await this.open(), await this.table(this.TABLE_PRESETS, "readwrite"); - } - async newPreset(name, data) { - let table = await this.presetsTable(), [, id2] = await this.add(table, { name, data }); - return id2; - } - async updatePreset(preset) { - let table = await this.presetsTable(), [, id2] = await this.put(table, preset); - return id2; - } - async deletePreset(id2) { - let table = await this.presetsTable(); - return await this.delete(table, id2), id2; - } - async getPreset(id2) { - let table = await this.presetsTable(), [, preset] = await this.get(table, id2); - return preset; - } - async getPresets() { - let table = await this.presetsTable(), [, count] = await this.count(table); - if (count > 0) { - let [, items] = await this.getAll(table), presets = {}; - return items.forEach((item2) => presets[item2.id] = item2), presets; - } - let preset = { - name: t("default"), - data: MkbPreset.DEFAULT_PRESET - }, [, id2] = await this.add(table, preset); - return preset.id = id2, setPref("mkb_default_preset_id", id2), { - [id2]: preset - }; - } -} var PointerToMouseButton = { 1: 0, 2: 2, 4: 1 -}, VIRTUAL_GAMEPAD_ID = "Xbox 360 Controller"; +}, VIRTUAL_GAMEPAD_ID = "Better xCloud Virtual Controller"; class WebSocketMouseDataProvider extends MouseDataProvider { - #pointerClient; - #connected = !1; + pointerClient; + isConnected = !1; init() { - this.#pointerClient = PointerClient.getInstance(), this.#connected = !1; + this.pointerClient = PointerClient.getInstance(), this.isConnected = !1; try { - this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.#connected = !0; + this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.isConnected = !0; } catch (e) { Toast.show("Cannot enable Mouse & Keyboard feature"); } } start() { - this.#connected && AppInterface.requestPointerCapture(); + this.isConnected && AppInterface.requestPointerCapture(); } stop() { - this.#connected && AppInterface.releasePointerCapture(); + this.isConnected && AppInterface.releasePointerCapture(); } destroy() { - this.#connected && this.#pointerClient?.stop(); + this.isConnected && 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); + 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); + 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) => { + onMouseMoveEvent = (e) => { this.mkbHandler.handleMouseMove({ movementX: e.movementX, movementY: e.movementY }); }; - #onMouseEvent = (e) => { + onMouseEvent = (e) => { e.preventDefault(); - let isMouseDown = e.type === "mousedown", data = { + let data = { mouseButton: e.button, - pressed: isMouseDown + pressed: e.type === "mousedown" }; this.mkbHandler.handleMouseClick(data); }; - #onWheelEvent = (e) => { + onWheelEvent = (e) => { if (!KeyHelper.getKeyFromEvent(e)) return; let data = { vertical: e.deltaY, @@ -2825,19 +2930,23 @@ class PointerLockMouseDataProvider extends MouseDataProvider { }; if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); }; - #disableContextMenu = (e) => e.preventDefault(); + disableContextMenu = (e) => e.preventDefault(); } class EmulatedMkbHandler extends MkbHandler { static instance; - static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler); + static getInstance() { + if (typeof EmulatedMkbHandler.instance === "undefined") if (EmulatedMkbHandler.isAllowed()) EmulatedMkbHandler.instance = new EmulatedMkbHandler; + else EmulatedMkbHandler.instance = null; + return EmulatedMkbHandler.instance; + } static LOG_TAG = "EmulatedMkbHandler"; - #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); - static DEFAULT_PANNING_SENSITIVITY = 0.001; - static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; - static MAXIMUM_STICK_RANGE = 1.1; - #VIRTUAL_GAMEPAD = { + static isAllowed() { + return getPref("mkbEnabled") && (AppInterface || !UserAgent.isMobile()); + } + PRESET; + VIRTUAL_GAMEPAD = { id: VIRTUAL_GAMEPAD_ID, - index: 3, + index: 0, connected: !1, hapticActuators: null, mapping: "standard", @@ -2846,250 +2955,233 @@ class EmulatedMkbHandler extends MkbHandler { 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 = []; + nativeGetGamepads; + initialized = !1; + enabled = !1; + mouseDataProvider; + isPolling = !1; + prevWheelCode = null; + wheelStoppedTimeoutId = null; + detectMouseStoppedTimeoutId = null; + escKeyDownTime = -1; + LEFT_STICK_X = []; + LEFT_STICK_Y = []; + RIGHT_STICK_X = []; + RIGHT_STICK_Y = []; + popup; + STICK_MAP = { + 102: [this.LEFT_STICK_X, 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] + }; constructor() { super(); - BxLogger.info(EmulatedMkbHandler.LOG_TAG, "constructor()"), 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] - }; + BxLogger.info(EmulatedMkbHandler.LOG_TAG, "constructor()"), this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this); } - isEnabled = () => this.#enabled; - #patchedGetGamepads = () => { - let gamepads = this.#nativeGetGamepads() || []; - return gamepads[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD, gamepads; + isEnabled = () => this.enabled; + patchedGetGamepads = () => { + let gamepads = this.nativeGetGamepads() || []; + return gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD, gamepads; }; - #getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD; - #updateStick(stick, x, y) { - let virtualGamepad = this.#getVirtualGamepad(); + getVirtualGamepad = () => this.VIRTUAL_GAMEPAD; + updateStick(stick, x, y) { + let 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 = () => { - let gamepad = this.#getVirtualGamepad(); + vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); + resetGamepad() { + let 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) => { - let virtualGamepad = this.#getVirtualGamepad(); + } + pressButton(buttonIndex, pressed) { + let virtualGamepad = this.getVirtualGamepad(); if (buttonIndex >= 100) { - let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]; + 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]; + 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) => { + } + onKeyboardEvent = (e) => { let 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; + 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; - let buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]; + if (!this.isPolling || !this.PRESET) return; + if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return; + let buttonIndex = this.PRESET.mapping[e.code || e.key]; if (typeof buttonIndex === "undefined") return; if (e.repeat) return; - e.preventDefault(), this.#pressButton(buttonIndex, isKeyDown); + e.preventDefault(), this.pressButton(buttonIndex, isKeyDown); }; - #onMouseStopped = () => { - this.#detectMouseStoppedTimeout = null; - let analog = this.#CURRENT_PRESET_DATA.mouse["map_to"] === 1 ? 0 : 1; - this.#updateStick(analog, 0, 0); + onMouseStopped = () => { + if (this.detectMouseStoppedTimeoutId = null, !this.PRESET) return; + let analog = this.PRESET.mouse["mapTo"] === 1 ? 0 : 1; + this.updateStick(analog, 0, 0); }; - handleMouseClick = (data) => { + handleMouseClick(data) { let mouseButton; if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton; else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton]; - let keyCode = "Mouse" + mouseButton, key = { - code: keyCode, - name: KeyHelper.codeToKeyName(keyCode) + let key = { + code: "Mouse" + mouseButton }; - if (!key.name) return; - let buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + if (!this.PRESET) return; + let buttonIndex = this.PRESET.mapping[key.code]; if (typeof buttonIndex === "undefined") return; - this.#pressButton(buttonIndex, data.pressed); - }; - handleMouseMove = (data) => { - let mouseMapTo = this.#CURRENT_PRESET_DATA.mouse["map_to"]; + this.pressButton(buttonIndex, data.pressed); + } + handleMouseMove(data) { + let preset = this.PRESET; + if (!preset) return; + let mouseMapTo = preset.mouse["mapTo"]; if (mouseMapTo === 0) return; - this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout), this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50); - let deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse["deadzone_counterweight"], 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); + this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId), this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50); + let deadzoneCounterweight = preset.mouse["deadzoneCounterweight"], x = data.movementX * preset.mouse["sensitivityX"], y = data.movementY * preset.mouse["sensitivityY"], length = this.vectorLength(x, y); if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; - else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length, y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length; + else if (length > 1.1) x *= 1.1 / length, y *= 1.1 / length; let analog = mouseMapTo === 1 ? 0 : 1; - this.#updateStick(analog, x, y); - }; - handleMouseWheel = (data) => { + this.updateStick(analog, x, y); + } + handleMouseWheel(data) { let code = ""; if (data.vertical < 0) code = "ScrollUp"; else if (data.vertical > 0) code = "ScrollDown"; else if (data.horizontal < 0) code = "ScrollLeft"; else if (data.horizontal > 0) code = "ScrollRight"; if (!code) return !1; + if (!this.PRESET) return !1; let key = { - code, - name: KeyHelper.codeToKeyName(code) - }, buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + code + }, buttonIndex = this.PRESET.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); + if (this.prevWheelCode === null || this.prevWheelCode === key.code) this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId), this.pressButton(buttonIndex, !0); + return this.wheelStoppedTimeoutId = window.setTimeout(() => { + this.prevWheelCode = null, this.pressButton(buttonIndex, !1); }, 20), !0; - }; - toggle = (force) => { - if (typeof force !== "undefined") this.#enabled = force; - else this.#enabled = !this.#enabled; - if (this.#enabled) document.body.requestPointerLock(); + } + toggle(force) { + if (!this.initialized) return; + if (typeof force !== "undefined") this.enabled = force; + else this.enabled = !this.enabled; + if (this.enabled) document.body.requestPointerLock(); else document.pointerLockElement && document.exitPointerLock(); + } + refreshPresetData() { + this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset, this.resetGamepad(); + } + waitForMouseData(showPopup) { + this.popup.toggleVisibility(showPopup); + } + onPollingModeChanged = (e) => { + let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none"; + this.popup.moveOffscreen(move); }; - #getCurrentPreset = () => { - return new Promise((resolve) => { - let presetId = getPref("mkb_default_preset_id"); - MkbPresetsDb.getInstance().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 = () => { + 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(); - let dialog = SettingsNavigationDialog.getInstance(); - dialog.focusTab("mkb"), NavigationDialogManager.getInstance().show(dialog); - } - })))); - if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); - }; - #onPointerLockChange = () => { + onPointerLockChange = () => { if (document.pointerLockElement) this.start(); else this.stop(); }; - #onPointerLockError = (e) => { + onPointerLockError = (e) => { console.log(e), this.stop(); }; - #onPointerLockRequested = () => { + onPointerLockRequested = () => { this.start(); }; - #onPointerLockExited = () => { - this.#mouseDataProvider?.stop(); + onPointerLockExited = () => { + this.mouseDataProvider?.stop(); }; handleEvent(event) { switch (event.type) { case BxEvent.POINTER_LOCK_REQUESTED: - this.#onPointerLockRequested(); + this.onPointerLockRequested(); break; case BxEvent.POINTER_LOCK_EXITED: - this.#onPointerLockExited(); + 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); + init() { + if (!STATES.browser.capabilities.mkb) { + this.initialized = !1; + return; + } + if (this.initialized = !0, this.refreshPresetData(), this.enabled = !1, AppInterface) this.mouseDataProvider = new WebSocketMouseDataProvider(this); + else this.mouseDataProvider = new PointerLockMouseDataProvider(this); + if (this.mouseDataProvider.init(), window.addEventListener("keydown", this.onKeyboardEvent), window.addEventListener("keyup", this.onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), 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 (MkbPopup.getInstance().reset(), 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(); - let virtualGamepad = this.#getVirtualGamepad(); + } + destroy() { + if (!this.initialized) return; + if (this.initialized = !1, 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); + } + updateGamepadSlots() { + this.VIRTUAL_GAMEPAD.index = getPref("mkbSlotP1") - 1; + } + start() { + if (!this.enabled) this.enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); + this.isPolling = !0, this.escKeyDownTime = -1, this.resetGamepad(), this.updateGamepadSlots(), window.navigator.getGamepads = this.patchedGetGamepads, this.waitForMouseData(!1), this.mouseDataProvider?.start(); + let virtualGamepad = this.getVirtualGamepad(); virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", { gamepad: virtualGamepad }), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); - }; - stop = () => { - this.#enabled = !1, this.#isPolling = !1, this.#escKeyDownTime = -1; - let virtualGamepad = this.#getVirtualGamepad(); - if (virtualGamepad.connected) this.#resetGamepad(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", { + } + stop() { + this.enabled = !1, this.isPolling = !1, this.escKeyDownTime = -1; + let 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(); - }; + }), 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(EmulatedMkbHandler.LOG_TAG, "Emulate MKB"), EmulatedMkbHandler.getInstance().init(); - }); + if (window.addEventListener(BxEvent.STREAM_PLAYING, () => { + if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.init(); + else EmulatedMkbHandler.getInstance()?.init(); + }), EmulatedMkbHandler.isAllowed()) + window.addEventListener(BxEvent.MKB_UPDATED, () => { + EmulatedMkbHandler.getInstance()?.refreshPresetData(); + }); } } class NavigationDialog { dialogManager; + onMountedCallbacks = []; constructor() { this.dialogManager = NavigationDialogManager.getInstance(); } - show() { - if (NavigationDialogManager.getInstance().show(this), !this.getFocusedElement()) this.focusIfNeeded(); + isCancellable() { + return !0; + } + isOverlayVisible() { + return !0; + } + show(configs = {}, clearStack = !1) { + if (NavigationDialogManager.getInstance().show(this, configs, clearStack), !this.getFocusedElement()) this.focusIfNeeded(); } hide() { NavigationDialogManager.getInstance().hide(); @@ -3100,8 +3192,11 @@ class NavigationDialog { if (this.$container.contains($activeElement)) return $activeElement; return null; } - onBeforeMount() {} - onMounted() {} + onBeforeMount(configs = {}) {} + onMounted(configs = {}) { + for (let callback of this.onMountedCallbacks) + callback.call(this); + } onBeforeUnmount() {} onUnmounted() {} handleKeyPress(key) { @@ -3154,10 +3249,11 @@ class NavigationDialogManager { $overlay; $container; dialog = null; + dialogsStack = []; constructor() { if (BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => { - e.preventDefault(), e.stopPropagation(), this.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")) + e.preventDefault(), e.stopPropagation(), this.dialog?.isCancellable() && this.hide(); + }), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), getPref("uiControllerFriendly")) new MutationObserver((mutationList) => { if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return; let $dialog = mutationList[0].addedNodes[0]; @@ -3202,7 +3298,7 @@ class NavigationDialogManager { isShowing() { return this.$container && !this.$container.classList.contains("bx-gone"); } - pollGamepad() { + pollGamepad = () => { let gamepads = window.navigator.getGamepads(); for (let gamepad of gamepads) { if (!gamepad || !gamepad.connected) continue; @@ -3255,7 +3351,7 @@ class NavigationDialogManager { } if (this.handleGamepad(gamepad, releasedButton)) return; } - } + }; handleGamepad(gamepad, key) { let handled = this.dialog?.handleGamepad(key); if (handled) return !0; @@ -3271,12 +3367,15 @@ class NavigationDialogManager { 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(); + show(dialog, configs = {}, clearStack = !1) { + this.clearGamepadHoldingInterval(), BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.unmountCurrentDialog(), this.dialogsStack.push(dialog), this.dialog = dialog, dialog.onBeforeMount(configs), this.$container.appendChild(dialog.getContent()), dialog.onMounted(configs), this.$overlay.classList.remove("bx-gone"), this.$overlay.classList.toggle("bx-invisible", !dialog.isOverlayVisible()), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling(); } hide() { - 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; + if (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.dialog) { + let dialogIndex = this.dialogsStack.indexOf(this.dialog); + if (dialogIndex > -1) this.dialogsStack = this.dialogsStack.slice(0, dialogIndex); + } + if (this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1, this.dialogsStack.length) this.dialogsStack[this.dialogsStack.length - 1].show(); } focus($elm) { if (!$elm) return !1; @@ -3341,7 +3440,7 @@ class NavigationDialogManager { return null; } startGamepadPolling() { - this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad.bind(this), NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); + this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); } stopGamepadPolling() { this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null; @@ -3359,334 +3458,6 @@ class NavigationDialogManager { dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null; } } -var BxIcon = { - BETTER_XCLOUD: "", - TRUE_ACHIEVEMENTS: "", - STREAM_SETTINGS: "", - STREAM_STATS: "", - CLOSE: "", - COMMAND: "", - CONTROLLER: "", - CREATE_SHORTCUT: "", - DISPLAY: "", - EYE: "", - EYE_SLASH: "", - HOME: "", - NATIVE_MKB: "", - NEW: "", - COPY: "", - TRASH: "", - CURSOR_TEXT: "", - POWER: "", - QUESTION: "", - REFRESH: "", - VIRTUAL_CONTROLLER: "", - REMOTE_PLAY: "", - CARET_LEFT: "", - CARET_RIGHT: "", - SCREENSHOT: "", - SPEAKER_MUTED: "", - TOUCH_CONTROL_ENABLE: "", - TOUCH_CONTROL_DISABLE: "", - MICROPHONE: "", - MICROPHONE_MUTED: "", - BATTERY: "", - PLAYTIME: "", - SERVER: "", - DOWNLOAD: "", - UPLOAD: "", - AUDIO: "" -}; -class Dialog { - $dialog; - $title; - $content; - $overlay; - onClose; - constructor(options) { - let { - 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 getInstance = () => MkbRemapper.instance ?? (MkbRemapper.instance = new MkbRemapper); - LOG_TAG = "MkbRemapper"; - states = { - currentPresetId: 0, - presets: {}, - editingPresetData: null, - isEditing: !1 - }; - $wrapper; - $presetsSelect; - $activateButton; - $currentBindingKey; - allKeyElements = []; - allMouseElements = {}; - bindingDialog; - constructor() { - BxLogger.info(this.LOG_TAG, "constructor()"), this.states.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) => { - let buttonIndex = parseInt($elm.dataset.buttonIndex), keySlot = parseInt($elm.dataset.keySlot); - if ($elm.dataset.keyCode === key.code) return; - for (let $otherElm of this.allKeyElements) - if ($otherElm.dataset.keyCode === key.code) this.unbindKey($otherElm); - this.states.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.dataset.keyCode = key.code; - }; - unbindKey = ($elm) => { - let buttonIndex = parseInt($elm.dataset.buttonIndex), keySlot = parseInt($elm.dataset.keySlot); - this.states.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", delete $elm.dataset.keyCode; - }; - 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.states.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.dataset.prompt }); - }; - onContextMenu = (e) => { - if (e.preventDefault(), !this.states.isEditing) return; - this.unbindKey(e.target); - }; - getPreset = (presetId) => { - return this.states.presets[presetId]; - }; - getCurrentPreset = () => { - let preset = this.getPreset(this.states.currentPresetId); - if (!preset) { - let firstPresetId = parseInt(Object.keys(this.states.presets)[0]); - preset = this.states.presets[firstPresetId], this.states.currentPresetId = firstPresetId, setPref("mkb_default_preset_id", firstPresetId); - } - return preset; - }; - switchPreset = (presetId) => { - this.states.currentPresetId = presetId; - let presetData = this.getCurrentPreset().data; - for (let $elm of this.allKeyElements) { - let buttonIndex = parseInt($elm.dataset.buttonIndex), keySlot = parseInt($elm.dataset.keySlot), buttonKeys = presetData.mapping[buttonIndex]; - if (buttonKeys && buttonKeys[keySlot]) $elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]), $elm.dataset.keyCode = buttonKeys[keySlot]; - else $elm.textContent = "", delete $elm.dataset.keyCode; - } - let key; - for (key in this.allMouseElements) { - let $elm = this.allMouseElements[key], value = presetData.mouse[key]; - if (typeof value === "undefined") value = MkbPreset.MOUSE_SETTINGS[key].default; - "setValue" in $elm && $elm.setValue(value); - } - let activated = getPref("mkb_default_preset_id") === this.states.currentPresetId; - this.$activateButton.disabled = activated, this.$activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"); - }; - async refresh() { - removeChildElements(this.$presetsSelect); - let presets = await MkbPresetsDb.getInstance().getPresets(); - this.states.presets = presets; - let fragment = document.createDocumentFragment(), defaultPresetId; - if (this.states.currentPresetId === 0) this.states.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.states.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; - let $options = CE("option", { value: id2 }, name); - $options.selected = parseInt(id2) === this.states.currentPresetId, fragment.appendChild($options); - } - this.$presetsSelect.appendChild(fragment); - let activated = defaultPresetId === this.states.currentPresetId; - this.$activateButton.disabled = activated, this.$activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.states.isEditing && this.switchPreset(this.states.currentPresetId); - } - toggleEditing = (force) => { - if (this.states.isEditing = typeof force !== "undefined" ? force : !this.states.isEditing, this.$wrapper.classList.toggle("bx-editing", this.states.isEditing), this.states.isEditing) this.states.editingPresetData = deepClone(this.getCurrentPreset().data); - else this.states.editingPresetData = null; - let 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.states.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)); - }); - let 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: async () => { - let preset = this.getCurrentPreset(), newName = promptNewName(preset.name); - if (!newName || newName === preset.name) return; - preset.name = newName, await MkbPresetsDb.getInstance().updatePreset(preset), await this.refresh(); - } - }), createButton({ - icon: BxIcon.NEW, - title: t("new"), - tabIndex: -1, - onClick: (e) => { - let newName = promptNewName(""); - if (!newName) return; - MkbPresetsDb.getInstance().newPreset(newName, MkbPreset.DEFAULT_PRESET).then((id2) => { - this.states.currentPresetId = id2, this.refresh(); - }); - } - }), createButton({ - icon: BxIcon.COPY, - title: t("copy"), - tabIndex: -1, - onClick: (e) => { - let preset = this.getCurrentPreset(), newName = promptNewName(`${preset.name} (2)`); - if (!newName) return; - MkbPresetsDb.getInstance().newPreset(newName, preset.data).then((id2) => { - this.states.currentPresetId = id2, this.refresh(); - }); - } - }), createButton({ - icon: BxIcon.TRASH, - style: 2, - title: t("delete"), - tabIndex: -1, - onClick: (e) => { - if (!confirm(t("confirm-delete-preset"))) return; - MkbPresetsDb.getInstance().deletePreset(this.states.currentPresetId).then((id2) => { - this.states.currentPresetId = 0, this.refresh(); - }); - } - })); - this.$wrapper.appendChild($header); - let $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) { - let [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex], $elm, $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); - let $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"))); - let $mouseSettings = document.createDocumentFragment(); - for (let key in MkbPreset.MOUSE_SETTINGS) { - let setting = MkbPreset.MOUSE_SETTINGS[key], value = setting.default, $elm, onChange = (e, value2) => { - this.states.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); - let $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.states.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.refresh(); - } - })), CE("div", {}, createButton({ - label: t("cancel"), - style: 4, - tabIndex: -1, - onClick: (e) => { - this.switchPreset(this.states.currentPresetId), this.toggleEditing(!1); - } - }), createButton({ - label: t("save"), - style: 1, - tabIndex: -1, - onClick: (e) => { - let updatedPreset = deepClone(this.getCurrentPreset()); - updatedPreset.data = this.states.editingPresetData, MkbPresetsDb.getInstance().updatePreset(updatedPreset).then((id2) => { - if (id2 === getPref("mkb_default_preset_id")) EmulatedMkbHandler.getInstance().refreshPresetData(); - this.toggleEditing(!1), this.refresh(); - }); - } - }))); - return this.$wrapper.appendChild($actionButtons), this.toggleEditing(!1), this.refresh(), this.$wrapper; - } -} var LOG_TAG = "TouchController"; class TouchController { static #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent("message", { @@ -3756,14 +3527,13 @@ class TouchController { TouchController.#customLayouts[xboxTitleId] = null, window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000); return; } - let baseUrl = "https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts", url = `${baseUrl}/${xboxTitleId}.json`; try { - let json = await (await NATIVE_FETCH(url)).json(), layouts = {}; + let json = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`touch-layouts/${xboxTitleId}.json`))).json(), layouts = {}; json.layouts.forEach(async (layoutName) => { let baseLayouts = {}; if (layoutName in TouchController.#baseCustomLayouts) baseLayouts = TouchController.#baseCustomLayouts[layoutName]; else try { - let layoutUrl = `${baseUrl}/layouts/${layoutName}.json`; + let layoutUrl = GhPagesUtils.getUrl(`touch-layouts/layouts/${layoutName}.json`); baseLayouts = (await (await NATIVE_FETCH(layoutUrl)).json()).layouts, TouchController.#baseCustomLayouts[layoutName] = baseLayouts; } catch (e) {} Object.assign(layouts, baseLayouts); @@ -3775,9 +3545,9 @@ class TouchController { static applyCustomLayout(layoutId, delay = 0) { if (!window.BX_EXPOSED.touchLayoutManager) { let listener = (e) => { - if (window.removeEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener), TouchController.#enabled) TouchController.applyCustomLayout(layoutId, 0); + if (TouchController.#enabled) TouchController.applyCustomLayout(layoutId, 0); }; - window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener); + window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener, { once: !0 }); return; } let xboxTitleId = TouchController.#xboxTitleId; @@ -3818,9 +3588,7 @@ class TouchController { }, 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)); - }); + TouchController.#customList = GhPagesUtils.getTouchControlCustomList(); } static getCustomList() { return TouchController.#customList; @@ -3841,7 +3609,7 @@ class TouchController { }; let $style = document.createElement("style"); document.documentElement.appendChild($style), TouchController.#$style = $style; - let PREF_STYLE_STANDARD = getPref("stream_touch_controller_style_standard"), PREF_STYLE_CUSTOM = getPref("stream_touch_controller_style_custom"); + let PREF_STYLE_STANDARD = getPref("touchControllerStyleStandard"), PREF_STYLE_CUSTOM = getPref("touchControllerStyleCustom"); window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { let dataChannel = e.dataChannel; if (!dataChannel || dataChannel.label !== "message") return; @@ -3876,74 +3644,177 @@ class TouchController { }); } } -var VIBRATION_DATA_MAP = { - gamepadIndex: 8, - leftMotorPercent: 8, - rightMotorPercent: 8, - leftTriggerMotorPercent: 8, - rightTriggerMotorPercent: 8, - durationMs: 16 +var BxIcon = { + BETTER_XCLOUD: "", + TRUE_ACHIEVEMENTS: "", + STREAM_SETTINGS: "", + STREAM_STATS: "", + CLOSE: "", + CONTROLLER: "", + CREATE_SHORTCUT: "", + DISPLAY: "", + EYE: "", + EYE_SLASH: "", + HOME: "", + NATIVE_MKB: "", + NEW: "", + COPY: "", + TRASH: "", + CURSOR_TEXT: "", + POWER: "", + QUESTION: "", + REFRESH: "", + REMOTE_PLAY: "", + CARET_LEFT: "", + CARET_RIGHT: "", + SCREENSHOT: "", + SPEAKER_MUTED: "", + TOUCH_CONTROL_ENABLE: "", + TOUCH_CONTROL_DISABLE: "", + MICROPHONE: "", + MICROPHONE_MUTED: "", + BATTERY: "", + PLAYTIME: "", + SERVER: "", + DOWNLOAD: "", + UPLOAD: "", + AUDIO: "" }; -class VibrationManager { - static #playDeviceVibration(data) { - if (AppInterface) { - AppInterface.vibrate(JSON.stringify(data), window.BX_VIBRATION_INTENSITY); - return; - } - let 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; - } - let 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); - let value = getPref("controller_device_vibration"), enabled; - if (value === "on") enabled = !0; - else if (value === "auto") { - enabled = !0; - let 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; - let dataView = new DataView(e.data), offset = 0, messageType; - if (dataView.byteLength === 13) messageType = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT; - else messageType = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT; - if (!(messageType & 128)) return; - let vibrationType = dataView.getUint8(offset); - if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0) return; - let data = {}, key; - for (key in VIBRATION_DATA_MAP) - if (VIBRATION_DATA_MAP[key] === 16) data[key] = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT; - else data[key] = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT; - 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) => { - let dataChannel = e.dataChannel; - if (!dataChannel || dataChannel.label !== "input") return; - dataChannel.addEventListener("message", VibrationManager.#onMessage); +class BxSelectElement extends HTMLSelectElement { + optionsList; + indicatorsList; + $indicators; + visibleIndex; + isMultiple; + $select; + $btnNext; + $btnPrev; + $label; + $checkBox; + static create($select, forceFriendly = !1) { + if (!forceFriendly && !getPref("uiControllerFriendly")) return $select.classList.add("bx-select"), $select; + $select.removeAttribute("tabindex"); + let $wrapper = CE("div", { class: "bx-select" }), $btnPrev = createButton({ + label: "<", + style: 64 + }), $btnNext = createButton({ + label: ">", + style: 64 }); + setNearby($wrapper, { + orientation: "horizontal", + focus: $btnNext + }); + let $content, self = $wrapper; + if (self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = [], self.$btnNext = $btnNext, self.$btnPrev = $btnPrev, self.isMultiple) $content = CE("button", { + class: "bx-select-value bx-focusable", + tabindex: 0 + }, CE("div", {}, self.$checkBox = CE("input", { type: "checkbox" }), self.$label = CE("span", {}, "")), self.$indicators), $content.addEventListener("click", (e) => { + self.$checkBox.click(); + }), self.$checkBox.addEventListener("input", (e) => { + let $option = BxSelectElement.getOptionAtIndex.call(self, self.visibleIndex); + $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); + }); + else $content = CE("div", {}, self.$label = CE("label", { for: $select.id + "_checkbox" }, ""), self.$indicators); + let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self); + return $select.addEventListener("input", BxSelectElement.render.bind(self)), $btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext), new MutationObserver((mutationList, observer2) => { + mutationList.forEach((mutation) => { + if (mutation.type === "childList" || mutation.type === "attributes") self.optionsList = Array.from($select.querySelectorAll("option")), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); + }); + }).observe($select, { + subtree: !0, + childList: !0, + attributes: !0 + }), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), self.append($select, $btnPrev, $content, $btnNext), Object.defineProperty(self, "value", { + get() { + return $select.value; + }, + set(value) { + $select.value = value, self.visibleIndex = $select.selectedIndex, BxSelectElement.render.call(self); + } + }), Object.defineProperty(self, "disabled", { + get() { + return $select.disabled; + }, + set(value) { + $select.disabled = value; + } + }), self.addEventListener = function() { + $select.addEventListener.apply($select, arguments); + }, self.removeEventListener = function() { + $select.removeEventListener.apply($select, arguments); + }, self.dispatchEvent = function() { + return $select.dispatchEvent.apply($select, arguments); + }, self.appendChild = function(node) { + return $select.appendChild(node), node; + }, self; + } + static resetIndicators() { + let { + optionsList, + indicatorsList, + $indicators + } = this, targetSize = optionsList.length; + if (indicatorsList.length > targetSize) while (indicatorsList.length > targetSize) + indicatorsList.pop()?.remove(); + else if (indicatorsList.length < targetSize) while (indicatorsList.length < targetSize) { + let $indicator = CE("span", {}); + indicatorsList.push($indicator), $indicators.appendChild($indicator); + } + for (let $indicator of indicatorsList) + clearDataSet($indicator); + $indicators.classList.toggle("bx-invisible", targetSize <= 1); + } + static getOptionAtIndex(index) { + return this.optionsList[index]; + } + static render(e) { + let { + $label, + $btnNext, + $btnPrev, + $checkBox, + visibleIndex, + optionsList, + indicatorsList + } = this; + if (e && e.manualTrigger) this.visibleIndex = this.$select.selectedIndex; + this.visibleIndex = BxSelectElement.normalizeIndex.call(this, this.visibleIndex); + let $option = BxSelectElement.getOptionAtIndex.call(this, this.visibleIndex), content = ""; + if ($option) { + let $parent = $option.parentElement, hasLabel = $parent instanceof HTMLOptGroupElement || this.$select.querySelector("optgroup"); + if (content = $option.textContent || "", content && hasLabel) { + let groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : " "; + $label.innerHTML = ""; + let fragment = document.createDocumentFragment(); + fragment.appendChild(CE("span", {}, groupLabel)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment); + } else $label.textContent = content; + } else $label.textContent = content; + if ($label.classList.toggle("bx-line-through", $option && $option.disabled), this.isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); + let disableButtons = optionsList.length <= 1; + $btnPrev.classList.toggle("bx-inactive", disableButtons), $btnNext.classList.toggle("bx-inactive", disableButtons); + for (let i = 0;i < optionsList.length; i++) { + let $option2 = optionsList[i], $indicator = indicatorsList[i]; + if (clearDataSet($indicator), $option2.selected) $indicator.dataset.selected = "true"; + if ($option2.index === visibleIndex) $indicator.dataset.highlighted = "true"; + } + } + static normalizeIndex(index) { + return Math.min(Math.max(index, 0), this.optionsList.length - 1); + } + static onPrevNext(e) { + if (!e.target) return; + let { + $btnNext, + $select, + isMultiple, + visibleIndex: currentIndex + } = this, newIndex = e.target.closest("button") === $btnNext ? currentIndex + 1 : currentIndex - 1; + if (newIndex > this.optionsList.length - 1) newIndex = 0; + else if (newIndex < 0) newIndex = this.optionsList.length - 1; + if (newIndex = BxSelectElement.normalizeIndex.call(this, newIndex), this.visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; + if (isMultiple) BxSelectElement.render.call(this); + else BxEvent.dispatch($select, "input"); } } var controller_shortcuts_default = `if (window.BX_EXPOSED.disableGamepadPolling) { @@ -4018,7 +3889,7 @@ const isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500; intervalMs = isLongPress ? 500 : 100; this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings); } else { -intervalMs = window.BX_CONTROLLER_POLLING_RATE; +intervalMs = window.BX_STREAM_SETTINGS.controllerPollingRate; } } if (hijack && intervalMs) { @@ -4047,23 +3918,45 @@ const scrollWidth = \${renderTargetVar}.dataset.width ? parseInt(\${renderTarget const scrollHeight = \${renderTargetVar}.dataset.height ? parseInt(\${renderTargetVar}.dataset.height) : \${renderTargetVar}.scrollHeight; \`); eval(\`this.updateDimensions = function \${updateDimensionsStr}\`);`; -var local_co_op_enable_default = `let match; +var local_co_op_enable_default = `this.orgOnGamepadChanged = this.onGamepadChanged; +this.orgOnGamepadInput = this.onGamepadInput; +let match; let onGamepadChangedStr = this.onGamepadChanged.toString(); if (onGamepadChangedStr.startsWith('function ')) { onGamepadChangedStr = onGamepadChangedStr.substring(9); } onGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]'); -eval(\`this.onGamepadChanged = function \${onGamepadChangedStr}\`); +eval(\`this.patchedOnGamepadChanged = function \${onGamepadChangedStr}\`); let onGamepadInputStr = this.onGamepadInput.toString(); +if (onGamepadInputStr.startsWith('function ')) { +onGamepadInputStr = onGamepadInputStr.substring(9); +} match = onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/); if (match) { const gamepadIndexVar = match[0]; onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', \`this.gamepadStates.get(\${gamepadIndexVar},\`); -eval(\`this.onGamepadInput = function \${onGamepadInputStr}\`); +eval(\`this.patchedOnGamepadInput = function \${onGamepadInputStr}\`); BxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support'); } else { BxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support'); -}`; +} +this.toggleLocalCoOp = enable => { +BxLogger.info('toggleLocalCoOp', enable ? 'Enabled' : 'Disabled'); +const gamepads = window.navigator.getGamepads(); +for (const gamepad of gamepads) { +if (!gamepad?.connected) { +continue; +} +if (gamepad.id.includes('Better xCloud')) { +continue; +} +const event = new GamepadEvent('gamepaddisconnected', { gamepad }); +window.dispatchEvent(event); +} +this.onGamepadChanged = enable ? this.patchedOnGamepadChanged : this.orgOnGamepadChanged; +this.onGamepadInput = enable ? this.patchedOnGamepadInput : this.orgOnGamepadInput; +}; +window.BX_EXPOSED.toggleLocalCoOp = this.toggleLocalCoOp.bind(this);`; var set_currently_focused_interactable_default = `e && BxEvent.dispatch(window, BxEvent.NAVIGATION_FOCUS_CHANGED, {element: e});`; var remote_play_enable_default = `connectMode: window.BX_REMOTE_PLAY_CONFIG ? "xhome-connect" : "cloud-connect", remotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',`; @@ -4074,26 +3967,30 @@ this.sendKeepAlive(); return; } catch (ex) { console.log(ex); } }`; -var vibration_adjust_default = `if (!window.BX_ENABLE_CONTROLLER_VIBRATION) { -return void(0); -} -const intensity = window.BX_VIBRATION_INTENSITY; +var vibration_adjust_default = `const gamepad = e.gamepad; +if (gamepad?.connected) { +const gamepadSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id]; +if (gamepadSettings) { +const intensity = gamepadSettings.vibrationIntensity; if (intensity === 0) { -return void(0); -} -if (intensity < 1) { +return void(e.repeat = 0); +} else if (intensity < 1) { e.leftMotorPercent *= intensity; e.rightMotorPercent *= intensity; e.leftTriggerMotorPercent *= intensity; e.rightTriggerMotorPercent *= intensity; +} +} }`; var FeatureGates = { PwaPrompt: !1, EnableWifiWarnings: !1, EnableUpdateRequiredPage: !1, ShowForcedUpdateScreen: !1 -}; -if (getPref("block_social_features")) FeatureGates.EnableGuideChatTab = !1; +}, nativeMkbMode = getPref("nativeMkbMode"); +if (nativeMkbMode !== "default") FeatureGates.EnableMouseAndKeyboard = nativeMkbMode === "on"; +if (getPref("blockSocialFeatures")) FeatureGates.EnableGuideChatTab = !1; +if (getPref("byogDisabled")) FeatureGates.EnableBYOG = !1, FeatureGates.EnableBYOGPurchase = !1; if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates); class PatcherUtils { static indexOf(txt, searchString, startIndex, maxRange) { @@ -4149,7 +4046,7 @@ var ENDING_CHUNKS_PATCH_NAME = "loadingEndingChunks", LOG_TAG2 = "Patcher", PATC websiteLayout(str) { let text = '?"tv":"default"'; if (!str.includes(text)) return !1; - let layout = getPref("ui_layout") === "tv" ? "tv" : "default"; + let layout = getPref("uiLayout") === "tv" ? "tv" : "default"; return str.replace(text, `?"${layout}":"${layout}"`); }, remotePlayDirectConnectUrl(str) { @@ -4194,8 +4091,8 @@ var ENDING_CHUNKS_PATCH_NAME = "loadingEndingChunks", LOG_TAG2 = "Patcher", PATC if (index < 0) return !1; let setTimeoutIndex = str.indexOf("setTimeout(this.pollGamepads", index); if (setTimeoutIndex < 0) return !1; - let codeBlock = str.substring(index, setTimeoutIndex), tmp = str.substring(setTimeoutIndex, setTimeoutIndex + 150), tmpPatched = tmp.replaceAll("Math.max(0,4-", "Math.max(0,window.BX_CONTROLLER_POLLING_RATE-"); - if (str = PatcherUtils.replaceWith(str, setTimeoutIndex, tmp, tmpPatched), getPref("block_tracking")) codeBlock = codeBlock.replace("this.inputPollingIntervalStats.addValue", ""), codeBlock = codeBlock.replace("this.inputPollingDurationStats.addValue", ""); + let codeBlock = str.substring(index, setTimeoutIndex), tmp = str.substring(setTimeoutIndex, setTimeoutIndex + 150), tmpPatched = tmp.replaceAll("Math.max(0,4-", "Math.max(0,window.BX_STREAM_SETTINGS.controllerPollingRate - "); + if (str = PatcherUtils.replaceWith(str, setTimeoutIndex, tmp, tmpPatched), getPref("blockTracking")) codeBlock = codeBlock.replace("this.inputPollingIntervalStats.addValue", ""), codeBlock = codeBlock.replace("this.inputPollingDurationStats.addValue", ""); let match = codeBlock.match(/this\.gamepadTimestamps\.set\((\w+)\.index/); if (match) { let gamepadVar = match[1], newCode = renderString(controller_shortcuts_default, { @@ -4223,7 +4120,7 @@ logFunc(logTag, '//', logMessage); playVibration(str) { let text = "}playVibration(e){"; if (!str.includes(text)) return !1; - return VibrationManager.updateGlobalVars(), str = str.replaceAll(text, text + vibration_adjust_default), str; + return str = str.replaceAll(text, text + vibration_adjust_default), str; }, overrideSettings(str) { let index = str.indexOf(",EnableStreamGate:"); @@ -4298,8 +4195,8 @@ if (window.BX_EXPOSED.stopTakRendering) { 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 = ` + if (getPref("touchControllerMode") === "off") autoOffCode = "return;"; + else if (getPref("touchControllerAutoOff")) autoOffCode = ` const gamepads = window.navigator.getGamepads(); let gamepadFound = false; for (let gamepad of gamepads) { @@ -4335,14 +4232,15 @@ window.BX_EXPOSED.showStreamMenu = e.onShowStreamMenu; // Restore the "..." button e.guideUI = null; `; - if (getPref("stream_touch_controller") === "off") newCode += "e.canShowTakHUD = false;"; + if (getPref("touchControllerMode") === "off") newCode += "e.canShowTakHUD = false;"; return str = str.replace(text, newCode + text), str; }, broadcastPollingMode(str) { let text = ".setPollingMode=e=>{"; if (!str.includes(text)) return !1; let newCode = ` -BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e.toLowerCase()}); +window.BX_STREAM_SETTINGS.xCloudPollingMode = e.toLowerCase(); +BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED); `; return str = str.replace(text, text + newCode), str; }, @@ -4388,7 +4286,7 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar}); patchTouchControlDefaultOpacity(str) { let text = "opacityMultiplier:1"; if (!str.includes(text)) return !1; - let newCode = `opacityMultiplier: ${(getPref("stream_touch_controller_default_opacity") / 100).toFixed(1)}`; + let newCode = `opacityMultiplier: ${(getPref("touchControllerDefaultOpacity") / 100).toFixed(1)}`; return str = str.replace(text, newCode), str; }, patchShowSensorControls(str) { @@ -4411,7 +4309,9 @@ true` + text; return str = str.replace(text, text + "return !1;"), str; }, enableNativeMkb(str) { - let text = "e.mouseSupported&&e.keyboardSupported&&e.fullscreenSupported;"; + let index = str.indexOf(".mouseSupported&&"); + if (index < 0) return !1; + let varName = str.charAt(index - 1), text = `${varName}.mouseSupported&&${varName}.keyboardSupported&&${varName}.fullscreenSupported;`; if (!str.includes(text)) return !1; return str = str.replace(text, text + "return true;"), str; }, @@ -4473,7 +4373,7 @@ true` + text; let index = str.indexOf("SiglRow-module__heroCard___"); if (index < 0) return !1; if (index = PatcherUtils.lastIndexOf(str, "const[", index, 300), index < 0) return !1; - let PREF_HIDE_SECTIONS = getPref("ui_hide_sections"), siglIds = [], sections = { + let PREF_HIDE_SECTIONS = getPref("uiHideSections"), siglIds = [], sections = { "native-mkb": "8fa264dd-124f-4af3-97e8-596fcdf4b486", "most-popular": "e7590b22-e299-44db-ae22-25c61405454c" }; @@ -4519,9 +4419,8 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { }, 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; + if (index >= 0 && (index = PatcherUtils.lastIndexOf("return", str, index, 200)), 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:()=>"; @@ -4553,10 +4452,8 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { return str = str.replace(text, "=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);"), str; } }, PATCH_ORDERS = [ - ...getPref("native_mkb_enabled") === "on" ? [ + ...getPref("nativeMkbMode") === "on" ? [ "enableNativeMkb", - "patchMouseAndKeyboardEnabled", - "disableNativeRequestPointerLock", "exposeInputSink" ] : [], "modifyPreloadedState", @@ -4571,27 +4468,26 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { "exposeDialogRoutes", "guideAchievementsDefaultLocked", "enableTvRoutes", - AppInterface && "detectProductDetailsPage", + "supportLocalCoOp", "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("uiGameCardShowWaitTime") && "patchSetCurrentlyFocusedInteractable", + getPref("uiLayout") !== "default" && "websiteLayout", + getPref("gameFortniteForceConsole") && "forceFortniteConsole", + getPref("uiHideSections").includes("friends") && "ignorePlayWithFriendsSection", + getPref("uiHideSections").includes("all-games") && "ignoreAllGamesSection", + getPref("uiHideSections").includes("touch") && "ignorePlayWithTouchSection", + (getPref("uiHideSections").includes("native-mkb") || getPref("uiHideSections").includes("most-popular")) && "ignoreSiglSections", ...STATES.userAgent.capabilities.touch ? [ "disableTouchContextMenu" ] : [], - ...getPref("block_tracking") ? [ + ...getPref("blockTracking") ? [ "disableAiTrack", "disableTelemetry", "blockWebRtcStatsCollector", "disableIndexDbLogging", "disableTelemetryProvider" ] : [], - ...getPref("xhome_enabled") ? [ + ...getPref("xhomeEnabled") ? [ "remotePlayKeepAlive", "remotePlayDirectConnectUrl", "remotePlayDisableAchievementToast", @@ -4609,22 +4505,26 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { "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", + getPref("audioEnableVolumeControl") && !getPref("streamCombineSources") && "patchAudioMediaStream", + getPref("audioEnableVolumeControl") && getPref("streamCombineSources") && "patchCombinedAudioVideoMediaStream", + getPref("uiDisableFeedbackDialog") && "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", + getPref("touchControllerMode") === "all" && "patchShowSensorControls", + getPref("touchControllerMode") === "all" && "exposeTouchLayoutManager", + (getPref("touchControllerMode") === "off" || getPref("touchControllerAutoOff") || !STATES.userAgent.capabilities.touch) && "disableTakRenderer", + getPref("touchControllerDefaultOpacity") !== 100 && "patchTouchControlDefaultOpacity", "patchBabylonRendererClass" ] : [], BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging", "patchPollGamepads", - getPref("stream_combine_sources") && "streamCombineSources", - ...getPref("xhome_enabled") ? [ + getPref("streamCombineSources") && "streamCombineSources", + ...getPref("xhomeEnabled") ? [ "patchRemotePlayMkb", "remotePlayConnectMode" + ] : [], + ...getPref("nativeMkbMode") === "on" ? [ + "patchMouseAndKeyboardEnabled", + "disableNativeRequestPointerLock" ] : [] ].filter((item2) => !!item2), ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS]; class Patcher { @@ -4638,20 +4538,20 @@ class Patcher { if (!valid) return nativeBind.apply(this, arguments); if (PatcherCache.getInstance().init(), typeof arguments[1] === "function") BxLogger.info(LOG_TAG2, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind; let orgFunc = this, newFunc = (a, item2) => { - Patcher.patch(item2), orgFunc(a, item2); + Patcher.checkChunks(item2), orgFunc(a, item2); }; return nativeBind.apply(newFunc, arguments); }; } - static patch(item) { - let patchesToCheck, appliedPatches, patchesMap = {}, patcherCache = PatcherCache.getInstance(); - for (let id in item[1]) { + static checkChunks(item) { + let patchesToCheck, appliedPatches, chunkData = item[1], patchesMap = {}, patcherCache = PatcherCache.getInstance(); + for (let chunkId in chunkData) { appliedPatches = []; - let cachedPatches = patcherCache.getPatches(id); + let cachedPatches = patcherCache.getPatches(chunkId); if (cachedPatches) patchesToCheck = cachedPatches.slice(0), patchesToCheck.push(...PATCH_ORDERS); else patchesToCheck = PATCH_ORDERS.slice(0); if (!patchesToCheck.length) continue; - let func = item[1][id], funcStr = func.toString(), patchedFuncStr = funcStr, modified = !1; + let func = chunkData[chunkId], funcStr = func.toString(), patchedFuncStr = funcStr, modified = !1; for (let patchIndex = 0;patchIndex < patchesToCheck.length; patchIndex++) { let patchName = patchesToCheck[patchIndex]; if (appliedPatches.indexOf(patchName) > -1) continue; @@ -4661,11 +4561,11 @@ class Patcher { modified = !0, patchedFuncStr = tmpStr, BxLogger.info(LOG_TAG2, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName); } if (modified) try { - item[1][id] = eval(patchedFuncStr); + chunkData[chunkId] = eval(patchedFuncStr); } catch (e) { if (e instanceof Error) BxLogger.error(LOG_TAG2, "Error", appliedPatches, e.message, patchedFuncStr); } - if (appliedPatches.length) patchesMap[id] = appliedPatches; + if (appliedPatches.length) patchesMap[chunkId] = appliedPatches; } if (Object.keys(patchesMap).length) patcherCache.saveToCache(patchesMap); } @@ -4676,8 +4576,8 @@ class Patcher { class PatcherCache { static instance; static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache); - KEY_CACHE = "better_xcloud_patches_cache"; - KEY_SIGNATURE = "better_xcloud_patches_cache_signature"; + KEY_CACHE = "BetterXcloud.Patches.Cache"; + KEY_SIGNATURE = "BetterXcloud.Patches.CacheSignature"; CACHE; isInitialized = !1; getSignature() { @@ -4698,18 +4598,18 @@ class PatcherCache { } cleanupPatches(patches) { return patches.filter((item2) => { - for (let id2 in this.CACHE) - if (this.CACHE[id2].includes(item2)) return !1; + for (let id in this.CACHE) + if (this.CACHE[id].includes(item2)) return !1; return !0; }); } - getPatches(id2) { - return this.CACHE[id2]; + getPatches(id) { + return this.CACHE[id]; } saveToCache(subCache) { - for (let id2 in subCache) { - let patchNames = subCache[id2], data = this.CACHE[id2]; - if (!data) this.CACHE[id2] = patchNames; + for (let id in subCache) { + let patchNames = subCache[id], data = this.CACHE[id]; + if (!data) this.CACHE[id] = patchNames; else for (let patchName of patchNames) if (!data.includes(patchName)) data.push(patchName); } @@ -4722,6 +4622,215 @@ class PatcherCache { PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS), PLAYING_PATCH_ORDERS = this.cleanupPatches(PLAYING_PATCH_ORDERS), BxLogger.info(LOG_TAG2, PATCH_ORDERS.slice(0)), BxLogger.info(LOG_TAG2, PLAYING_PATCH_ORDERS.slice(0)); } } +class BxNumberStepper extends HTMLInputElement { + intervalId = null; + isHolding; + controlValue; + controlMin; + controlMax; + uiMin; + uiMax; + steps; + options; + onChange; + $text; + $btnInc; + $btnDec; + $range; + onInput; + onRangeInput; + onClick; + onPointerUp; + onPointerDown; + setValue; + normalizeValue; + static create(key, value, min, max, options = {}, onChange) { + options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider; + let $text, $btnInc, $btnDec, $range, self = CE("div", { + class: "bx-number-stepper", + id: `bx_setting_${key}` + }, CE("div", {}, $btnDec = CE("button", { + _dataset: { + type: "dec" + }, + type: "button", + class: options.hideSlider ? "bx-focusable" : "", + tabindex: options.hideSlider ? 0 : -1 + }, "-"), $text = CE("span"), $btnInc = CE("button", { + _dataset: { + type: "inc" + }, + type: "button", + class: options.hideSlider ? "bx-focusable" : "", + tabindex: options.hideSlider ? 0 : -1 + }, "+"))); + if (self.$text = $text, self.$btnInc = $btnInc, self.$btnDec = $btnDec, self.onChange = onChange, self.onInput = BxNumberStepper.onInput.bind(self), self.onRangeInput = BxNumberStepper.onRangeInput.bind(self), self.onClick = BxNumberStepper.onClick.bind(self), self.onPointerUp = BxNumberStepper.onPointerUp.bind(self), self.onPointerDown = BxNumberStepper.onPointerDown.bind(self), self.controlMin = min, self.controlMax = max, self.isHolding = !1, self.options = options, self.uiMin = options.reverse ? -max : min, self.uiMax = options.reverse ? -min : max, self.steps = Math.max(options.steps || 1, 1), BxNumberStepper.setValue.call(self, value), options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), self.disabled = !0, self; + if ($range = CE("input", { + id: `bx_inp_setting_${key}`, + type: "range", + min: self.uiMin, + max: self.uiMax, + value: options.reverse ? -value : value, + step: self.steps, + tabindex: 0 + }), self.$range = $range, options.hideSlider && $range.classList.add("bx-gone"), $range.addEventListener("input", self.onRangeInput), self.addEventListener("input", self.onInput), self.appendChild($range), options.ticks || options.exactTicks) { + let markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId }); + if ($range.setAttribute("list", markersId), options.exactTicks) { + let start = Math.max(Math.floor(min / options.exactTicks), 1) * options.exactTicks; + if (start === min) start += options.exactTicks; + for (let i = start;i < max; i += options.exactTicks) + $markers.appendChild(CE("option", { + value: options.reverse ? -i : i + })); + } else for (let i = self.uiMin + options.ticks;i < self.uiMax; i += options.ticks) + $markers.appendChild(CE("option", { value: i })); + self.appendChild($markers); + } + return BxNumberStepper.updateButtonsVisibility.call(self), self.addEventListener("click", self.onClick), self.addEventListener("pointerdown", self.onPointerDown), self.addEventListener("contextmenu", BxNumberStepper.onContextMenu), setNearby(self, { + focus: options.hideSlider ? $btnInc : $range + }), Object.defineProperty(self, "value", { + get() { + return self.controlValue; + }, + set(value2) { + BxNumberStepper.setValue.call(self, value2); + } + }), self; + } + static setValue(value) { + if (this.controlValue = BxNumberStepper.normalizeValue.call(this, value), this.$text.textContent = BxNumberStepper.updateTextValue.call(this), this.$range) this.$range.value = this.options.reverse ? -value : value; + BxNumberStepper.updateButtonsVisibility.call(this); + } + static normalizeValue(value) { + return value = parseInt(value), value = Math.max(this.controlMin, value), value = Math.min(this.controlMax, value), value; + } + static onInput(e) { + BxEvent.dispatch(this.$range, "input"); + } + static onRangeInput(e) { + let value = parseInt(e.target.value); + if (this.options.reverse) value *= -1; + if (BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), !e.ignoreOnChange && this.onChange) this.onChange(e, value); + } + static onClick(e) { + if (e.preventDefault(), this.isHolding) return; + let $btn = e.target.closest("button"); + $btn && BxNumberStepper.buttonPressed.call(this, e, $btn), BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1; + } + static onPointerDown(e) { + BxNumberStepper.clearIntervalId.call(this); + let $btn = e.target.closest("button"); + if (!$btn) return; + this.isHolding = !0, e.preventDefault(), this.intervalId = window.setInterval((e2) => { + BxNumberStepper.buttonPressed.call(this, e2, $btn); + }, 200), window.addEventListener("pointerup", this.onPointerUp, { once: !0 }), window.addEventListener("pointercancel", this.onPointerUp, { once: !0 }); + } + static onPointerUp(e) { + BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1; + } + static onContextMenu(e) { + e.preventDefault(); + } + static updateTextValue() { + let value = this.controlValue, textContent = null; + if (this.options.customTextValue) textContent = this.options.customTextValue(value, this.controlMin, this.controlMax); + if (textContent === null) textContent = value.toString() + this.options.suffix; + return textContent; + } + static buttonPressed(e, $btn) { + let value = this.controlValue; + if (value = this.options.reverse ? -value : value, $btn.dataset.type === "dec") value = Math.max(this.uiMin, value - this.steps); + else value = Math.min(this.uiMax, value + this.steps); + value = this.options.reverse ? -value : value, BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), this.onChange && this.onChange(e, value); + } + static clearIntervalId() { + this.intervalId && clearInterval(this.intervalId), this.intervalId = null; + } + static updateButtonsVisibility() { + if (this.$btnDec.classList.toggle("bx-inactive", this.controlValue === this.uiMin), this.$btnInc.classList.toggle("bx-inactive", this.controlValue === this.uiMax), this.controlValue === this.uiMin || this.controlValue === this.uiMax) BxNumberStepper.clearIntervalId.call(this); + } +} +class SettingElement { + static renderOptions(key, setting, currentValue, onChange) { + let $control = CE("select", { + tabindex: 0 + }), $parent; + if (setting.optionsGroup) $parent = CE("optgroup", { + label: setting.optionsGroup + }), $control.appendChild($parent); + else $parent = $control; + for (let value in setting.options) { + let label = setting.options[value], $option = CE("option", { value }, label); + $parent.appendChild($option); + } + return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { + let target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value; + !e.ignoreOnChange && onChange(e, value); + }), $control.setValue = (value) => { + $control.value = value; + }, $control; + } + static renderMultipleOptions(key, setting, currentValue, onChange, params = {}) { + let $control = CE("select", { + multiple: !0, + tabindex: 0 + }), size = params.size ? params.size : Object.keys(setting.multipleOptions).length; + $control.setAttribute("size", size.toString()); + for (let value in setting.multipleOptions) { + let label = setting.multipleOptions[value], $option = CE("option", { value }, label); + $option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) { + e.preventDefault(); + let target = e.target; + target.selected = !target.selected; + let $parent = target.parentElement; + $parent.focus(), BxEvent.dispatch($parent, "input"); + }), $control.appendChild($option); + } + return $control.addEventListener("mousedown", function(e) { + let self = this, orgScrollTop = self.scrollTop; + window.setTimeout(() => self.scrollTop = orgScrollTop, 0); + }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => { + let target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value); + !e.ignoreOnChange && onChange(e, values); + }), $control; + } + static renderCheckbox(key, setting, currentValue, onChange) { + let $control = CE("input", { type: "checkbox", tabindex: 0 }); + return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => { + !e.ignoreOnChange && onChange(e, e.target.checked); + }), $control.setValue = (value) => { + $control.checked = !!value; + }, $control; + } + static renderNumberStepper(key, setting, value, onChange, options = {}) { + return BxNumberStepper.create(key, value, setting.min, setting.max, options, onChange); + } + static METHOD_MAP = { + options: SettingElement.renderOptions, + "multiple-options": SettingElement.renderMultipleOptions, + "number-stepper": SettingElement.renderNumberStepper, + checkbox: SettingElement.renderCheckbox + }; + static render(type, key, setting, currentValue, onChange, options) { + let method = SettingElement.METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1)); + if (type !== "number-stepper") $control.id = `bx_setting_${key}`; + if (type === "options" || type === "multiple-options") $control.name = $control.id; + return $control; + } + static fromPref(key, storage, onChange, overrideParams = {}) { + let definition = storage.getDefinition(key), currentValue = storage.getSetting(key), type; + if ("options" in definition) type = "options"; + else if ("multipleOptions" in definition) type = "multiple-options"; + else if (typeof definition.default === "number") type = "number-stepper"; + else type = "checkbox"; + let params = {}; + if ("params" in definition) params = Object.assign(overrideParams, definition.params || {}); + if (params.disabled) currentValue = definition.default; + return SettingElement.render(type, key, definition, currentValue, (e, value) => { + storage.setSetting(key, value), onChange && onChange(e, value); + }, params); + } +} class FullscreenText { static instance; static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText); @@ -4739,546 +4848,341 @@ class FullscreenText { document.body.classList.remove("bx-no-scroll"), this.$text.classList.add("bx-gone"); } } -function showGamepadToast(gamepad) { - if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; - BxLogger.info("Gamepad", gamepad); - let text = "🎮"; - if (getPref("local_co_op_enabled")) text += ` #${gamepad.index + 1}`; - let gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, ""); - text += ` - ${gamepadId}`; - let status; - if (gamepad.connected) status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status"); - else status = t("disconnected"); - Toast.show(text, status, { instant: !1 }); -} -function updatePollingRate() { - window.BX_CONTROLLER_POLLING_RATE = getPref("controller_polling_rate"); -} -class SettingsNavigationDialog extends NavigationDialog { - static instance; - static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog); - LOG_TAG = "SettingsNavigationDialog"; +class BaseProfileManagerDialog extends NavigationDialog { $container; - $tabs; - $tabContents; - $btnReload; - $btnGlobalReload; - $noteGlobalReload; - $btnSuggestion; - renderFullSettings; - suggestedSettings = { - recommended: {}, - default: {}, - lowest: {}, - highest: {} - }; - suggestedSettingLabels = {}; - settingElements = {}; - TAB_GLOBAL_ITEMS = [{ - group: "general", - label: t("better-xcloud"), - helpUrl: "https://better-xcloud.github.io/features/", - items: [ - ($parent) => { - let PREF_LATEST_VERSION = getPref("version_latest"), topButtons = []; - if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) { - let opts = { - label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }), - style: 1 | 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); - let $div = CE("div", { - class: "bx-top-buttons", - _nearby: { - orientation: "vertical" - } - }, ...topButtons); - $parent.appendChild($div); - }, - "bx_locale", - "server_bypass_restriction", - "ui_controller_friendly", - "xhome_enabled" - ] - }, { - group: "server", - label: t("server"), - items: [ - "server_region", - "stream_preferred_locale", - "prefer_ipv6_server" - ] - }, { - group: "stream", - label: t("stream"), - items: [ - "stream_target_resolution", - "stream_codec_profile", - "bitrate_video_max", - "audio_enable_volume_control", - "stream_disable_feedback_dialog", - "screenshot_apply_filters", - "audio_mic_on_playing", - "game_fortnite_force_console", - "stream_combine_sources" - ] - }, { - requiredVariants: "full", - group: "co-op", - label: t("local-co-op"), - items: [ - "local_co_op_enabled" - ] - }, { - requiredVariants: "full", - group: "mkb", - label: t("mouse-and-keyboard"), - unsupportedNote: !STATES.userAgent.capabilities.mkb ? CE("a", { - href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657", - target: "_blank" - }, "⚠️ " + t("browser-unsupported-feature")) : null, - unsupported: !STATES.userAgent.capabilities.mkb, - items: [ - "native_mkb_enabled", - "game_msfs2020_force_native_mkb", - "mkb_enabled", - "mkb_hide_idle_cursor" - ] - }, { - requiredVariants: "full", - group: "touch-control", - label: t("touch-controller"), - unsupported: !STATES.userAgent.capabilities.touch, - unsupportedNote: !STATES.userAgent.capabilities.touch ? "⚠️ " + t("device-unsupported-touch") : null, - items: [ - "stream_touch_controller", - "stream_touch_controller_auto_off", - "stream_touch_controller_default_opacity", - "stream_touch_controller_style_standard", - "stream_touch_controller_style_custom" - ] - }, { - group: "ui", - label: t("ui"), - items: [ - "ui_layout", - "ui_game_card_show_wait_time", - "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) => { - let 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) => { - let profile = $control.value, custom = e.target.value.trim(); - UserAgent.updateStorage(profile, custom), this.onGlobalSettingChanged(e); - }), $control.insertAdjacentElement("afterend", $inpCustomUserAgent), setNearby($inpCustomUserAgent.parentElement, { - orientation: "vertical" - }); - } - } - ] - }, { - group: "footer", - items: [ - ($parent) => { - $parent.appendChild(CE("a", { - class: "bx-donation-link", - href: "https://ko-fi.com/redphx", - target: "_blank", - tabindex: 0 - }, `❤️ ${t("support-better-xcloud")}`)); - }, - ($parent) => { - try { - let appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10); - $parent.appendChild(CE("div", { - class: "bx-settings-app-version" - }, `xCloud website version ${appVersion} (${appDate})`)); - } catch (e) {} - }, - ($parent) => { - let debugInfo = deepClone(BX_FLAGS.DeviceInfo); - debugInfo.settings = JSON.parse(window.localStorage.getItem("better_xcloud") || "{}"); - let $debugInfo = CE("div", { class: "bx-debug-info" }, createButton({ - label: "Debug info", - style: 4 | 64 | 32, - onClick: (e) => { - let $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) => { - let $range = $elm.querySelector("input[type=range"); - window.addEventListener(BxEvent.SETTINGS_CHANGED, (e) => { - let { storageKey, settingKey, settingValue } = e; - if (storageKey !== "better_xcloud" || settingKey !== "audio_volume") return; - $range.value = settingValue, BxEvent.dispatch($range, "input", { - ignoreOnChange: !0 - }); - }); - } - }] - }, { - group: "video", - label: t("video"), - helpUrl: "https://better-xcloud.github.io/ingame-features/#video", - items: [{ - pref: "video_player_type", - onChange: onChangeVideoPlayerType - }, { - pref: "video_max_fps", - onChange: (e) => { - limitVideoPlayerFps(parseInt(e.target.value)); - } - }, { - pref: "video_power_preference", - onChange: () => { - let 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() - }, { - pref: "controller_polling_rate", - onChange: () => updatePollingRate() - }] - }, - 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) => { - let customLayouts = TouchController.getCustomLayouts(); - while ($elm.firstChild) - $elm.removeChild($elm.firstChild); - if ($elm.disabled = !customLayouts, !customLayouts) { - $elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("input")); - return; - } - let $fragment = document.createDocumentFragment(); - for (let key in customLayouts.layouts) { - let layout = customLayouts.layouts[key], name; - if (layout.author) name = `${layout.name} (${layout.author})`; - else name = layout.name; - let $option = CE("option", { value: key }, name); - $fragment.appendChild($option); - } - $elm.appendChild($fragment), $elm.value = customLayouts.default_layout; - }); - } - }] - } - ]; - TAB_VIRTUAL_CONTROLLER_ITEMS = () => [{ - group: "mkb", - label: t("virtual-controller"), - helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/", - content: MkbRemapper.getInstance().render() - }]; - TAB_NATIVE_MKB_ITEMS = [{ - requiredVariants: "full", - group: "native-mkb", - label: t("native-mkb"), - items: [{ - pref: "native_mkb_scroll_y_sensitivity", - onChange: (e, value) => { - NativeMkbHandler.getInstance().setVerticalScrollMultiplier(value / 100); - } - }, { - pref: "native_mkb_scroll_x_sensitivity", - onChange: (e, value) => { - NativeMkbHandler.getInstance().setHorizontalScrollMultiplier(value / 100); - } - }] - }]; - TAB_SHORTCUTS_ITEMS = () => [{ - requiredVariants: "full", - group: "controller-shortcuts", - label: t("controller-shortcuts"), - content: ControllerShortcut.renderSettings() - }]; - TAB_STATS_ITEMS = [{ - group: "stats", - label: t("stream-stats"), - helpUrl: "https://better-xcloud.github.io/stream-stats/", - items: [ - { - pref: "stats_show_when_playing" - }, - { - pref: "stats_quick_glance", - onChange: (e) => { - let streamStats = StreamStats.getInstance(); - e.target.checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); - } - }, - { - pref: "stats_items", - onChange: StreamStats.refreshStyles - }, - { - pref: "stats_position", - onChange: StreamStats.refreshStyles - }, - { - pref: "stats_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 = { - global: { - group: "global", - icon: BxIcon.HOME, - items: this.TAB_GLOBAL_ITEMS - }, - stream: { - group: "stream", - icon: BxIcon.DISPLAY, - items: this.TAB_DISPLAY_ITEMS - }, - controller: { - group: "controller", - icon: BxIcon.CONTROLLER, - items: this.TAB_CONTROLLER_ITEMS, - requiredVariants: "full" - }, - mkb: getPref("mkb_enabled") && { - group: "mkb", - icon: BxIcon.VIRTUAL_CONTROLLER, - items: this.TAB_VIRTUAL_CONTROLLER_ITEMS, - lazyContent: !0, - requiredVariants: "full" - }, - "native-mkb": AppInterface && getPref("native_mkb_enabled") === "on" && { - group: "native-mkb", - icon: BxIcon.NATIVE_MKB, - items: this.TAB_NATIVE_MKB_ITEMS, - requiredVariants: "full" - }, - shortcuts: { - group: "shortcuts", - icon: BxIcon.COMMAND, - items: this.TAB_SHORTCUTS_ITEMS, - lazyContent: !0, - requiredVariants: "full" - }, - stats: { - group: "stats", - icon: BxIcon.STREAM_STATS, - items: this.TAB_STATS_ITEMS - } - }; - constructor() { + title; + presetsDb; + allPresets; + currentPresetId = 0; + $presets; + $header; + $content; + $btnRename; + $btnDelete; + constructor(title, presetsDb) { super(); - BxLogger.info(this.LOG_TAG, "constructor()"), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(); + this.title = title, this.presetsDb = presetsDb; + } + updateButtonStates() { + let isDefaultPreset = this.currentPresetId < 0; + this.$btnRename.disabled = isDefaultPreset, this.$btnDelete.disabled = isDefaultPreset; + } + async renderPresetsList() { + if (this.allPresets = await this.presetsDb.getPresets(), !this.currentPresetId) this.currentPresetId = this.allPresets.default[0]; + renderPresetsList(this.$presets, this.allPresets, this.currentPresetId); + } + promptNewName(action, value = "") { + let newName = ""; + while (!newName) { + if (newName = prompt(`[${action}] ${t("prompt-preset-name")}`, value), newName === null) return !1; + newName = newName.trim(); + } + return newName ? newName : !1; + } + async renderDialog() { + this.$presets = CE("select", { tabindex: -1 }); + let $select = BxSelectElement.create(this.$presets); + $select.classList.add("bx-full-width"), $select.addEventListener("input", (e) => { + this.switchPreset(parseInt($select.value)); + }); + let $header = CE("div", { + class: "bx-dialog-preset-tools", + _nearby: { + orientation: "horizontal", + focus: $select + } + }, $select, this.$btnRename = createButton({ + title: t("rename"), + icon: BxIcon.CURSOR_TEXT, + style: 64, + onClick: async () => { + let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("rename"), preset.name); + if (!newName) return; + preset.name = newName, await this.presetsDb.updatePreset(preset), await this.renderPresetsList(); + } + }), this.$btnDelete = createButton({ + icon: BxIcon.TRASH, + title: t("delete"), + style: 4 | 64, + onClick: async (e) => { + if (!confirm(t("confirm-delete-preset"))) return; + await this.presetsDb.deletePreset(this.currentPresetId), delete this.allPresets.data[this.currentPresetId], this.currentPresetId = parseInt(Object.keys(this.allPresets.data)[0]), await this.renderPresetsList(); + } + }), createButton({ + icon: BxIcon.NEW, + title: t("new"), + style: 64, + onClick: async (e) => { + let newName = this.promptNewName(t("new")); + if (!newName) return; + let newId = await this.presetsDb.newPreset(newName, this.BLANK_PRESET_DATA); + this.currentPresetId = newId, await this.renderPresetsList(); + } + }), createButton({ + icon: BxIcon.COPY, + title: t("copy"), + style: 64, + onClick: async (e) => { + let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("copy"), `${preset.name} (2)`); + if (!newName) return; + let newId = await this.presetsDb.newPreset(newName, preset.data); + this.currentPresetId = newId, await this.renderPresetsList(); + } + })); + this.$header = $header, this.$container = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", {}, this.title), createButton({ + icon: BxIcon.CLOSE, + style: 64 | 1024 | 8, + onClick: (e) => this.hide() + })), $header, CE("div", { class: "bx-dialog-content bx-hide-scroll-bar" }, this.$content)); + } + async refresh() { + await this.renderPresetsList(), this.switchPreset(this.currentPresetId); + } + async onBeforeMount(configs = {}) { + if (configs?.id) this.currentPresetId = configs.id; + this.refresh(); } getDialog() { return this; } getContent() { + if (!this.$container) this.renderDialog(); return this.$container; } - onMounted() { - if (!this.renderFullSettings) return; - if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); - let $selectUserAgent = document.querySelector(`#bx_setting_${"user_agent_profile"}`); - if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1; + focusIfNeeded() { + this.dialogManager.focus(this.$header); } - 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(androidInfo) { - function normalize(str) { - return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-"); +} +var SHORTCUT_ACTIONS = { + [t("better-xcloud")]: { + "bx-settings-show": [t("settings"), t("show")] + }, + ...AppInterface ? { + [t("device")]: { + "device-sound-toggle": [t("sound"), t("toggle")], + "device-volume-inc": [t("volume"), t("increase")], + "device-volume-dec": [t("volume"), t("decrease")], + "device-brightness-inc": [t("brightness"), t("increase")], + "device-brightness-dec": [t("brightness"), t("decrease")] } - try { - let { brand, board, model } = androidInfo; - brand = normalize(brand), board = normalize(board), model = normalize(model); - let url = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${brand}/${board}-${model}.json`, json = await (await NATIVE_FETCH(url)).json(), recommended = {}; - if (json.schema_version !== 1) return null; - let scriptSettings = json.settings.script; - if (scriptSettings._base) { - let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base; - for (let profile of base) - Object.assign(recommended, this.suggestedSettings[profile]); - delete scriptSettings._base; + } : {}, + [t("stream")]: { + "stream-screenshot-capture": [t("take-screenshot")], + "stream-video-toggle": [t("video"), t("toggle")], + "stream-sound-toggle": [t("sound"), t("toggle")], + ...getPref("audioEnableVolumeControl") ? { + "stream-volume-inc": [t("volume"), t("increase")], + "stream-volume-dec": [t("volume"), t("decrease")] + } : {}, + "stream-menu-show": [t("menu"), t("show")], + "stream-stats-toggle": [t("stats"), t("show-hide")], + "stream-microphone-toggle": [t("microphone"), t("toggle")] + }, + ...STATES.browser.capabilities.mkb ? { + [t("mouse-and-keyboard")]: { + "mkb-toggle": [t("toggle")] + } + } : {}, + [t("other")]: { + "ta-open": [t("true-achievements"), t("show")] + } +}; +class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog { + static instance; + static getInstance = () => ControllerShortcutsManagerDialog.instance ?? (ControllerShortcutsManagerDialog.instance = new ControllerShortcutsManagerDialog(t("controller-shortcuts"))); + $content; + selectActions = {}; + BLANK_PRESET_DATA = { + mapping: {} + }; + BUTTONS_ORDER = [ + 3, + 0, + 2, + 1, + 12, + 13, + 14, + 15, + 8, + 9, + 4, + 5, + 6, + 7, + 10, + 11 + ]; + constructor(title) { + super(title, ControllerShortcutsTable.getInstance()); + let PREF_CONTROLLER_FRIENDLY_UI = getPref("uiControllerFriendly"), $baseSelect = CE("select", { autocomplete: "off" }, CE("option", { value: "" }, "---")); + for (let groupLabel in SHORTCUT_ACTIONS) { + let items = SHORTCUT_ACTIONS[groupLabel]; + if (!items) continue; + let $optGroup = CE("optgroup", { label: groupLabel }); + for (let action in items) { + let crumbs = items[action]; + if (!crumbs) continue; + let label = crumbs.join(" ❯ "), $option = CE("option", { value: action }, label); + $optGroup.appendChild($option); } - 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) {} + $baseSelect.appendChild($optGroup); + } + let $content = CE("div", { + class: "bx-controller-shortcuts-manager-container" + }), onActionChanged = (e) => { + let $target = e.target, action = $target.value; + if (!PREF_CONTROLLER_FRIENDLY_UI) { + let $fakeSelect = $target.previousElementSibling, fakeText = "---"; + if (action) { + let $selectedOption = $target.options[$target.selectedIndex]; + fakeText = $selectedOption.parentElement.label + " ❯ " + $selectedOption.text; + } + $fakeSelect.firstElementChild.text = fakeText; + } + if (!e.ignoreOnChange) this.updatePreset(); + }, fragment = document.createDocumentFragment(); + fragment.appendChild(CE("p", { class: "bx-shortcut-note" }, CE("span", { class: "bx-prompt" }, ""), ": " + t("controller-shortcuts-xbox-note"))); + for (let button of this.BUTTONS_ORDER) { + let prompt2 = GamepadKeyName[button][1], $row = CE("div", { + class: "bx-shortcut-row", + _nearby: { + orientation: "horizontal" + } + }), $label = CE("label", { class: "bx-prompt" }, `${""}${prompt2}`), $div = CE("div", { class: "bx-shortcut-actions" }), $fakeSelect = null; + if (!PREF_CONTROLLER_FRIENDLY_UI) $fakeSelect = CE("select", { autocomplete: "off" }, CE("option", {}, "---")), $div.appendChild($fakeSelect); + let $select = BxSelectElement.create($baseSelect.cloneNode(!0)); + $select.dataset.button = button.toString(), $select.classList.add("bx-full-width"), $select.addEventListener("input", onActionChanged), this.selectActions[button] = [$select, $fakeSelect], $div.appendChild($select), setNearby($row, { + focus: $select + }), $row.append($label, $div), fragment.appendChild($row); + } + $content.appendChild(fragment), this.$content = $content; + } + switchPreset(id) { + let preset = this.allPresets.data[id]; + if (!preset) { + this.currentPresetId = 0; + return; + } + this.currentPresetId = id; + let isDefaultPreset = id < 0, actions = preset.data, button; + for (button in this.selectActions) { + let [$select, $fakeSelect] = this.selectActions[button]; + $select.value = actions.mapping[button] || "", $select.disabled = isDefaultPreset, $fakeSelect && ($fakeSelect.disabled = isDefaultPreset), BxEvent.dispatch($select, "input", { + ignoreOnChange: !0, + manualTrigger: !0 + }); + } + super.updateButtonStates(); + } + updatePreset() { + let newData = deepClone(this.BLANK_PRESET_DATA), button; + for (button in this.selectActions) { + let [$select, _] = this.selectActions[button], action = $select.value; + if (!action) continue; + newData.mapping[button] = action; + } + let preset = this.allPresets.data[this.currentPresetId]; + preset.data = newData, this.presetsDb.updatePreset(preset), StreamSettings.refreshControllerSettings(); + } +} +class ControllerExtraSettings extends HTMLElement { + currentControllerId; + controllerIds; + $selectControllers; + $selectShortcuts; + $vibrationIntensity; + updateLayout; + switchController; + getCurrentControllerId; + saveSettings; + static renderSettings() { + let $container = CE("label", { + class: "bx-settings-row bx-controller-extra-settings" + }); + $container.updateLayout = ControllerExtraSettings.updateLayout.bind($container), $container.switchController = ControllerExtraSettings.switchController.bind($container), $container.getCurrentControllerId = ControllerExtraSettings.getCurrentControllerId.bind($container), $container.saveSettings = ControllerExtraSettings.saveSettings.bind($container); + let $selectControllers = BxSelectElement.create(CE("select", { + autocomplete: "off", + _on: { + input: (e) => { + $container.switchController($selectControllers.value); + } + } + })); + $selectControllers.classList.add("bx-full-width"); + let $selectShortcuts = BxSelectElement.create(CE("select", { + autocomplete: "off", + _on: { + input: $container.saveSettings + } + })), $vibrationIntensity = BxNumberStepper.create("controller_vibration_intensity", 50, 0, 100, { + steps: 10, + suffix: "%", + exactTicks: 20, + customTextValue: (value) => { + return value = parseInt(value), value === 0 ? t("off") : value + "%"; + } + }, $container.saveSettings); + return $container.append(CE("span", {}, t("no-controllers-connected")), CE("div", { class: "bx-controller-extra-wrapper" }, $selectControllers, CE("div", { class: "bx-sub-content-box" }, createSettingRow(t("controller-shortcuts-in-game"), CE("div", { + class: "bx-preset-row", + _nearby: { + orientation: "horizontal" + } + }, $selectShortcuts, createButton({ + label: t("manage"), + style: 64, + onClick: () => ControllerShortcutsManagerDialog.getInstance().show({ + id: parseInt($container.$selectShortcuts.value) + }) + })), { multiLines: !0 }), createSettingRow(t("vibration-intensity"), $vibrationIntensity)))), $container.$selectControllers = $selectControllers, $container.$selectShortcuts = $selectShortcuts, $container.$vibrationIntensity = $vibrationIntensity, $container.updateLayout(), window.addEventListener("gamepadconnected", $container.updateLayout), window.addEventListener("gamepaddisconnected", $container.updateLayout), this.onMountedCallbacks.push(() => { + $container.updateLayout(); + }), $container; + } + static async updateLayout(e) { + if (this.controllerIds = getUniqueGamepadNames(), this.dataset.hasGamepad = (this.controllerIds.length > 0).toString(), this.controllerIds.length === 0) return; + let $fragment = document.createDocumentFragment(); + removeChildElements(this.$selectControllers); + for (let name of this.controllerIds) { + let $option = CE("option", { value: name }, name); + $fragment.appendChild($option); + } + this.$selectControllers.appendChild($fragment); + let allShortcutPresets = await ControllerShortcutsTable.getInstance().getPresets(); + renderPresetsList(this.$selectShortcuts, allShortcutPresets, null, !0); + for (let name of this.controllerIds) { + let $option = CE("option", { value: name }, name); + $fragment.appendChild($option); + } + BxEvent.dispatch(this.$selectControllers, "input"); + } + static async switchController(id) { + if (this.currentControllerId = id, !this.getCurrentControllerId()) return; + let controllerSettings = await ControllerSettingsTable.getInstance().getControllerData(this.currentControllerId); + this.$selectShortcuts.value = controllerSettings.shortcutPresetId.toString(), this.$vibrationIntensity.value = controllerSettings.vibrationIntensity.toString(); + } + static getCurrentControllerId() { + if (this.currentControllerId) { + if (this.controllerIds.includes(this.currentControllerId)) return this.currentControllerId; + this.currentControllerId = ""; + } + if (!this.currentControllerId) this.currentControllerId = this.controllerIds[0]; + if (this.currentControllerId) return this.currentControllerId; return null; } - addDefaultSuggestedSetting(prefKey, value) { - let key; - for (key in this.suggestedSettings) - if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value; + static async saveSettings() { + if (!this.getCurrentControllerId()) return; + let data = { + id: this.currentControllerId, + data: { + shortcutPresetId: parseInt(this.$selectShortcuts.value), + vibrationIntensity: parseInt(this.$vibrationIntensity.value) + } + }; + await ControllerSettingsTable.getInstance().put(data), StreamSettings.refreshControllerSettings(); } - 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) { +} +class SuggestionsSetting { + static async renderSuggestions(e) { let $btnSuggest = e.target.closest("div"); $btnSuggest.toggleAttribute("bx-open"); let $content = $btnSuggest.nextElementSibling; @@ -5291,7 +5195,7 @@ class SettingsNavigationDialog extends NavigationDialog { let settingTab = this.SETTINGS_UI[settingTabGroup]; if (!settingTab || !settingTab.items || typeof settingTab.items === "function") continue; for (let settingTabContent of settingTab.items) { - if (!settingTabContent || !settingTabContent.items) continue; + if (!settingTabContent || settingTabContent instanceof HTMLElement || !settingTabContent.items) continue; for (let setting of settingTabContent.items) { let prefKey; if (typeof setting === "string") prefKey = setting; @@ -5302,13 +5206,13 @@ class SettingsNavigationDialog extends NavigationDialog { } let recommendedDevice = ""; if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) { - if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await this.getRecommendedSettings(BX_FLAGS.DeviceInfo.androidInfo); + if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await SuggestionsSetting.getRecommendedSettings.call(this, BX_FLAGS.DeviceInfo.androidInfo); } let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType; - if (deviceType === "android-handheld") 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(); + if (deviceType === "android-handheld") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchControllerMode", "off"), SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibrationMode", "on"); + else if (deviceType === "android") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibrationMode", "auto"); + else if (deviceType === "android-tv") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchControllerMode", "off"); + SuggestionsSetting.generateDefaultSuggestedSettings.call(this); let $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) => { let profile = $select.value; @@ -5355,6 +5259,8 @@ class SettingsNavigationDialog extends NavigationDialog { setPref(prefKey, suggestedValue); continue; } + let settingDefinition = getPrefDefinition(prefKey); + if (settingDefinition.transformValue) suggestedValue = settingDefinition.transformValue.get.call(settingDefinition, suggestedValue); if ("setValue" in $control) $control.setValue(suggestedValue); else $control.value = suggestedValue; BxEvent.dispatch($control, "input", { @@ -5364,15 +5270,15 @@ class SettingsNavigationDialog extends NavigationDialog { BxEvent.dispatch($select, "input"); }, $btnApply = createButton({ label: t("apply"), - style: 64 | 32, + style: 128 | 64, onClick: onClickApply }); $content = CE("div", { - class: "bx-suggest-box", + class: "bx-sub-content-box bx-suggest-box", _nearby: { orientation: "vertical" } - }, BxSelectElement.wrap($select), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", { + }, BxSelectElement.create($select, !0), $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", @@ -5384,31 +5290,973 @@ class SettingsNavigationDialog extends NavigationDialog { tabindex: 0 }, t("suggest-settings-link"))), $btnSuggest.insertAdjacentElement("afterend", $content); } - onTabClicked(e) { + static async getRecommendedSettings(androidInfo) { + function normalize(str) { + return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-"); + } + try { + let { brand, board, model } = androidInfo; + brand = normalize(brand), board = normalize(board), model = normalize(model); + let url = GhPagesUtils.getUrl(`devices/${brand}/${board}-${model}.json`), json = await (await NATIVE_FETCH(url)).json(), recommended = {}; + if (json.schema_version !== 1) return null; + let scriptSettings = json.settings.script; + if (scriptSettings._base) { + let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base; + for (let profile of base) + Object.assign(recommended, this.suggestedSettings[profile]); + delete scriptSettings._base; + } + let key; + for (key in scriptSettings) + recommended[key] = scriptSettings[key]; + return BX_FLAGS.DeviceInfo.deviceType = json.device_type, this.suggestedSettings.recommended = recommended, json.device_name; + } catch (e) {} + return null; + } + static addDefaultSuggestedSetting(prefKey, value) { + let key; + for (key in this.suggestedSettings) + if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value; + } + static generateDefaultSuggestedSettings() { + let key; + for (key in this.suggestedSettings) { + if (key === "default") continue; + let prefKey; + for (prefKey in this.suggestedSettings[key]) + if (!(prefKey in this.suggestedSettings.default)) this.suggestedSettings.default[prefKey] = getPrefDefinition(prefKey).default; + } + } +} +class BxKeyBindingButton extends HTMLButtonElement { + title; + isPrompt = !1; + allowedFlags; + keyInfo = null; + bindKey; + unbindKey; + static create(options) { + let $btn = CE("button", { + class: "bx-binding-button bx-focusable", + type: "button" + }); + return $btn.title = options.title, $btn.isPrompt = !!options.isPrompt, $btn.allowedFlags = options.allowedFlags, $btn.bindKey = BxKeyBindingButton.bindKey.bind($btn), $btn.unbindKey = BxKeyBindingButton.unbindKey.bind($btn), $btn.addEventListener("click", BxKeyBindingButton.onClick.bind($btn)), $btn.addEventListener("contextmenu", BxKeyBindingButton.onContextMenu), $btn.addEventListener("change", options.onChanged), $btn; + } + static onClick(e) { + KeyBindingDialog.getInstance().show({ + $elm: this + }); + } + static onContextMenu = (e) => { + e.preventDefault(); + let $btn = e.target; + if (!$btn.disabled) $btn.unbindKey.apply($btn); + }; + static bindKey(key, force = !1) { + if (!key) return; + if (force || this.keyInfo === null || key.code !== this.keyInfo?.code || key.modifiers !== this.keyInfo?.modifiers) { + if (this.textContent = KeyHelper.codeToKeyName(key), this.keyInfo = key, !force) BxEvent.dispatch(this, "change"); + } + } + static unbindKey(force = !1) { + this.textContent = "", this.keyInfo = null, !force && BxEvent.dispatch(this, "change"); + } + constructor() { + super(); + } +} +class KeyBindingDialog { + static instance; + static getInstance = () => KeyBindingDialog.instance ?? (KeyBindingDialog.instance = new KeyBindingDialog); + $dialog; + $wait; + $title; + $inputList; + $overlay; + $currentElm; + countdownIntervalId; + constructor() { + this.$overlay = CE("div", { class: "bx-key-binding-dialog-overlay bx-gone" }), this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$overlay), this.$dialog = CE("div", { class: "bx-key-binding-dialog bx-gone" }, this.$title = CE("h2", {}), CE("div", { class: "bx-key-binding-dialog-content" }, CE("div", {}, this.$wait = CE("p", { class: "bx-blink-me" }), this.$inputList = CE("ul", {}, CE("li", { _dataset: { flag: 1 } }, t("keyboard-key")), CE("li", { _dataset: { flag: 2 } }, t("modifiers-note")), CE("li", { _dataset: { flag: 4 } }, t("mouse-click")), CE("li", { _dataset: { flag: 8 } }, t("mouse-wheel"))), CE("i", {}, t("press-esc-to-cancel"))))), this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$dialog); + } + show(options) { + this.$currentElm = options.$elm, this.addEventListeners(); + let allowedFlags = this.$currentElm.allowedFlags; + this.$inputList.dataset.flags = "[" + allowedFlags.join("][") + "]", document.activeElement && document.activeElement.blur(), this.$title.textContent = this.$currentElm.title, this.$title.classList.toggle("bx-prompt", this.$currentElm.isPrompt), this.$dialog.classList.remove("bx-gone"), this.$overlay.classList.remove("bx-gone"), this.startCountdown(); + } + startCountdown() { + this.stopCountdown(); + let count = 9; + this.$wait.textContent = `[${count}] ${t("waiting-for-input")}`, this.countdownIntervalId = window.setInterval(() => { + if (count -= 1, count === 0) { + this.stopCountdown(), this.hide(); + return; + } + this.$wait.textContent = `[${count}] ${t("waiting-for-input")}`; + }, 1000); + } + stopCountdown() { + this.countdownIntervalId && clearInterval(this.countdownIntervalId), this.countdownIntervalId = null; + } + hide = () => { + this.clearEventListeners(), this.$dialog.classList.add("bx-gone"), this.$overlay.classList.add("bx-gone"); + }; + addEventListeners() { + let allowedFlags = this.$currentElm.allowedFlags; + if (allowedFlags.includes(1)) window.addEventListener("keyup", this); + if (allowedFlags.includes(4)) window.addEventListener("mousedown", this); + if (allowedFlags.includes(8)) window.addEventListener("wheel", this); + } + clearEventListeners() { + window.removeEventListener("keyup", this), window.removeEventListener("mousedown", this), window.removeEventListener("wheel", this); + } + handleEvent(e) { + let allowedFlags = this.$currentElm.allowedFlags, handled = !1, valid = !1; + switch (e.type) { + case "wheel": + if (handled = !0, allowedFlags.includes(8)) valid = !0; + break; + case "mousedown": + if (handled = !0, allowedFlags.includes(4)) valid = !0; + break; + case "keyup": + if (handled = !0, allowedFlags.includes(1)) { + let keyboardEvent = e; + if (valid = keyboardEvent.code !== "Escape", valid && allowedFlags.includes(2)) { + let key = keyboardEvent.key; + valid = key !== "Control" && key !== "Shift" && key !== "Alt", handled = valid; + } + } + break; + } + if (handled) { + if (e.preventDefault(), e.stopPropagation(), valid) this.$currentElm.bindKey(KeyHelper.getKeyFromEvent(e)), this.stopCountdown(); + else this.startCountdown(); + window.setTimeout(this.hide, 200); + } + } +} +class MkbMappingManagerDialog extends BaseProfileManagerDialog { + static instance; + static getInstance = () => MkbMappingManagerDialog.instance ?? (MkbMappingManagerDialog.instance = new MkbMappingManagerDialog(t("virtual-controller"))); + KEYS_PER_BUTTON = 2; + BUTTONS_ORDER = [ + 16, + 12, + 13, + 14, + 15, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 100, + 101, + 102, + 103, + 11, + 200, + 201, + 202, + 203 + ]; + BLANK_PRESET_DATA = { + mapping: {}, + mouse: { + mapTo: 2, + sensitivityX: 100, + sensitivityY: 100, + deadzoneCounterweight: 20 + } + }; + allKeyElements = []; + $mouseMapTo; + $mouseSensitivityX; + $mouseSensitivityY; + $mouseDeadzone; + constructor(title) { + super(title, MkbMappingPresetsTable.getInstance()); + this.render(); + } + onBindingKey = (e) => { + if (e.target.disabled) return; + if (e.button !== 0) return; + }; + parseDataset($btn) { + let dataset = $btn.dataset; + return { + keySlot: parseInt(dataset.keySlot), + buttonIndex: parseInt(dataset.buttonIndex) + }; + } + onKeyChanged = (e) => { + let $current = e.target, keyInfo = $current.keyInfo; + if (keyInfo) { + for (let $elm of this.allKeyElements) + if ($elm !== $current && $elm.keyInfo?.code === keyInfo.code) $elm.unbindKey(!0); + } + this.savePreset(); + }; + render() { + let $rows = CE("div", {}, CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind"))); + for (let buttonIndex of this.BUTTONS_ORDER) { + let [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex], $elm, $fragment = document.createDocumentFragment(); + for (let i = 0;i < this.KEYS_PER_BUTTON; i++) + $elm = BxKeyBindingButton.create({ + title: buttonPrompt, + isPrompt: !0, + allowedFlags: [1, 4, 8], + onChanged: this.onKeyChanged + }), $elm.dataset.buttonIndex = buttonIndex.toString(), $elm.dataset.keySlot = i.toString(), $elm.addEventListener("mouseup", this.onBindingKey), $fragment.appendChild($elm), this.allKeyElements.push($elm); + let $keyRow = CE("div", { + class: "bx-mkb-key-row", + _nearby: { + orientation: "horizontal" + } + }, CE("label", { title: buttonName }, buttonPrompt), $fragment); + $rows.appendChild($keyRow); + } + let savePreset = () => this.savePreset(), $extraSettings = CE("div", {}, createSettingRow(t("map-mouse-to"), this.$mouseMapTo = BxSelectElement.create(CE("select", { _on: { input: savePreset } }, CE("option", { value: 2 }, t("right-stick")), CE("option", { value: 1 }, t("left-stick")), CE("option", { value: 0 }, t("off"))))), createSettingRow(t("horizontal-sensitivity"), this.$mouseSensitivityX = BxNumberStepper.create("hor_sensitivity", 0, 1, 300, { + suffix: "%", + exactTicks: 50 + }, savePreset)), createSettingRow(t("vertical-sensitivity"), this.$mouseSensitivityY = BxNumberStepper.create("ver_sensitivity", 0, 1, 300, { + suffix: "%", + exactTicks: 50 + }, savePreset)), createSettingRow(t("deadzone-counterweight"), this.$mouseDeadzone = BxNumberStepper.create("deadzone_counterweight", 0, 1, 50, { + suffix: "%", + exactTicks: 10 + }, savePreset))); + this.$content = CE("div", {}, $rows, $extraSettings); + } + switchPreset(id) { + let preset = this.allPresets.data[id]; + if (!preset) { + this.currentPresetId = 0; + return; + } + let presetData = preset.data; + this.currentPresetId = id; + let isDefaultPreset = id < 0; + this.updateButtonStates(); + for (let $elm of this.allKeyElements) { + let { buttonIndex, keySlot } = this.parseDataset($elm), buttonKeys = presetData.mapping[buttonIndex]; + if (buttonKeys && buttonKeys[keySlot]) $elm.bindKey({ + code: buttonKeys[keySlot] + }, !0); + else $elm.unbindKey(!0); + $elm.disabled = isDefaultPreset; + } + let mouse = presetData.mouse; + this.$mouseMapTo.value = mouse.mapTo.toString(), this.$mouseSensitivityX.value = mouse.sensitivityX.toString(), this.$mouseSensitivityY.value = mouse.sensitivityY.toString(), this.$mouseDeadzone.value = mouse.deadzoneCounterweight.toString(), this.$mouseMapTo.disabled = isDefaultPreset, this.$mouseSensitivityX.dataset.disabled = isDefaultPreset.toString(), this.$mouseSensitivityY.dataset.disabled = isDefaultPreset.toString(), this.$mouseDeadzone.dataset.disabled = isDefaultPreset.toString(); + } + savePreset() { + let presetData = deepClone(this.BLANK_PRESET_DATA); + for (let $elm of this.allKeyElements) { + let { buttonIndex, keySlot } = this.parseDataset($elm), mapping = presetData.mapping; + if (!mapping[buttonIndex]) mapping[buttonIndex] = []; + if (!$elm.keyInfo) delete mapping[buttonIndex][keySlot]; + else mapping[buttonIndex][keySlot] = $elm.keyInfo.code; + } + let mouse = presetData.mouse; + mouse.mapTo = parseInt(this.$mouseMapTo.value), mouse.sensitivityX = parseInt(this.$mouseSensitivityX.value), mouse.sensitivityY = parseInt(this.$mouseSensitivityY.value), mouse.deadzoneCounterweight = parseInt(this.$mouseDeadzone.value); + let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = { + id: this.currentPresetId, + name: oldPreset.name, + data: presetData + }; + this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset, StreamSettings.refreshMkbSettings(); + } +} +class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog { + static instance; + static getInstance = () => KeyboardShortcutsManagerDialog.instance ?? (KeyboardShortcutsManagerDialog.instance = new KeyboardShortcutsManagerDialog(t("keyboard-shortcuts"))); + $content; + allKeyElements = []; + BLANK_PRESET_DATA = { + mapping: {} + }; + constructor(title) { + super(title, KeyboardShortcutsTable.getInstance()); + let $rows = CE("div", { class: "bx-keyboard-shortcuts-manager-container" }); + for (let groupLabel in SHORTCUT_ACTIONS) { + let items = SHORTCUT_ACTIONS[groupLabel]; + if (!items) continue; + let $fieldSet = CE("fieldset", {}, CE("legend", {}, groupLabel)); + for (let action in items) { + let crumbs = items[action]; + if (!crumbs) continue; + let label = crumbs.join(" ❯ "), $btn = BxKeyBindingButton.create({ + title: label, + isPrompt: !1, + onChanged: this.onKeyChanged, + allowedFlags: [1, 2] + }); + $btn.classList.add("bx-full-width"), $btn.dataset.action = action, this.allKeyElements.push($btn); + let $row = createSettingRow(label, CE("div", { class: "bx-binding-button-wrapper" }, $btn)); + $fieldSet.appendChild($row); + } + if ($fieldSet.childElementCount > 1) $rows.appendChild($fieldSet); + } + this.$content = CE("div", {}, $rows); + } + onKeyChanged = (e) => { + let $current = e.target, keyInfo = $current.keyInfo; + if (keyInfo) for (let $elm of this.allKeyElements) { + if ($elm === $current) continue; + if ($elm.keyInfo?.code === keyInfo.code && $elm.keyInfo?.modifiers === keyInfo.modifiers) $elm.unbindKey(!0); + } + this.savePreset(); + }; + parseDataset($btn) { + return { + action: $btn.dataset.action + }; + } + switchPreset(id) { + let preset = this.allPresets.data[id]; + if (!preset) { + this.currentPresetId = 0; + return; + } + let presetData = preset.data; + this.currentPresetId = id; + let isDefaultPreset = id < 0; + this.updateButtonStates(); + for (let $elm of this.allKeyElements) { + let { action } = this.parseDataset($elm), keyInfo = presetData.mapping[action]; + if (keyInfo) $elm.bindKey(keyInfo, !0); + else $elm.unbindKey(!0); + $elm.disabled = isDefaultPreset; + } + } + savePreset() { + let presetData = deepClone(this.BLANK_PRESET_DATA); + for (let $elm of this.allKeyElements) { + let { action } = this.parseDataset($elm), mapping = presetData.mapping; + if ($elm.keyInfo) mapping[action] = $elm.keyInfo; + } + let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = { + id: this.currentPresetId, + name: oldPreset.name, + data: presetData + }; + this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset, StreamSettings.refreshKeyboardShortcuts(); + } +} +class MkbExtraSettings extends HTMLElement { + $mappingPresets; + $shortcutsPresets; + updateLayout; + saveMkbSettings; + saveShortcutsSettings; + static renderSettings() { + let $container = document.createDocumentFragment(); + $container.updateLayout = MkbExtraSettings.updateLayout.bind($container), $container.saveMkbSettings = MkbExtraSettings.saveMkbSettings.bind($container), $container.saveShortcutsSettings = MkbExtraSettings.saveShortcutsSettings.bind($container); + let $mappingPresets = BxSelectElement.create(CE("select", { + autocomplete: "off", + _on: { + input: $container.saveMkbSettings + } + })), $shortcutsPresets = BxSelectElement.create(CE("select", { + autocomplete: "off", + _on: { + input: $container.saveShortcutsSettings + } + })); + return $container.append(createSettingRow(t("virtual-controller"), CE("div", { + class: "bx-preset-row", + _nearby: { + orientation: "horizontal" + } + }, $mappingPresets, createButton({ + label: t("manage"), + style: 64, + onClick: () => MkbMappingManagerDialog.getInstance().show({ + id: parseInt($container.$mappingPresets.value) + }) + })), { multiLines: !0 }), createSettingRow(ut("Virtual controller slot"), SettingElement.fromPref("mkbSlotP1", STORAGE.Global, () => { + EmulatedMkbHandler.getInstance()?.updateGamepadSlots(); + })), createSettingRow(t("keyboard-shortcuts-in-game"), CE("div", { + class: "bx-preset-row", + _nearby: { + orientation: "horizontal" + } + }, $shortcutsPresets, createButton({ + label: t("manage"), + style: 64, + onClick: () => KeyboardShortcutsManagerDialog.getInstance().show({ + id: parseInt($container.$shortcutsPresets.value) + }) + })), { multiLines: !0 })), $container.$mappingPresets = $mappingPresets, $container.$shortcutsPresets = $shortcutsPresets, $container.updateLayout(), this.onMountedCallbacks.push(() => { + $container.updateLayout(); + }), $container; + } + static async updateLayout() { + let mappingPresets = await MkbMappingPresetsTable.getInstance().getPresets(); + renderPresetsList(this.$mappingPresets, mappingPresets, null, !1); + let shortcutsPresets = await KeyboardShortcutsTable.getInstance().getPresets(); + renderPresetsList(this.$shortcutsPresets, shortcutsPresets, null, !1), this.$mappingPresets.value = getPref("mkbMappingPresetIdP1").toString(), this.$shortcutsPresets.value = getPref("keyboardShortcutsInGamePresetId").toString(); + } + static async saveMkbSettings() { + let presetId = parseInt(this.$mappingPresets.value); + setPref("mkbMappingPresetIdP1", presetId), StreamSettings.refreshMkbSettings(); + } + static async saveShortcutsSettings() { + let presetId = parseInt(this.$shortcutsPresets.value); + setPref("keyboardShortcutsInGamePresetId", presetId), StreamSettings.refreshKeyboardShortcuts(); + } +} +class SettingsDialog extends NavigationDialog { + static instance; + static getInstance = () => SettingsDialog.instance ?? (SettingsDialog.instance = new SettingsDialog); + LOG_TAG = "SettingsNavigationDialog"; + $container; + $tabs; + $tabContents; + $btnReload; + $btnGlobalReload; + $noteGlobalReload; + $btnSuggestion; + renderFullSettings; + suggestedSettings = { + recommended: {}, + default: {}, + lowest: {}, + highest: {} + }; + suggestedSettingLabels = {}; + settingElements = {}; + TAB_GLOBAL_ITEMS = [{ + group: "general", + label: t("better-xcloud"), + helpUrl: "https://better-xcloud.github.io/features/", + items: [ + ($parent) => { + let PREF_LATEST_VERSION = getPref("versionLatest"), topButtons = []; + if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) { + let opts = { + label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }), + style: 1 | 64 | 128 + }; + if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript(); + else opts.url = "https://github.com/redphx/better-xcloud/releases/latest"; + topButtons.push(createButton(opts)); + } + if (AppInterface) topButtons.push(createButton({ + label: t("app-settings"), + icon: BxIcon.STREAM_SETTINGS, + style: 128 | 64, + onClick: (e) => { + AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide(); + } + })); + else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({ + label: "🔥 " + t("install-android"), + style: 128 | 64, + url: "https://better-xcloud.github.io/android" + })); + this.$btnGlobalReload = createButton({ + label: t("settings-reload"), + classes: ["bx-settings-reload-button", "bx-gone"], + style: 64 | 128, + onClick: (e) => { + this.reloadPage(); + } + }), topButtons.push(this.$btnGlobalReload), this.$noteGlobalReload = CE("span", { + class: "bx-settings-reload-note" + }, t("settings-reload-note")), topButtons.push(this.$noteGlobalReload), this.$btnSuggestion = CE("div", { + class: "bx-suggest-toggler bx-focusable", + tabindex: 0 + }, CE("label", {}, t("suggest-settings")), CE("span", {}, "❯")), this.$btnSuggestion.addEventListener("click", SuggestionsSetting.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion); + let $div = CE("div", { + class: "bx-top-buttons", + _nearby: { + orientation: "vertical" + } + }, ...topButtons); + $parent.appendChild($div); + }, + { + pref: "bxLocale", + multiLines: !0 + }, + "serverBypassRestriction", + "uiControllerFriendly", + "xhomeEnabled" + ] + }, { + group: "server", + label: t("server"), + items: [ + { + pref: "serverRegion", + multiLines: !0 + }, + { + pref: "streamLocale", + multiLines: !0 + }, + "serverPreferIpv6" + ] + }, { + group: "stream", + label: t("stream"), + items: [ + "streamResolution", + "streamCodecProfile", + "streamMaxVideoBitrate", + "audioEnableVolumeControl", + "uiDisableFeedbackDialog", + "screenshotApplyFilters", + "audioMicOnPlaying", + "gameFortniteForceConsole", + "streamCombineSources" + ] + }, { + requiredVariants: "full", + group: "mkb", + label: t("mouse-and-keyboard"), + items: [ + "nativeMkbMode", + { + pref: "forceNativeMkbGames", + multiLines: !0 + }, + "mkbEnabled", + "mkbHideIdleCursor" + ], + ...!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? { + unsupported: !0, + unsupportedNote: CE("a", { + href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657", + target: "_blank" + }, "⚠️ " + t("browser-unsupported-feature")) + } : {} + }, { + requiredVariants: "full", + group: "touch-control", + label: t("touch-controller"), + items: [ + { + pref: "touchControllerMode", + note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/241", target: "_blank" }, t("unofficial-game-list")) + }, + "touchControllerAutoOff", + "touchControllerDefaultOpacity", + "touchControllerStyleStandard", + "touchControllerStyleCustom" + ], + ...!STATES.userAgent.capabilities.touch ? { + unsupported: !0, + unsupportedNote: "⚠️ " + t("device-unsupported-touch") + } : {} + }, { + group: "ui", + label: t("ui"), + items: [ + "uiLayout", + "uiGameCardShowWaitTime", + "uiShowControllerStatus", + "uiSimplifyStreamMenu", + "uiSkipSplashVideo", + !AppInterface && "uiHideScrollbar", + "uiHideSystemMenuIcon", + "uiReduceAnimations", + "blockSocialFeatures", + "byogDisabled", + { + pref: "uiHideSections", + multiLines: !0 + } + ] + }, { + requiredVariants: "full", + group: "game-bar", + label: t("game-bar"), + items: [ + "gameBarPosition" + ] + }, { + group: "loading-screen", + label: t("loading-screen"), + items: [ + "loadingScreenShowWaitTime", + "loadingScreenRocket" + ] + }, { + group: "other", + label: t("other"), + items: [ + "blockTracking" + ] + }, { + group: "advanced", + label: t("advanced"), + items: [ + { + pref: "userAgentProfile", + multiLines: !0, + onCreated: (setting, $control) => { + let 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) => { + let profile = $control.value, custom = e.target.value.trim(); + UserAgent.updateStorage(profile, custom), this.onGlobalSettingChanged(e); + }), $control.insertAdjacentElement("afterend", $inpCustomUserAgent), setNearby($inpCustomUserAgent.parentElement, { + orientation: "vertical" + }); + } + } + ] + }, { + group: "footer", + items: [ + ($parent) => { + try { + let appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10); + $parent.appendChild(CE("div", { + class: "bx-settings-app-version" + }, `xCloud website version ${appVersion} (${appDate})`)); + } catch (e) {} + }, + ($parent) => { + $parent.appendChild(CE("a", { + class: "bx-donation-link", + href: "https://ko-fi.com/redphx", + target: "_blank", + tabindex: 0 + }, `❤️ ${t("support-better-xcloud")}`)); + }, + ($parent) => { + $parent.appendChild(createButton({ + label: t("clear-data"), + style: 8 | 128 | 64, + onClick: (e) => { + if (confirm(t("clear-data-confirm"))) clearAllData(); + } + })); + }, + ($parent) => { + $parent.appendChild(CE("div", { class: "bx-debug-info" }, createButton({ + label: "Debug info", + style: 8 | 128 | 64, + onClick: (e) => { + let $button = e.target.closest("button"); + if (!$button) return; + let $pre = $button.nextElementSibling; + if (!$pre) { + let debugInfo = deepClone(BX_FLAGS.DeviceInfo); + debugInfo.settings = JSON.parse(window.localStorage.getItem("BetterXcloud") || "{}"), $pre = CE("pre", { + class: "bx-focusable bx-gone", + tabindex: 0, + _on: { + click: async (e2) => { + await copyToClipboard(e2.target.innerText); + } + } + }, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```"), $button.insertAdjacentElement("afterend", $pre); + } + $pre.classList.toggle("bx-gone"), $pre.scrollIntoView(); + } + }))); + } + ] + }]; + TAB_DISPLAY_ITEMS = [{ + requiredVariants: "full", + group: "audio", + label: t("audio"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#audio", + items: [{ + pref: "audioVolume", + onChange: (e, value) => { + SoundShortcut.setGainNodeVolume(value); + }, + params: { + disabled: !getPref("audioEnableVolumeControl") + }, + onCreated: (setting, $elm) => { + let $range = $elm.querySelector("input[type=range"); + window.addEventListener(BxEvent.SETTINGS_CHANGED, (e) => { + let { storageKey, settingKey, settingValue } = e; + if (storageKey !== "BetterXcloud" || settingKey !== "audioVolume") 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: "videoPlayerType", + onChange: onChangeVideoPlayerType + }, { + pref: "videoMaxFps", + onChange: (e) => { + limitVideoPlayerFps(parseInt(e.target.value)); + } + }, { + pref: "videoPowerPreference", + onChange: () => { + let streamPlayer = STATES.currentStream.streamPlayer; + if (!streamPlayer) return; + streamPlayer.reloadPlayer(), updateVideoPlayer(); + } + }, { + pref: "videoProcessing", + onChange: updateVideoPlayer + }, { + pref: "videoRatio", + onChange: updateVideoPlayer + }, { + pref: "videoSharpness", + onChange: updateVideoPlayer + }, { + pref: "videoSaturation", + onChange: updateVideoPlayer + }, { + pref: "videoContrast", + onChange: updateVideoPlayer + }, { + pref: "videoBrightness", + onChange: updateVideoPlayer + }] + }]; + TAB_CONTROLLER_ITEMS = [ + STATES.browser.capabilities.deviceVibration && { + group: "device", + label: t("device"), + items: [{ + pref: "deviceVibrationMode", + multiLines: !0, + unsupported: !STATES.browser.capabilities.deviceVibration, + onChange: () => StreamSettings.refreshControllerSettings() + }, { + pref: "deviceVibrationIntensity", + unsupported: !STATES.browser.capabilities.deviceVibration, + onChange: () => StreamSettings.refreshControllerSettings() + }] + }, + { + group: "controller", + label: t("controller"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#controller", + items: [ + { + pref: "localCoOpEnabled", + onChange: () => { + BxExposed.toggleLocalCoOp(getPref("localCoOpEnabled")); + } + }, + { + pref: "controllerPollingRate", + onChange: () => StreamSettings.refreshControllerSettings() + }, + ($parent) => { + $parent.appendChild(ControllerExtraSettings.renderSettings.apply(this)); + } + ] + }, + STATES.userAgent.capabilities.touch && { + group: "touch-control", + label: t("touch-controller"), + items: [{ + label: t("layout"), + content: CE("select", { + disabled: !0 + }, CE("option", {}, t("default"))), + onCreated: (setting, $elm) => { + $elm.addEventListener("input", (e) => { + TouchController.applyCustomLayout($elm.value, 1000); + }), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => { + let customLayouts = TouchController.getCustomLayouts(); + while ($elm.firstChild) + $elm.removeChild($elm.firstChild); + if ($elm.disabled = !customLayouts, !customLayouts) { + $elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("input")); + return; + } + let $fragment = document.createDocumentFragment(); + for (let key in customLayouts.layouts) { + let layout = customLayouts.layouts[key], name; + if (layout.author) name = `${layout.name} (${layout.author})`; + else name = layout.name; + let $option = CE("option", { value: key }, name); + $fragment.appendChild($option); + } + $elm.appendChild($fragment), $elm.value = customLayouts.default_layout; + }); + } + }] + } + ]; + TAB_MKB_ITEMS = () => [ + { + requiredVariants: "full", + group: "mkb", + label: t("mouse-and-keyboard"), + helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/", + items: [ + ($parent) => { + $parent.appendChild(MkbExtraSettings.renderSettings.apply(this)); + } + ] + }, + NativeMkbHandler.isAllowed() && { + requiredVariants: "full", + group: "native-mkb", + label: t("native-mkb"), + items: [{ + pref: "nativeMkbScrollYSensitivity", + onChange: (e, value) => { + NativeMkbHandler.getInstance()?.setVerticalScrollMultiplier(value / 100); + } + }, { + pref: "nativeMkbScrollXSensitivity", + onChange: (e, value) => { + NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100); + } + }] + } + ]; + TAB_STATS_ITEMS = [{ + group: "stats", + label: t("stream-stats"), + helpUrl: "https://better-xcloud.github.io/stream-stats/", + items: [ + { + pref: "statsShowWhenPlaying" + }, + { + pref: "statsQuickGlance", + onChange: (e) => { + let streamStats = StreamStats.getInstance(); + e.target.checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); + } + }, + { + pref: "statsItems", + onChange: StreamStats.refreshStyles + }, + { + pref: "statsPosition", + onChange: StreamStats.refreshStyles + }, + { + pref: "statsTextSize", + onChange: StreamStats.refreshStyles + }, + { + pref: "statsOpacity", + onChange: StreamStats.refreshStyles + }, + { + pref: "statsTransparent", + onChange: StreamStats.refreshStyles + }, + { + pref: "statsConditionalFormatting", + onChange: StreamStats.refreshStyles + } + ] + }]; + SETTINGS_UI = { + global: { + group: "global", + icon: BxIcon.HOME, + items: this.TAB_GLOBAL_ITEMS + }, + stream: { + group: "stream", + icon: BxIcon.DISPLAY, + items: this.TAB_DISPLAY_ITEMS + }, + controller: { + group: "controller", + icon: BxIcon.CONTROLLER, + items: this.TAB_CONTROLLER_ITEMS, + requiredVariants: "full" + }, + mkb: (getPref("mkbEnabled") || AppInterface && getPref("nativeMkbMode") === "on") && { + group: "mkb", + icon: BxIcon.NATIVE_MKB, + items: this.TAB_MKB_ITEMS, + lazyContent: !0, + requiredVariants: "full" + }, + stats: { + group: "stats", + icon: BxIcon.STREAM_STATS, + items: this.TAB_STATS_ITEMS + } + }; + constructor() { + super(); + BxLogger.info(this.LOG_TAG, "constructor()"), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(), this.onMountedCallbacks.push(() => { + if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); + let $selectUserAgent = document.querySelector(`#bx_setting_${"userAgentProfile"}`); + if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1; + }); + } + getDialog() { + return this; + } + getContent() { + return this.$container; + } + onMounted() { + super.onMounted(); + } + isOverlayVisible() { + return !STATES.isPlaying; + } + reloadPage() { + this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload(); + } + isSupportedVariant(requiredVariants) { + if (typeof requiredVariants === "undefined") return !0; + return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT); + } + onTabClicked = (e) => { let $svg = e.target.closest("svg"); if ($svg.dataset.lazy) { delete $svg.dataset.lazy; - let settingTab = this.SETTINGS_UI[$svg.dataset.group], items = settingTab.items(), $tabContent = this.renderTabContent.call(this, settingTab, items); + let settingTab = this.SETTINGS_UI[$svg.dataset.group], items = settingTab.items(), $tabContent = this.renderSettingsSection.call(this, settingTab, items); this.$tabContents.appendChild($tabContent); } let $child, children = Array.from(this.$tabContents.children); for ($child of children) if ($child.dataset.tabGroup === $svg.dataset.group) { - if ($child.classList.remove("bx-gone"), getPref("ui_controller_friendly")) this.dialogManager.calculateSelectBoxes($child); + if ($child.classList.remove("bx-gone"), getPref("uiControllerFriendly")) this.dialogManager.calculateSelectBoxes($child); } else $child.classList.add("bx-gone"); for (let $child2 of Array.from(this.$tabs.children)) $child2.classList.remove("bx-active"); $svg.classList.add("bx-active"); - } + }; renderTab(settingTab) { let $svg = createSvgIcon(settingTab.icon); - return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, settingTab.lazyContent && ($svg.dataset.lazy = settingTab.lazyContent.toString()), $svg.addEventListener("click", this.onTabClicked.bind(this)), $svg; + return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, settingTab.lazyContent && ($svg.dataset.lazy = settingTab.lazyContent.toString()), $svg.addEventListener("click", this.onTabClicked), $svg; } - onGlobalSettingChanged(e) { + onGlobalSettingChanged = (e) => { PatcherCache.getInstance().clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger"); - } + }; renderServerSetting(setting) { - let selectedValue = getPref("server_region"), continents = { + let selectedValue = getPref("serverRegion"), continents = { "america-north": { label: t("continent-north-america") }, @@ -5463,10 +6311,10 @@ class SettingsNavigationDialog extends NavigationDialog { 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) => { + if (pref === "serverRegion") $control = this.renderServerSetting(setting); + else if (pref === "bxLocale") $control = SettingElement.fromPref(pref, STORAGE.Global, async (e) => { let newLocale = e.target.value; - if (getPref("ui_controller_friendly")) { + if (getPref("uiControllerFriendly")) { let timeoutId = e.target.timeoutId; timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => { Translations.refreshLocale(newLocale), Translations.updateTranslations(); @@ -5474,7 +6322,7 @@ class SettingsNavigationDialog extends NavigationDialog { } else Translations.refreshLocale(newLocale), Translations.updateTranslations(); this.onGlobalSettingChanged(e); }); - else if (pref === "user_agent_profile") $control = SettingElement.fromPref("user_agent_profile", STORAGE.Global, (e) => { + else if (pref === "userAgentProfile") $control = SettingElement.fromPref("userAgentProfile", STORAGE.Global, (e) => { let value = e.target.value, isCustom = value === "custom", userAgent2 = UserAgent.get(value); UserAgent.updateStorage(value); let $inp = $control.nextElementSibling; @@ -5482,16 +6330,16 @@ class SettingsNavigationDialog extends NavigationDialog { }); else { let onChange = setting.onChange; - if (!onChange && settingTab.group === "global") onChange = this.onGlobalSettingChanged.bind(this); + if (!onChange && settingTab.group === "global") onChange = this.onGlobalSettingChanged; $control = SettingElement.fromPref(pref, STORAGE.Global, onChange, setting.params); } - if ($control instanceof HTMLSelectElement && getPref("ui_controller_friendly")) $control = BxSelectElement.wrap($control); + if ($control instanceof HTMLSelectElement) $control = BxSelectElement.create($control); pref && (this.settingElements[pref] = $control); } let prefDefinition = null; if (pref) prefDefinition = getPrefDefinition(pref); if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return; - let label = prefDefinition?.label || setting.label, note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental; + let label = prefDefinition?.label || setting.label || "", note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental; if (typeof note === "function") note = note(); if (typeof unsupportedNote === "function") unsupportedNote = unsupportedNote(); if (settingTabContent.label && setting.pref) { @@ -5502,35 +6350,32 @@ class SettingsNavigationDialog extends NavigationDialog { let $note; if (unsupportedNote) $note = CE("div", { class: "bx-settings-dialog-note" }, unsupportedNote); else if (note) $note = CE("div", { class: "bx-settings-dialog-note" }, note); - let $label, $row = CE("label", { - class: "bx-settings-row", - for: `bx_setting_${pref}`, - "data-type": settingTabContent.group, - _nearby: { - orientation: "horizontal" - } - }, $label = CE("span", { class: "bx-settings-label" }, label, $note), !prefDefinition?.unsupported && $control), $link = $label.querySelector("a"); - if ($link) $link.classList.add("bx-focusable"), setNearby($label, { - focus: $link - }); - $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control); + let $row = createSettingRow(label, !prefDefinition?.unsupported && $control, { + $note, + multiLines: setting.multiLines + }); + $row.htmlFor = `bx_setting_${pref}`, $row.dataset.type = settingTabContent.group, $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control); } - renderTabContent(settingTab, items) { + renderSettingsSection(settingTab, sections) { let $tabContent = CE("div", { class: "bx-gone", "data-tab-group": settingTab.group }); - for (let settingTabContent of items) { - if (!settingTabContent) continue; - if (!this.isSupportedVariant(settingTabContent.requiredVariants)) continue; - if (!this.renderFullSettings && settingTab.group === "global" && settingTabContent.group !== "general" && settingTabContent.group !== "footer") continue; - let label = settingTabContent.label; + for (let section of sections) { + if (!section) continue; + if (section instanceof HTMLElement) { + $tabContent.appendChild(section); + continue; + } + if (!this.isSupportedVariant(section.requiredVariants)) continue; + if (!this.renderFullSettings && settingTab.group === "global" && section.group !== "general" && section.group !== "footer") continue; + let label = section.label; if (label === t("better-xcloud")) { if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)"; label = createButton({ label, url: "https://github.com/redphx/better-xcloud/releases", - style: 1024 | 8 | 32 + style: 2048 | 16 | 64 }); } if (label) { @@ -5538,31 +6383,31 @@ class SettingsNavigationDialog extends NavigationDialog { _nearby: { orientation: "horizontal" } - }, CE("span", {}, label), settingTabContent.helpUrl && createButton({ + }, CE("span", {}, label), section.helpUrl && createButton({ icon: BxIcon.QUESTION, - style: 4 | 32, - url: settingTabContent.helpUrl, + style: 8 | 64, + url: section.helpUrl, title: t("help") })); $tabContent.appendChild($title); } - if (settingTabContent.unsupportedNote) { - let $note = CE("b", { class: "bx-note-unsupported" }, settingTabContent.unsupportedNote); + if (section.unsupportedNote) { + let $note = CE("b", { class: "bx-note-unsupported" }, section.unsupportedNote); $tabContent.appendChild($note); } - if (settingTabContent.unsupported) continue; - if (settingTabContent.content) { - $tabContent.appendChild(settingTabContent.content); + if (section.unsupported) continue; + if (section.content) { + $tabContent.appendChild(section.content); continue; } - settingTabContent.items = settingTabContent.items || []; - for (let setting of settingTabContent.items) { + section.items = section.items || []; + for (let setting of section.items) { if (setting === !1) continue; if (typeof setting === "function") { setting.apply(this, [$tabContent]); continue; } - this.renderSettingRow(settingTab, $tabContent, settingTabContent, setting); + this.renderSettingRow(settingTab, $tabContent, section, setting); } } return $tabContent; @@ -5592,13 +6437,13 @@ class SettingsNavigationDialog extends NavigationDialog { } }), CE("div", {}, this.$btnReload = createButton({ icon: BxIcon.REFRESH, - style: 32 | 16, + style: 64 | 32, onClick: (e) => { this.reloadPage(); } }), createButton({ icon: BxIcon.CLOSE, - style: 32 | 16, + style: 64 | 32, onClick: (e) => { this.dialogManager.hide(); } @@ -5624,7 +6469,7 @@ class SettingsNavigationDialog extends NavigationDialog { if (settingTab.group !== "global" && !this.renderFullSettings) continue; let $svg = this.renderTab(settingTab); if ($tabs.appendChild($svg), typeof settingTab.items === "function") continue; - let $tabContent = this.renderTabContent.call(this, settingTab, settingTab.items); + let $tabContent = this.renderSettingsSection.call(this, settingTab, settingTab.items); $tabContents.appendChild($tabContent); } $tabs.firstElementChild.dispatchEvent(new Event("click")); @@ -5727,37 +6572,153 @@ class SettingsNavigationDialog extends NavigationDialog { return handled; } } -class ControllerShortcut { - static STORAGE_KEY = "better_xcloud_controller_shortcuts"; - static buttonsCache = {}; - static buttonsStatus = {}; - static $selectProfile; - static $selectActions = {}; - static $container; - static ACTIONS = null; - static reset(index) { - ControllerShortcut.buttonsCache[index] = [], ControllerShortcut.buttonsStatus[index] = []; +class ScreenshotManager { + static instance; + static getInstance = () => ScreenshotManager.instance ?? (ScreenshotManager.instance = new ScreenshotManager); + LOG_TAG = "ScreenshotManager"; + $download; + $canvas; + canvasContext; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"), this.$download = CE("a"), this.$canvas = CE("canvas", { class: "bx-gone" }), this.canvasContext = this.$canvas.getContext("2d", { + alpha: !1, + willReadFrequently: !1 + }); } - static handle(gamepad) { - if (!ControllerShortcut.ACTIONS) ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage(); - let gamepadIndex = gamepad.index, actions = ControllerShortcut.ACTIONS[gamepad.id]; - if (!actions) return !1; - ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0), ControllerShortcut.buttonsStatus[gamepadIndex] = []; - let pressed = [], otherButtonPressed = !1, entries = gamepad.buttons.entries(); - for (let [index, button] of entries) - if (button.pressed && index !== 16) { - if (otherButtonPressed = !0, pressed[index] = !0, actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) setTimeout(() => ControllerShortcut.runAction(actions[index]), 0); - } - return ControllerShortcut.buttonsStatus[gamepadIndex] = pressed, otherButtonPressed; + updateCanvasSize(width, height) { + this.$canvas.width = width, this.$canvas.height = height; } + updateCanvasFilters(filters) { + this.canvasContext.filter = filters; + } + onAnimationEnd(e) { + e.target.classList.remove("bx-taking-screenshot"); + } + takeScreenshot(callback) { + let currentStream = STATES.currentStream, streamPlayer = currentStream.streamPlayer, $canvas = this.$canvas; + if (!streamPlayer || !$canvas) return; + let $player; + if (getPref("screenshotApplyFilters")) $player = streamPlayer.getPlayerElement(); + else $player = streamPlayer.getPlayerElement("default"); + if (!$player || !$player.isConnected) return; + $player.parentElement.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $player.parentElement.classList.add("bx-taking-screenshot"); + let canvasContext = this.canvasContext; + if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().forceDrawFrame(); + if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) { + let data = $canvas.toDataURL("image/png").split(";base64,")[1]; + AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback(); + return; + } + $canvas.toBlob((blob) => { + if (!blob) return; + let now = +new Date, $download = this.$download; + $download.download = `${currentStream.titleSlug}-${now}.png`, $download.href = URL.createObjectURL(blob), $download.click(), URL.revokeObjectURL($download.href), $download.href = "", $download.download = "", canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback(); + }, "image/png"); + } +} +class RendererShortcut { + static toggleVisibility() { + let $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]'); + if (!$mediaContainer) { + BxEvent.dispatch(window, BxEvent.VIDEO_VISIBILITY_CHANGED, { isShowing: !0 }); + return; + } + $mediaContainer.classList.toggle("bx-gone"); + let isShowing = !$mediaContainer.classList.contains("bx-gone"); + limitVideoPlayerFps(isShowing ? getPref("videoMaxFps") : 0), BxEvent.dispatch(window, BxEvent.VIDEO_VISIBILITY_CHANGED, { isShowing }); + } +} +class TrueAchievements { + static instance; + static getInstance = () => TrueAchievements.instance ?? (TrueAchievements.instance = new TrueAchievements); + LOG_TAG = "TrueAchievements"; + $link; + $button; + $hiddenLink; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"), this.$link = createButton({ + label: t("true-achievements"), + url: "#", + icon: BxIcon.TRUE_ACHIEVEMENTS, + style: 64 | 8 | 128 | 4096, + onClick: this.onClick + }), this.$button = createButton({ + label: t("true-achievements"), + title: t("true-achievements"), + icon: BxIcon.TRUE_ACHIEVEMENTS, + style: 64, + onClick: this.onClick + }), this.$hiddenLink = CE("a", { + target: "_blank" + }); + } + onClick = (e) => { + e.preventDefault(), window.BX_EXPOSED.dialogRoutes?.closeAll(); + let dataset = this.$link.dataset; + this.open(!0, dataset.xboxTitleId, dataset.id); + }; + updateIds(xboxTitleId, id) { + let $link = this.$link, $button = this.$button; + if (clearDataSet($link), clearDataSet($button), xboxTitleId) $link.dataset.xboxTitleId = xboxTitleId, $button.dataset.xboxTitleId = xboxTitleId; + if (id) $link.dataset.id = id, $button.dataset.id = id; + } + injectAchievementsProgress($elm) { + if (SCRIPT_VARIANT !== "full") return; + let $parent = $elm.parentElement, $div = CE("div", { + class: "bx-guide-home-achievements-progress" + }, $elm), xboxTitleId; + try { + let $container = $parent.closest("div[class*=AchievementsPreview-module__container]"); + if ($container) xboxTitleId = getReactProps($container).children.props.data.data.xboxTitleId; + } catch (e) {} + if (!xboxTitleId) xboxTitleId = this.getStreamXboxTitleId(); + if (typeof xboxTitleId !== "undefined") xboxTitleId = xboxTitleId.toString(); + if (this.updateIds(xboxTitleId), document.documentElement.dataset.xdsPlatform === "tv") $div.appendChild(this.$link); + else $div.appendChild(this.$button); + $parent.appendChild($div); + } + injectAchievementDetailPage($parent) { + if (SCRIPT_VARIANT !== "full") return; + let props = getReactProps($parent); + if (!props) return; + try { + let achievementList = props.children.props.data.data, $header = $parent.querySelector("div[class*=AchievementDetailHeader]"), achievementName = getReactProps($header).children[0].props.achievementName, id, xboxTitleId; + for (let achiev of achievementList) + if (achiev.name === achievementName) { + id = achiev.id, xboxTitleId = achiev.title.id; + break; + } + if (id) this.updateIds(xboxTitleId, id), $parent.appendChild(this.$link); + } catch (e) {} + } + getStreamXboxTitleId() { + return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId; + } + open(override, xboxTitleId, id) { + if (!xboxTitleId || xboxTitleId === "undefined") xboxTitleId = this.getStreamXboxTitleId(); + if (AppInterface && AppInterface.openTrueAchievementsLink) { + AppInterface.openTrueAchievementsLink(override, xboxTitleId?.toString(), id?.toString()); + return; + } + let url = "https://www.trueachievements.com"; + if (xboxTitleId) { + if (url += `/deeplink/${xboxTitleId}`, id) url += `/${id}`; + } + this.$hiddenLink.href = url, this.$hiddenLink.click(); + } +} +class ShortcutHandler { static runAction(action) { switch (action) { case "bx-settings-show": - SettingsNavigationDialog.getInstance().show(); + SettingsDialog.getInstance().show(); break; case "stream-screenshot-capture": ScreenshotManager.getInstance().takeScreenshot(); break; + case "stream-video-toggle": + RendererShortcut.toggleVisibility(); + break; case "stream-stats-toggle": StreamStats.getInstance().toggle(); break; @@ -5783,138 +6744,38 @@ class ControllerShortcut { case "device-volume-dec": AppInterface && AppInterface.runShortcut && AppInterface.runShortcut(action); break; + case "mkb-toggle": + if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.toggle(); + else EmulatedMkbHandler.getInstance()?.toggle(); + break; + case "ta-open": + TrueAchievements.getInstance().open(!1); + break; } } - static updateAction(profile, button, action) { - let actions = ControllerShortcut.ACTIONS; - if (!(profile in actions)) actions[profile] = []; - if (!action) action = null; - actions[profile][button] = action; - for (let key in ControllerShortcut.ACTIONS) { - let empty = !0; - for (let value of ControllerShortcut.ACTIONS[key]) - if (value) { - empty = !1; - break; +} +class ControllerShortcut { + static buttonsCache = {}; + static buttonsStatus = {}; + static reset(index) { + ControllerShortcut.buttonsCache[index] = [], ControllerShortcut.buttonsStatus[index] = []; + } + static handle(gamepad) { + let controllerSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id]; + if (!controllerSettings) return !1; + let actions = controllerSettings.shortcuts; + if (!actions) return !1; + let gamepadIndex = gamepad.index; + ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0), ControllerShortcut.buttonsStatus[gamepadIndex] = []; + let pressed = [], otherButtonPressed = !1, entries = gamepad.buttons.entries(), index, button; + for ([index, button] of entries) + if (button.pressed && index !== 16) { + if (otherButtonPressed = !0, pressed[index] = !0, actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) { + let idx = index; + setTimeout(() => ShortcutHandler.runAction(actions[idx]), 0); } - if (empty) delete ControllerShortcut.ACTIONS[key]; - } - window.localStorage.setItem(ControllerShortcut.STORAGE_KEY, JSON.stringify(ControllerShortcut.ACTIONS)); - } - static updateProfileList(e) { - let { $selectProfile: $select, $container } = ControllerShortcut, $fragment = document.createDocumentFragment(); - removeChildElements($select); - let gamepads = navigator.getGamepads(), hasGamepad = !1; - for (let gamepad of gamepads) { - if (!gamepad || !gamepad.connected) continue; - if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; - hasGamepad = !0; - let $option = CE("option", { value: gamepad.id }, gamepad.id); - $fragment.appendChild($option); - } - if ($container.dataset.hasGamepad = hasGamepad.toString(), hasGamepad) $select.appendChild($fragment), $select.selectedIndex = 0, $select.dispatchEvent(new Event("input")); - } - static switchProfile(profile) { - let actions = ControllerShortcut.ACTIONS[profile]; - if (!actions) actions = []; - let button; - for (button in ControllerShortcut.$selectActions) { - let $select = ControllerShortcut.$selectActions[button]; - $select.value = actions[button] || "", BxEvent.dispatch($select, "input", { - ignoreOnChange: !0, - manualTrigger: !0 - }); - } - } - static getActionsFromStorage() { - return JSON.parse(window.localStorage.getItem(ControllerShortcut.STORAGE_KEY) || "{}"); - } - static renderSettings() { - let PREF_CONTROLLER_FRIENDLY_UI = getPref("ui_controller_friendly"); - ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage(); - let buttons = new Map; - buttons.set(3, "⇑"), buttons.set(0, "⇓"), buttons.set(1, "⇒"), buttons.set(2, "⇐"), buttons.set(12, "≻"), buttons.set(13, "≽"), buttons.set(14, "≺"), buttons.set(15, "≼"), buttons.set(8, "⇺"), buttons.set(9, "⇻"), buttons.set(4, "↘"), buttons.set(5, "↙"), buttons.set(6, "↖"), buttons.set(7, "↗"), buttons.set(10, "↺"), buttons.set(11, "↻"); - let actions = { - [t("better-xcloud")]: { - "bx-settings-show": [t("settings"), t("show")] - }, - [t("device")]: AppInterface && { - "device-sound-toggle": [t("sound"), t("toggle")], - "device-volume-inc": [t("volume"), t("increase")], - "device-volume-dec": [t("volume"), t("decrease")], - "device-brightness-inc": [t("brightness"), t("increase")], - "device-brightness-dec": [t("brightness"), t("decrease")] - }, - [t("stream")]: { - "stream-screenshot-capture": t("take-screenshot"), - "stream-sound-toggle": [t("sound"), t("toggle")], - "stream-volume-inc": getPref("audio_enable_volume_control") && [t("volume"), t("increase")], - "stream-volume-dec": getPref("audio_enable_volume_control") && [t("volume"), t("decrease")], - "stream-menu-show": [t("menu"), t("show")], - "stream-stats-toggle": [t("stats"), t("show-hide")], - "stream-microphone-toggle": [t("microphone"), t("toggle")] } - }, $baseSelect = CE("select", { autocomplete: "off" }, CE("option", { value: "" }, "---")); - for (let groupLabel in actions) { - let items = actions[groupLabel]; - if (!items) continue; - let $optGroup = CE("optgroup", { label: groupLabel }); - for (let action in items) { - let label = items[action]; - if (!label) continue; - if (Array.isArray(label)) label = label.join(" ❯ "); - let $option = CE("option", { value: action }, label); - $optGroup.appendChild($option); - } - $baseSelect.appendChild($optGroup); - } - let $remap, $selectProfile = CE("select", { class: "bx-shortcut-profile", autocomplete: "off" }), $profile = PREF_CONTROLLER_FRIENDLY_UI ? BxSelectElement.wrap($selectProfile) : $selectProfile; - $profile.classList.add("bx-full-width"); - let $container = CE("div", { - "data-has-gamepad": "false", - _nearby: { - focus: $profile - } - }, CE("div", {}, CE("p", { class: "bx-shortcut-note" }, t("controller-shortcuts-connect-note"))), $remap = CE("div", {}, CE("div", { - _nearby: { - focus: $profile - } - }, $profile), CE("p", { class: "bx-shortcut-note" }, CE("span", { class: "bx-prompt" }, ""), ": " + t("controller-shortcuts-xbox-note")))); - $selectProfile.addEventListener("input", (e) => { - ControllerShortcut.switchProfile($selectProfile.value); - }); - let onActionChanged = (e) => { - let $target = e.target, profile = $selectProfile.value, button = $target.dataset.button, action = $target.value; - if (!PREF_CONTROLLER_FRIENDLY_UI) { - let $fakeSelect = $target.previousElementSibling, fakeText = "---"; - if (action) { - let $selectedOption = $target.options[$target.selectedIndex]; - fakeText = $selectedOption.parentElement.label + " ❯ " + $selectedOption.text; - } - $fakeSelect.firstElementChild.text = fakeText; - } - !e.ignoreOnChange && ControllerShortcut.updateAction(profile, button, action); - }; - for (let [button, prompt2] of buttons) { - let $row = CE("div", { - class: "bx-shortcut-row" - }), $label = CE("label", { class: "bx-prompt" }, `${""} + ${prompt2}`), $div = CE("div", { class: "bx-shortcut-actions" }); - if (!PREF_CONTROLLER_FRIENDLY_UI) { - let $fakeSelect = CE("select", { autocomplete: "off" }, CE("option", {}, "---")); - $div.appendChild($fakeSelect); - } - let $select = $baseSelect.cloneNode(!0); - if ($select.dataset.button = button.toString(), $select.addEventListener("input", onActionChanged), ControllerShortcut.$selectActions[button] = $select, PREF_CONTROLLER_FRIENDLY_UI) { - let $bxSelect = BxSelectElement.wrap($select); - $bxSelect.classList.add("bx-full-width"), $div.appendChild($bxSelect), setNearby($row, { - focus: $bxSelect - }); - } else $div.appendChild($select), setNearby($row, { - focus: $select - }); - $row.appendChild($label), $row.appendChild($div), $remap.appendChild($row); - } - return $container.appendChild($remap), ControllerShortcut.$selectProfile = $selectProfile, ControllerShortcut.$container = $container, window.addEventListener("gamepadconnected", ControllerShortcut.updateProfileList), window.addEventListener("gamepaddisconnected", ControllerShortcut.updateProfileList), ControllerShortcut.updateProfileList(), $container; + return ControllerShortcut.buttonsStatus[gamepadIndex] = pressed, otherButtonPressed; } } var BxExposed = { @@ -5930,7 +6791,7 @@ var BxExposed = { let sigls = state.xcloud.sigls; if (STATES.userAgent.capabilities.touch) { let customList = TouchController.getCustomList(), allGames = sigls["ce573635-7c18-4d0c-9d68-90b932393470"].data.products; - customList = customList.filter((id2) => allGames.includes(id2)), sigls["9c86f07a-f3e8-45ad-82a0-a1f759597059"]?.data.products.push(...customList); + customList = customList.filter((id) => allGames.includes(id)), sigls["9c86f07a-f3e8-45ad-82a0-a1f759597059"]?.data.products.push(...customList); } } catch (e) { BxLogger.error(LOG_TAG3, e); @@ -5953,10 +6814,10 @@ var BxExposed = { 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 (getPref("nativeMkbMode") === "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")) { + let touchControllerAvailability = getPref("touchControllerMode"); + if (touchControllerAvailability !== "off" && getPref("touchControllerAutoOff")) { let gamepads = window.navigator.getGamepads(), gamepadFound = !1; for (let gamepad of gamepads) if (gamepad && gamepad.connected) { @@ -6009,7 +6870,8 @@ var BxExposed = { /[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, / {2,}/g, / /g - ] + ], + toggleLocalCoOp: (enable) => {} }; function localRedirect(path) { let url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); @@ -6026,11 +6888,11 @@ function localRedirect(path) { } 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; + let preferredRegion = getPref("serverRegion"), serverRegions = STATES.serverRegions; + if (preferredRegion in serverRegions) if (shortName && serverRegions[preferredRegion].shortName) return serverRegions[preferredRegion].shortName; else return preferredRegion; - for (let regionName in STATES.serverRegions) { - let region = STATES.serverRegions[regionName]; + for (let regionName in serverRegions) { + let region = serverRegions[regionName]; if (!region.isDefault) continue; if (shortName && region.shortName) return region.shortName; else return regionName; @@ -6051,32 +6913,32 @@ class HeaderSection { 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() + style: 8 | 64 | 1024, + onClick: (e) => RemotePlayManager.getInstance()?.togglePopup() }), this.$btnSettings = createButton({ classes: ["bx-header-settings-button"], label: "???", - style: 8 | 16 | 32 | 128, - onClick: (e) => SettingsNavigationDialog.getInstance().show() - }), this.$buttonsWrapper = CE("div", {}, getPref("xhome_enabled") ? this.$btnRemotePlay : null, this.$btnSettings); + style: 16 | 32 | 64 | 256, + onClick: (e) => SettingsDialog.getInstance().show() + }), this.$buttonsWrapper = CE("div", {}, getPref("xhomeEnabled") ? this.$btnRemotePlay : null, this.$btnSettings); } injectSettingsButton($parent) { if (!$parent) return; - let PREF_LATEST_VERSION = getPref("version_latest"), $btnSettings = this.$btnSettings; + let PREF_LATEST_VERSION = getPref("versionLatest"), $btnSettings = this.$btnSettings; if (isElementVisible(this.$buttonsWrapper)) return; if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true"); $parent.appendChild(this.$buttonsWrapper); } - checkHeader() { + checkHeader = () => { let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]"); if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]"); $target && this.injectSettingsButton($target); - } + }; watchHeader() { let $root = document.querySelector("#PageContent header") || document.querySelector("#root"); if (!$root) return; this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, this.observer && this.observer.disconnect(), this.observer = new MutationObserver((mutationList) => { - this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader.bind(this), 2000); + this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader, 2000); }), this.observer.observe($root, { subtree: !0, childList: !0 }), this.checkHeader(); } showRemotePlayButton() { @@ -6086,9 +6948,9 @@ class HeaderSection { HeaderSection.getInstance().watchHeader(); } } -class RemotePlayNavigationDialog extends NavigationDialog { +class RemotePlayDialog extends NavigationDialog { static instance; - static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog); + static getInstance = () => RemotePlayDialog.instance ?? (RemotePlayDialog.instance = new RemotePlayDialog); LOG_TAG = "RemotePlayNavigationDialog"; STATE_LABELS = { On: t("powered-on"), @@ -6102,11 +6964,10 @@ class RemotePlayNavigationDialog extends NavigationDialog { BxLogger.info(this.LOG_TAG, "constructor()"), this.setupDialog(); } setupDialog() { - let $fragment = CE("div", { class: "bx-remote-play-container" }), $settingNote = CE("p", {}), currentResolution = getPref("xhome_resolution"), $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) => { + let $fragment = CE("div", { class: "bx-remote-play-container" }), $settingNote = CE("p", {}), currentResolution = getPref("xhomeStreamResolution"), $resolutions = CE("select", {}, CE("option", { value: "720p" }, "720p"), CE("option", { value: "1080p" }, "1080p")); + $resolutions = BxSelectElement.create($resolutions), $resolutions.addEventListener("input", (e) => { let value = e.target.value; - $settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setPref("xhome_resolution", value); + $settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setPref("xhomeStreamResolution", value); }), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", { manualTrigger: !0 }); @@ -6119,7 +6980,7 @@ class RemotePlayNavigationDialog extends NavigationDialog { let $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, + style: 1 | 64, onClick: (e) => manager.play(con.serverId) })); $fragment.appendChild($child); @@ -6131,11 +6992,11 @@ class RemotePlayNavigationDialog extends NavigationDialog { } }, createButton({ icon: BxIcon.QUESTION, - style: 4 | 32, + style: 8 | 64, url: "https://better-xcloud.github.io/remote-play", label: t("help") }), createButton({ - style: 4 | 32, + style: 8 | 64, label: t("close"), onClick: (e) => this.hide() }))), this.$container = $fragment; @@ -6153,7 +7014,11 @@ class RemotePlayNavigationDialog extends NavigationDialog { } class RemotePlayManager { static instance; - static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager); + static getInstance() { + if (typeof RemotePlayManager.instance === "undefined") if (getPref("xhomeEnabled")) RemotePlayManager.instance = new RemotePlayManager; + else RemotePlayManager.instance = null; + return RemotePlayManager.instance; + } LOG_TAG = "RemotePlayManager"; isInitialized = !1; XCLOUD_TOKEN; @@ -6165,25 +7030,25 @@ class RemotePlayManager { } initialize() { if (this.isInitialized) return; - this.isInitialized = !0, this.getXhomeToken(() => { + this.isInitialized = !0, this.requestXhomeToken(() => { this.getConsolesList(() => { BxLogger.info(this.LOG_TAG, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.getInstance().showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY); }); }); } - get xcloudToken() { + getXcloudToken() { return this.XCLOUD_TOKEN; } - set xcloudToken(token) { + setXcloudToken(token) { this.XCLOUD_TOKEN = token; } - get xhomeToken() { + getXhomeToken() { return this.XHOME_TOKEN; } getConsoles() { return this.consoles; } - getXhomeToken(callback) { + requestXhomeToken(callback) { if (this.XHOME_TOKEN) { callback(); return; @@ -6240,7 +7105,7 @@ class RemotePlayManager { callback(); } play(serverId, resolution) { - if (resolution) setPref("xhome_resolution", resolution); + if (resolution) setPref("xhomeStreamResolution", resolution); STATES.remotePlay.config = { serverId }, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play"); @@ -6258,10 +7123,10 @@ class RemotePlayManager { AppInterface.showRemotePlayDialog(JSON.stringify(this.consoles)), document.activeElement.blur(); return; } - RemotePlayNavigationDialog.getInstance().show(); + RemotePlayDialog.getInstance().show(); } static detect() { - if (!getPref("xhome_enabled")) return; + if (!getPref("xhomeEnabled")) 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; } @@ -6343,7 +7208,7 @@ class XhomeInterceptor { } static async handleInputConfigs(request, opts) { let response = await NATIVE_FETCH(request); - if (getPref("stream_touch_controller") !== "all") return response; + if (getPref("touchControllerMode") !== "all") return response; let obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0]; TouchController.setXboxTitleId(xboxTitleId); let inputConfigs = obj[0], hasTouchSupport = inputConfigs.supportedTabs.length > 0; @@ -6361,7 +7226,7 @@ class XhomeInterceptor { let clone = request.clone(), headers = {}; for (let pair of clone.headers.entries()) headers[pair[0]] = pair[1]; - headers.authorization = `Bearer ${RemotePlayManager.getInstance().xcloudToken}`; + headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXcloudToken()}`; let index = request.url.indexOf(".xboxlive.com"); return request = new Request("https://wus.core.gssv-play-prod" + request.url.substring(index), { method: clone.method, @@ -6381,9 +7246,9 @@ class XhomeInterceptor { let clone = request.clone(), headers = {}; for (let pair of clone.headers.entries()) headers[pair[0]] = pair[1]; - headers.authorization = `Bearer ${RemotePlayManager.getInstance().xhomeToken}`; + headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXhomeToken()}`; let deviceInfo = XhomeInterceptor.BASE_DEVICE_INFO; - if (getPref("xhome_resolution") === "720p") deviceInfo.dev.os.name = "android"; + if (getPref("xhomeStreamResolution") === "720p") deviceInfo.dev.os.name = "android"; headers["x-ms-device-info"] = JSON.stringify(deviceInfo); let opts = { method: clone.method, @@ -6420,7 +7285,7 @@ class LoadingScreen { let $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(); + if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getPref("loadingScreenRocket") === "hide") LoadingScreen.hideRocket(); } static hideRocket() { let $bgStyle = LoadingScreen.$bgStyle; @@ -6435,7 +7300,7 @@ class LoadingScreen { }, bg.src = imageUrl; } static setupWaitTime(waitTime) { - if (getPref("ui_loading_screen_rocket") === "hide-queue") LoadingScreen.hideRocket(); + if (getPref("loadingScreenRocket") === "hide-queue") LoadingScreen.hideRocket(); let secondsLeft = waitTime, $countDown, $estimated; LoadingScreen.orgWebTitle = document.title; let endDate = new Date, timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60; @@ -6450,7 +7315,7 @@ class LoadingScreen { }, 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) { + if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), LoadingScreen.$bgStyle) { let $rocketBg = document.querySelector('#game-stream rect[width="800"]'); $rocketBg && $rocketBg.addEventListener("transitionend", (e) => { LoadingScreen.$bgStyle.textContent += "#game-stream{background:#000 !important}"; @@ -6462,85 +7327,6 @@ 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 instance; - static getInstance = () => TrueAchievements.instance ?? (TrueAchievements.instance = new TrueAchievements); - LOG_TAG = "TrueAchievements"; - $link; - $button; - $hiddenLink; - constructor() { - BxLogger.info(this.LOG_TAG, "constructor()"), this.$link = createButton({ - label: t("true-achievements"), - url: "#", - icon: BxIcon.TRUE_ACHIEVEMENTS, - style: 32 | 4 | 64 | 2048, - onClick: this.onClick.bind(this) - }), this.$button = createButton({ - label: t("true-achievements"), - title: t("true-achievements"), - icon: BxIcon.TRUE_ACHIEVEMENTS, - style: 32, - onClick: this.onClick.bind(this) - }), this.$hiddenLink = CE("a", { - target: "_blank" - }); - } - onClick(e) { - e.preventDefault(), window.BX_EXPOSED.dialogRoutes?.closeAll(); - let dataset = this.$link.dataset; - this.open(!0, dataset.xboxTitleId, dataset.id); - } - updateIds(xboxTitleId, id2) { - let $link = this.$link, $button = this.$button; - if (clearDataSet($link), clearDataSet($button), xboxTitleId) $link.dataset.xboxTitleId = xboxTitleId, $button.dataset.xboxTitleId = xboxTitleId; - if (id2) $link.dataset.id = id2, $button.dataset.id = id2; - } - injectAchievementsProgress($elm) { - if (SCRIPT_VARIANT !== "full") return; - let $parent = $elm.parentElement, $div = CE("div", { - class: "bx-guide-home-achievements-progress" - }, $elm), xboxTitleId; - try { - let $container = $parent.closest("div[class*=AchievementsPreview-module__container]"); - if ($container) xboxTitleId = getReactProps($container).children.props.data.data.xboxTitleId; - } catch (e) {} - if (!xboxTitleId) xboxTitleId = this.getStreamXboxTitleId(); - if (typeof xboxTitleId !== "undefined") xboxTitleId = xboxTitleId.toString(); - if (this.updateIds(xboxTitleId), document.documentElement.dataset.xdsPlatform === "tv") $div.appendChild(this.$link); - else $div.appendChild(this.$button); - $parent.appendChild($div); - } - injectAchievementDetailPage($parent) { - if (SCRIPT_VARIANT !== "full") return; - let props = getReactProps($parent); - if (!props) return; - try { - let achievementList = props.children.props.data.data, $header = $parent.querySelector("div[class*=AchievementDetailHeader]"), achievementName = getReactProps($header).children[0].props.achievementName, id2, xboxTitleId; - for (let achiev of achievementList) - if (achiev.name === achievementName) { - id2 = achiev.id, xboxTitleId = achiev.title.id; - break; - } - if (id2) this.updateIds(xboxTitleId, id2), $parent.appendChild(this.$link); - } catch (e) {} - } - getStreamXboxTitleId() { - return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId; - } - open(override, xboxTitleId, id2) { - if (!xboxTitleId || xboxTitleId === "undefined") xboxTitleId = this.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}`; - } - this.$hiddenLink.href = url, this.$hiddenLink.click(); - } -} class GuideMenu { static instance; static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu); @@ -6558,18 +7344,18 @@ class GuideMenu { let buttons = { scriptSettings: createButton({ label: t("better-xcloud"), - style: 64 | 32 | 1, - onClick: (() => { + style: 128 | 64 | 1, + onClick: () => { window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, (e) => { - setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50); + setTimeout(() => SettingsDialog.getInstance().show(), 50); }, { once: !0 }), this.closeGuideMenu(); - }).bind(this) + } }), closeApp: AppInterface && createButton({ icon: BxIcon.POWER, label: t("close-app"), title: t("close-app"), - style: 64 | 32 | 2, + style: 128 | 64 | 4, onClick: (e) => { AppInterface.closeApp(); }, @@ -6581,20 +7367,20 @@ class GuideMenu { icon: BxIcon.REFRESH, label: t("reload-page"), title: t("reload-page"), - style: 64 | 32, - onClick: (() => { + style: 128 | 64, + onClick: () => { if (this.closeGuideMenu(), STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload(); else window.location.reload(); - }).bind(this) + } }), backToHome: createButton({ icon: BxIcon.HOME, label: t("back-to-home"), title: t("back-to-home"), - style: 64 | 32, - onClick: (() => { + style: 128 | 64, + onClick: () => { this.closeGuideMenu(), confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)); - }).bind(this), + }, attributes: { "data-state": "playing" } @@ -6639,14 +7425,14 @@ class GuideMenu { let $buttons = this.renderButtons(); $buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons); } - async onShown(e) { + onShown = async (e) => { if (e.where === "home") { let $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]"); $root && this.injectHome($root, STATES.isPlaying); } - } + }; addEventListeners() { - window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown.bind(this)); + window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown); } observe($addedElm) { let className = $addedElm.className; @@ -6732,7 +7518,7 @@ class StreamBadges { if ($badge = CE("div", { class: "bx-badge", title: badgeInfo.name }, CE("span", { class: "bx-badge-name" }, createSvgIcon(badgeInfo.icon)), CE("span", { class: "bx-badge-value", style: `background-color: ${badgeInfo.color}` }, value)), name === "battery") $badge.classList.add("bx-badge-battery"); return this.badges[name].$element = $badge, $badge; } - async updateBadges(forceUpdate = !1) { + updateBadges = async (forceUpdate = !1) => { if (!this.$container || !forceUpdate && !this.$container.isConnected) { this.stop(); return; @@ -6753,9 +7539,9 @@ class StreamBadges { if ($elm.lastElementChild.textContent = value, name === "battery") if (batt.current === 100 && batt.start === 100) $elm.classList.add("bx-gone"); else $elm.dataset.charging = batt.isCharging.toString(), $elm.classList.remove("bx-gone"); } - } + }; async start() { - await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges.bind(this), this.REFRESH_INTERVAL); + await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges, this.REFRESH_INTERVAL); } stop() { this.intervalId && clearInterval(this.intervalId), this.intervalId = null; @@ -6853,7 +7639,7 @@ class XcloudInterceptor { WestEurope: ["🇪🇺", "europe"] }; static async handleLogin(request, init) { - let bypassServer = getPref("server_bypass_restriction"); + let bypassServer = getPref("serverBypassRestriction"); if (bypassServer !== "off") { let ip = BypassServerIps[bypassServer]; ip && request.headers.set("X-Forwarded-For", ip); @@ -6861,7 +7647,7 @@ class XcloudInterceptor { let response = await NATIVE_FETCH(request, init); if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response; let obj = await response.clone().json(); - RemotePlayManager.getInstance().xcloudToken = obj.gsToken; + RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken); let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region; for (region of obj.offeringSettings.regions) { let { name: regionName, name: shortName } = region; @@ -6881,7 +7667,7 @@ class XcloudInterceptor { } static async handlePlay(request, init) { BxEvent.dispatch(window, BxEvent.STREAM_LOADING); - let 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), badgeRegion = parsedUrl.host.split(".", 1)[0]; + let PREF_STREAM_TARGET_RESOLUTION = getPref("streamResolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("streamLocale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0]; for (let regionName in STATES.serverRegions) { let region = STATES.serverRegions[regionName]; if (parsedUrl.origin == region.baseUri) { @@ -6903,7 +7689,7 @@ class XcloudInterceptor { } static async handleWaitTime(request, init) { let response = await NATIVE_FETCH(request, init); - if (getPref("ui_loading_screen_wait_time")) { + if (getPref("loadingScreenShowWaitTime")) { let json = await response.clone().json(); if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds); } @@ -6911,7 +7697,7 @@ class XcloudInterceptor { } 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(); + if (getPref("touchControllerMode") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable(); else TouchController.enable(); let response = await NATIVE_FETCH(request, init), text = await response.clone().text(); if (!text.length) return response; @@ -6919,14 +7705,14 @@ class XcloudInterceptor { let obj = JSON.parse(text), 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 (getPref("nativeMkbMode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0; + if (getPref("nativeMkbMode") === "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; + if (getPref("audioMicOnPlaying")) 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) { @@ -6986,21 +7772,21 @@ async function patchIceCandidates(request, consoleAddrs) { let response = await NATIVE_FETCH(request), text = await response.clone().text(); if (!text.length) return response; let options = { - preferIpv6Server: getPref("prefer_ipv6_server"), + preferIpv6Server: getPref("serverPreferIpv6"), consoleAddrs }, obj = JSON.parse(text), exchangeResponse = JSON.parse(obj.exchangeResponse); return exchangeResponse = updateIceCandidates(exchangeResponse, options), obj.exchangeResponse = JSON.stringify(exchangeResponse), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; } function interceptHttpRequests() { let BLOCKED_URLS = []; - if (getPref("block_tracking")) clearAllLogs(), BLOCKED_URLS = BLOCKED_URLS.concat([ + if (getPref("blockTracking")) 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", "https://mscom.demdex.net" ]); - if (getPref("block_social_features")) BLOCKED_URLS = BLOCKED_URLS.concat([ + if (getPref("blockSocialFeatures")) 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" @@ -7053,7 +7839,7 @@ function interceptHttpRequests() { 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)); + customList = customList.filter((id) => gamepassAllGames.includes(id)); let newCustomList = customList.map((item2) => ({ id: item2 })); obj.push(...newCustomList); } catch (e) { @@ -7079,18 +7865,19 @@ function interceptHttpRequests() { }; } function addCss() { - let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#df5656;--bx-danger-button-disabled-rgb:223,86,86;--bx-fullscreen-text-z-index:99999;--bx-toast-z-index:60000;--bx-dialog-z-index:50000;--bx-dialog-overlay-z-index:40200;--bx-stats-bar-z-index:40100;--bx-mkb-pointer-lock-msg-z-index:40000;--bx-navigation-dialog-z-index:30100;--bx-navigation-dialog-overlay-z-index:30000;--bx-game-bar-z-index:10000;--bx-screenshot-animation-z-index:9000;--bx-wait-time-box-z-index:1000}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf")}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font)}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}select[multiple]{overflow:auto}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}div[class*=SupportedInputsBadge]:not(:has(:nth-child(2))),div[class*=SupportedInputsBadge] svg:first-of-type{display:none}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.549);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb))}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha));backdrop-filter:blur(4px) brightness(1.5)}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-left:10px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-dialog-overlay-z-index);background:#000;opacity:50%}.bx-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:20px;border-radius:8px;z-index:var(--bx-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-dialog *:focus{outline:none !important}.bx-dialog h2{display:flex;margin-bottom:12px}.bx-dialog h2 b{flex:1;color:#fff;display:block;font-family:var(--bx-title-font);font-size:26px;font-weight:400;line-height:var(--bx-button-height)}.bx-dialog.bx-binding-dialog h2 b{font-family:var(--bx-promptfont-font) !important}.bx-dialog > div{overflow:auto;padding:2px 0}.bx-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-dialog > button:hover{background-color:#515863}}.bx-dialog > button:focus{background-color:#515863}@media screen and (max-width:450px){.bx-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-focusable::after{border-radius:4px}.bx-settings-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-settings-dialog select option:disabled{display:none}.bx-settings-dialog input[type=checkbox]:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-settings-dialog a:hover,.bx-settings-dialog a:focus{color:#5dc21e}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;padding:10px;margin-left:48px;width:450px;max-width:calc(100vw - tabsWidth);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents > div[data-tab-group=mkb]{display:flex;flex-direction:column;height:100%;overflow:hidden}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:first-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:last-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:first-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:last-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-profile{width:100%;height:36px;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-note{margin-top:10px;font-size:14px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row{display:flex;margin-bottom:10px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row label.bx-prompt{flex:1;font-size:26px;margin-bottom:0}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions{flex:2;position:relative}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select{position:absolute;width:100%;height:100%;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select:last-of-type{opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:10px;border-top-right-radius:10px}.bx-settings-tab-contents > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:10px;border-bottom-right-radius:10px}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:10px}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861}.bx-suggest-toggler label{flex:1;margin-bottom:0;padding:10px;background:#004f87}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:40px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none;background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-container{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;width:420px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:20px}.bx-remote-play-container > .bx-button{display:table;margin:0 0 0 auto}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;padding:4px 0}.bx-remote-play-device-name{font-size:20px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:12px;background:#004c87;color:#fff;display:inline-block;border-radius:14px;padding:2px 10px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}.bx-select{display:flex;align-items:center;flex:0 1 auto}.bx-select select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-select > div,.bx-select button.bx-select-value{min-width:120px;text-align:left;margin:0 8px;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;flex:1}.bx-select > div{display:inline-block}.bx-select > div input{display:inline-block;margin-right:8px}.bx-select > div label{margin-bottom:0;font-size:14px;width:100%}.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:initial}.bx-select button.bx-select-value{border:none;display:inline-flex;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}.bx-select button.bx-select-value:hover input,.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}.bx-select button.bx-select-value:hover::after,.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}.bx-select button.bx-button{border:none;height:24px;width:24px;padding:0;line-height:24px;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}.bx-select button.bx-button span{line-height:unset}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}html[data-xds-platform=tv] .bx-guide-home-achievements-progress{flex-direction:column}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress{flex-direction:row}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}html[data-xds-platform=tv] .bx-guide-home-buttons > div{flex-direction:column}html[data-xds-platform=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}html:not([data-xds-platform=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state=\'normal\']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state=\'playing\']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container]{display:flex}div[data-testid=media-container].bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:\' \';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#game-stream video{margin:auto;align-self:center;background:#000}#game-stream canvas{position:absolute;align-self:center;margin:auto;left:0;right:0}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper span{display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);font-size:13px;margin:0 4px}.bx-number-stepper button{border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type="range"]{display:block;margin:12px auto 2px;width:180px;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-transparent=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{min-width:60px;display:inline-block;text-align:right;vertical-align:middle}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-stats-bar span:first-of-type{min-width:22px}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-settings select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;text-align:right;border:none;color:#fff}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:50%;transform:translateX(-50%) translateY(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;text-align:center;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:220px;opacity:.9}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > div:first-of-type{display:flex;flex-direction:column;text-align:left}.bx-mkb-pointer-lock-msg p{margin:0}.bx-mkb-pointer-lock-msg p:first-child{font-size:22px;margin-bottom:4px;font-weight:bold}.bx-mkb-pointer-lock-msg p:last-child{font-size:12px;font-style:italic}.bx-mkb-pointer-lock-msg > div:last-of-type{margin-top:10px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type=\'native\'] button:first-of-type{margin-bottom:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type=\'virtual\'] div{display:flex;flex-flow:row;margin-top:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type=\'virtual\'] div button{flex:1}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type=\'virtual\'] div button:first-of-type{margin-right:5px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type=\'virtual\'] div button:last-of-type{margin-left:5px}.bx-mkb-preset-tools{display:flex;margin-bottom:12px}.bx-mkb-preset-tools select{flex:1}.bx-mkb-preset-tools button{margin-left:6px}.bx-mkb-settings-rows{flex:1;overflow:scroll}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:26px;text-align:center;width:26px;height:32px;line-height:32px}.bx-mkb-key-row button{flex:1;height:32px;line-height:32px;margin:0 0 0 10px;background:transparent;border:none;color:#fff;border-radius:0;border-left:1px solid #373737}.bx-mkb-key-row button:hover{background:transparent;cursor:default}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:16px 0 10px;font-size:12px}.bx-mkb-note:first-of-type{margin-top:0}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getPref("ui_hide_sections"), selectorToHide = []; + let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-warning-button-color:#c16e04;--bx-warning-button-rgb:193,110,4;--bx-warning-button-hover-color:#fa9005;--bx-warning-button-hover-rgb:250,144,5;--bx-warning-button-active-color:#965603;--bx-warning-button-active-rgb:150,86,3;--bx-warning-button-disabled-color:#a2816c;--bx-warning-button-disabled-rgb:162,129,108;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#df5656;--bx-danger-button-disabled-rgb:223,86,86;--bx-fullscreen-text-z-index:9999;--bx-toast-z-index:6000;--bx-key-binding-dialog-z-index:5010;--bx-key-binding-dialog-overlay-z-index:5000;--bx-stats-bar-z-index:4010;--bx-navigation-dialog-z-index:3010;--bx-navigation-dialog-overlay-z-index:3000;--bx-mkb-pointer-lock-msg-z-index:2000;--bx-game-bar-z-index:1000;--bx-screenshot-animation-z-index:200;--bx-wait-time-box-z-index:100}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf")}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) !important}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}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-blink-me{animation:bx-blinker 1s linear infinite}@-moz-keyframes bx-blinker{100%{opacity:0}}@-webkit-keyframes bx-blinker{100%{opacity:0}}@-o-keyframes bx-blinker{100%{opacity:0}}@keyframes bx-blinker{100%{opacity:0}}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb))}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-warning{--button-rgb:var(--bx-warning-button-rgb)}.bx-button.bx-warning:not([disabled]):active{--button-active-rgb:var(--bx-warning-button-active-rgb)}.bx-button.bx-warning:not([disabled]):not(:active):hover,.bx-button.bx-warning:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-warning-button-hover-rgb)}.bx-button.bx-warning:disabled{--button-disabled-rgb:var(--bx-warning-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha));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);width:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-left:10px}.bx-button.bx-button-multi-lines{height:auto;text-align:left;padding:10px 0}.bx-button.bx-button-multi-lines span{line-height:unset;display:block}.bx-button.bx-button-multi-lines span:last-of-type{text-transform:none;font-weight:normal;font-family:"Segoe Sans Variable Text";font-size:12px;margin-top:4px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-key-binding-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-key-binding-dialog-overlay-z-index);background:#000;opacity:50%}.bx-key-binding-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:20px;border-radius:8px;z-index:var(--bx-key-binding-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-key-binding-dialog *:focus{outline:none !important}.bx-key-binding-dialog h2{margin-bottom:12px;color:#fff;display:block;font-family:var(--bx-title-font);font-size:32px;font-weight:400;line-height:var(--bx-button-height)}.bx-key-binding-dialog > div{overflow:auto;padding:2px 0}.bx-key-binding-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-key-binding-dialog > button:hover{background-color:#515863}}.bx-key-binding-dialog > button:focus{background-color:#515863}.bx-key-binding-dialog ul{margin-bottom:1rem}.bx-key-binding-dialog ul li{display:none}.bx-key-binding-dialog ul[data-flags*="[1]"] > li[data-flag="1"],.bx-key-binding-dialog ul[data-flags*="[2]"] > li[data-flag="2"],.bx-key-binding-dialog ul[data-flags*="[4]"] > li[data-flag="4"],.bx-key-binding-dialog ul[data-flags*="[8]"] > li[data-flag="8"]{display:list-item}@media screen and (max-width:450px){.bx-key-binding-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog select:disabled{-webkit-appearance:none;text-align-last:right;text-align:right;color:#fff;background:#131416;border:none;border-radius:4px;padding:0 5px}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-centered-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;width:450px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:20px;max-height:95vh;flex-direction:column;overflow:hidden;display:flex;flex-direction:column}.bx-centered-dialog .bx-dialog-title{display:flex;flex-direction:row;align-items:center;margin-bottom:10px}.bx-centered-dialog .bx-dialog-title p{padding:0;margin:0;flex:1;font-size:1.2rem;font-weight:bold}.bx-centered-dialog .bx-dialog-title button{flex-shrink:0}.bx-centered-dialog .bx-dialog-content{flex:1;overflow:auto;overflow-x:hidden}.bx-centered-dialog .bx-dialog-preset-tools{display:flex;margin-bottom:12px;gap:6px}.bx-centered-dialog .bx-dialog-preset-tools select{flex:1}.bx-centered-dialog input,.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-centered-dialog input:focus,.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-centered-dialog select:disabled,.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-centered-dialog select option:disabled,.bx-settings-dialog select option:disabled{display:none}.bx-centered-dialog input[type=checkbox]:focus,.bx-settings-dialog input[type=checkbox]:focus,.bx-centered-dialog select:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-centered-dialog a,.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-centered-dialog a:hover,.bx-settings-dialog a:hover,.bx-centered-dialog a:focus,.bx-settings-dialog a:focus{color:#5dc21e}.bx-centered-dialog label,.bx-settings-dialog label{margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-note{margin-top:10px;font-size:14px;text-align:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt{flex-shrink:0;font-size:32px;margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt::first-letter{letter-spacing:6px}.bx-controller-shortcuts-manager-container .bx-shortcut-row .bx-shortcut-actions{flex:1;position:relative}.bx-controller-shortcuts-manager-container .bx-shortcut-row .bx-shortcut-actions select{width:100%;height:100%;min-height:38px;display:block}.bx-controller-shortcuts-manager-container .bx-shortcut-row .bx-shortcut-actions select:first-of-type{position:absolute;top:0;left:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row .bx-shortcut-actions select:last-of-type{opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}.bx-controller-shortcuts-manager-container select:disabled{text-align:left;text-align-last:left}.bx-keyboard-shortcuts-manager-container{display:flex;flex-direction:column;gap:16px}.bx-keyboard-shortcuts-manager-container fieldset{background:#2a2a2a;border:1px solid #2a2a2a;border-radius:4px;padding:4px}.bx-keyboard-shortcuts-manager-container legend{width:auto;padding:4px 8px;margin:0 4px 4px;background:#004f87;box-shadow:0 2px 0 #071e3d;border-radius:4px;font-size:14px;font-weight:bold;text-transform:uppercase}.bx-keyboard-shortcuts-manager-container .bx-settings-row{background:none}.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-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;padding:10px;margin-left:48px;width:450px;max-width:calc(100vw - tabsWidth);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents > div[data-tab-group=mkb]{display:flex;flex-direction:column;height:100%;overflow:hidden}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;min-height:var(--bx-button-height);align-content:center}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-row[data-multi-lines="true"]{flex-direction:column}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label{align-self:start}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label + *{margin:unset}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;margin-bottom:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:6px;border-top-right-radius:6px}.bx-settings-tab-contents > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:6px;border-bottom-right-radius:6px}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:6px}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861}.bx-suggest-toggler label{flex:1;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}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-sub-content-box{background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-settings-row .bx-sub-content-box{background:#202020;padding:12px;box-shadow:0 0 4px #000 inset;border-radius:6px}.bx-controller-extra-settings[data-has-gamepad=true] > :first-child{display:none}.bx-controller-extra-settings[data-has-gamepad=true] > :last-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :first-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :last-child{display:none}.bx-controller-extra-settings .bx-controller-extra-wrapper{flex:1;min-width:1px}.bx-controller-extra-settings .bx-sub-content-box{flex:1;text-align:left;display:flex;flex-direction:column;margin-top:10px}.bx-controller-extra-settings .bx-sub-content-box > label{font-size:14px}.bx-preset-row{display:flex;gap:8px}.bx-preset-row .bx-select{flex:1}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-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}select.bx-select{min-height:30px}div.bx-select{display:flex;align-items:center;flex:0 1 auto;gap:8px}div.bx-select select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}div.bx-select select:disabled ~ button{display:none}div.bx-select select:disabled ~ div{background:#131416;color:#fff;pointer-events:none}div.bx-select select:disabled ~ div .bx-select-indicators{visibility:hidden}div.bx-select > div,div.bx-select button.bx-select-value{min-width:120px;text-align:left;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;display:flex;flex:1;flex-direction:column}div.bx-select > div{min-height:24px;box-sizing:content-box}div.bx-select > div input{display:inline-block;margin-right:8px}div.bx-select > div label{margin-bottom:0;font-size:14px;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:initial;white-space:pre}div.bx-select button.bx-select-value{border:none;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}div.bx-select button.bx-select-value > div{display:flex;width:100%}div.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}div.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}div.bx-select button.bx-select-value:hover input,div.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}div.bx-select button.bx-select-value:hover::after,div.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}div.bx-select button.bx-button{border:none;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}div.bx-select button.bx-button span{line-height:unset}.bx-select-indicators{display:flex;height:4px;gap:2px;margin-bottom:2px}.bx-select-indicators span{content:\' \';display:inline-block;flex:1;background:#cfcfcf;border-radius:4px}.bx-select-indicators span[data-highlighted]{background:#9c9c9c}.bx-select-indicators span[data-selected]{background:#aacfe7}.bx-select-indicators span[data-highlighted][data-selected]{background:#5fa3d0}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}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 > div{display:flex;align-items:center}.bx-number-stepper > div span{flex:1;display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);font-size:13px;margin:0 4px}.bx-number-stepper > div button{flex-shrink:0;border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper > div button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper > div button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper > div button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type="range"]{display:block;margin:8px 0 2px auto;min-width:180px;width:100%;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button,.bx-number-stepper[disabled=true] button{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-transparent=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{min-width:60px;display:inline-block;text-align:right;vertical-align:middle}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-stats-bar span:first-of-type{min-width:22px}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;bottom:40px;transform:translateX(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:300px;opacity:.9;display:flex;flex-direction:column;gap:10px}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > p{margin:0;width:100%;font-size:22px;margin-bottom:4px;font-weight:bold;text-align:left}.bx-mkb-pointer-lock-msg > div{width:100%;display:flex;flex-direction:row;gap:10px}.bx-mkb-pointer-lock-msg > div button:first-of-type{flex-shrink:1}.bx-mkb-pointer-lock-msg > div button:last-of-type{flex-grow:1}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center;gap:20px}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:0 0 10px;font-size:12px;text-align:center}button.bx-binding-button{flex:1;min-height:38px;border:none;border-radius:4px;font-size:14px;color:#fff;display:flex;align-items:center;align-self:center;padding:0 6px}button.bx-binding-button:disabled{background:#131416;padding:0 8px}button.bx-binding-button:not(:disabled){border:2px solid transparent;border-top:none;border-bottom:4px solid #252525;background:#3b3b3b;cursor:pointer}button.bx-binding-button:not(:disabled):hover,button.bx-binding-button:not(:disabled).bx-focusable:focus{background:#20b217;border-bottom-color:#186c13}button.bx-binding-button:not(:disabled):active{background:#16900f;border-bottom:3px solid #0c4e08;border-left-width:2px;border-right-width:2px}button.bx-binding-button:not(:disabled).bx-focusable:focus::after{top:-6px;left:-8px;right:-8px;bottom:-10px}.bx-settings-row .bx-binding-button-wrapper button.bx-binding-button{min-width:60px}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getPref("uiHideSections"), selectorToHide = []; if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]"); + if (getPref("byogDisabled")) selectorToHide.push("#BodyContent > div[class*=ByogRow-module__container___]"); if (PREF_HIDE_SECTIONS.includes("all-games")) selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__gridContainer]"), selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__rowHeader]"); if (PREF_HIDE_SECTIONS.includes("most-popular")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/popular"])'); if (PREF_HIDE_SECTIONS.includes("touch")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/touch"])'); - if (getPref("block_social_features")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]"); + if (getPref("blockSocialFeatures")) 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}"; + if (getPref("uiReduceAnimations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}"; + if (getPref("uiHideSystemMenuIcon")) 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("uiSimplifyStreamMenu")) 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}"; + if (getPref("uiHideScrollbar")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}"; let $style = CE("style", {}, css); document.documentElement.appendChild($style); } @@ -7105,22 +7892,28 @@ function preloadFonts() { 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 instance; + static getInstance() { + if (typeof MouseCursorHider.instance === "undefined") if (!getPref("mkbEnabled") && getPref("mkbHideIdleCursor")) MouseCursorHider.instance = new MouseCursorHider; + else MouseCursorHider.instance = null; + return MouseCursorHider.instance; } - static hide() { - document.body && (document.body.style.cursor = "none"), MouseCursorHider.#timeout = null, MouseCursorHider.#cursorVisible = !1; + timeoutId; + isCursorVisible = !0; + show() { + document.body && (document.body.style.cursor = "unset"), this.isCursorVisible = !0; } - static onMouseMove(e) { - !MouseCursorHider.#cursorVisible && MouseCursorHider.show(), MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), MouseCursorHider.#timeout = window.setTimeout(MouseCursorHider.hide, 3000); + hide() { + document.body && (document.body.style.cursor = "none"), this.timeoutId = null, this.isCursorVisible = !1; } - static start() { - MouseCursorHider.show(), document.addEventListener("mousemove", MouseCursorHider.onMouseMove); + onMouseMove = (e) => { + !this.isCursorVisible && this.show(), this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, 3000); + }; + start() { + this.show(), document.addEventListener("mousemove", this.onMouseMove); } - static stop() { - MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), document.removeEventListener("mousemove", MouseCursorHider.onMouseMove), MouseCursorHider.show(); + stop() { + this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, document.removeEventListener("mousemove", this.onMouseMove), this.show(); } } function patchHistoryMethod(type) { @@ -7141,8 +7934,8 @@ function onHistoryChanged(e) { function setCodecPreferences(sdp, preferredCodec) { let h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || []; for (let match of matches) { - let id2 = match[1]; - if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id2); + let id = match[1]; + if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id); } if (!preferredCodecIds.length) return sdp; let lines = sdp.split(`\r @@ -7306,6 +8099,7 @@ class WebGL2Player { } else frameCallback = requestAnimationFrame; let animate = () => { if (this.stopped) return; + this.animFrameId = frameCallback(animate); let draw = !0; if (this.targetFps === 0) draw = !1; else if (this.targetFps < 60) { @@ -7317,17 +8111,16 @@ class WebGL2Player { let gl = this.gl; gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6); } - this.animFrameId = frameCallback(animate); }; this.animFrameId = frameCallback(animate); } setupShaders() { - BxLogger.info(this.LOG_TAG, "Setting up", getPref("video_power_preference")); + BxLogger.info(this.LOG_TAG, "Setting up", getPref("videoPowerPreference")); let gl = this.$canvas.getContext("webgl2", { isBx: !0, antialias: !0, alpha: !1, - powerPreference: getPref("video_power_preference") + powerPreference: getPref("videoPowerPreference") }); this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); let vShader = gl.createShader(gl.VERTEX_SHADER); @@ -7414,7 +8207,7 @@ class StreamPlayer { return filters.join(" "); } resizePlayer() { - let PREF_RATIO = getPref("video_ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas; + let PREF_RATIO = getPref("videoRatio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas; if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas(); let targetWidth, targetHeight, targetObjectFit; if (PREF_RATIO.includes(":")) { @@ -7460,7 +8253,7 @@ class StreamPlayer { } else { let filters = this.getVideoPlayerFilterStyle(), videoCss = ""; if (filters) videoCss += `filter: ${filters} !important;`; - if (getPref("screenshot_apply_filters")) ScreenshotManager.getInstance().updateCanvasFilters(filters); + if (getPref("screenshotApplyFilters")) ScreenshotManager.getInstance().updateCanvasFilters(filters); let css = ""; if (videoCss) css = `#game-stream video { ${videoCss} }`; this.$videoCss.textContent = css; @@ -7478,16 +8271,16 @@ class StreamPlayer { } } function patchVideoApi() { - let PREF_SKIP_SPLASH_VIDEO = getPref("skip_splash_video"), showFunc = function() { + let PREF_SKIP_SPLASH_VIDEO = getPref("uiSkipSplashVideo"), showFunc = function() { if (this.style.visibility = "visible", !this.videoWidth) return; let playerOptions = { - processing: getPref("video_processing"), - sharpness: getPref("video_sharpness"), - saturation: getPref("video_saturation"), - contrast: getPref("video_contrast"), - brightness: getPref("video_brightness") + processing: getPref("videoProcessing"), + sharpness: getPref("videoSharpness"), + saturation: getPref("videoSaturation"), + contrast: getPref("videoContrast"), + brightness: getPref("videoBrightness") }; - STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref("video_player_type"), playerOptions), BxEvent.dispatch(window, BxEvent.STREAM_PLAYING, { + STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref("videoPlayerType"), playerOptions), BxEvent.dispatch(window, BxEvent.STREAM_PLAYING, { $video: this }); }, nativePlay = HTMLMediaElement.prototype.play; @@ -7502,7 +8295,7 @@ function patchVideoApi() { }; } function patchRtcCodecs() { - if (getPref("stream_codec_profile") === "default") return; + if (getPref("streamCodecProfile") === "default") return; if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1; } function patchRtcPeerConnection() { @@ -7513,13 +8306,13 @@ function patchRtcPeerConnection() { dataChannel }), dataChannel; }; - let maxVideoBitrate = getPref("bitrate_video_max"), codec = getPref("stream_codec_profile"); - if (codec !== "default" || maxVideoBitrate > 0) { + let maxVideoBitrateDef = getPrefDefinition("streamMaxVideoBitrate"), maxVideoBitrate = getPref("streamMaxVideoBitrate"), codec = getPref("streamCodecProfile"); + if (codec !== "default" || maxVideoBitrate < maxVideoBitrateDef.max) { let nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; RTCPeerConnection.prototype.setLocalDescription = function(description) { if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec); try { - if (maxVideoBitrate > 0 && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); + if (maxVideoBitrate < maxVideoBitrateDef.max && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); } catch (e) { BxLogger.error("setLocalDescription", e); } @@ -7541,7 +8334,7 @@ function patchAudioContext() { let ctx = new OrgAudioContext(options); return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() { let gainNode = nativeCreateGain.apply(this); - return gainNode.gain.value = getPref("audio_volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; + return gainNode.gain.value = getPref("audioVolume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; }, STATES.currentStream.audioContext = ctx, ctx; }; } @@ -7629,39 +8422,39 @@ class ScreenshotAction extends BaseGameBarAction { constructor() { super(); this.$content = createButton({ - style: 4, + style: 8, icon: BxIcon.SCREENSHOT, title: t("take-screenshot"), - onClick: this.onClick.bind(this) + onClick: this.onClick }); } - onClick(e) { + onClick = (e) => { super.onClick(e), ScreenshotManager.getInstance().takeScreenshot(); - } + }; } class TouchControlAction extends BaseGameBarAction { $content; constructor() { super(); let $btnEnable = createButton({ - style: 4, + style: 8, icon: BxIcon.TOUCH_CONTROL_ENABLE, title: t("show-touch-controller"), - onClick: this.onClick.bind(this) + onClick: this.onClick }), $btnDisable = createButton({ - style: 4, + style: 8, icon: BxIcon.TOUCH_CONTROL_DISABLE, title: t("hide-touch-controller"), - onClick: this.onClick.bind(this), + onClick: this.onClick, classes: ["bx-activated"] }); this.$content = CE("div", {}, $btnEnable, $btnDisable); } - onClick(e) { + onClick = (e) => { super.onClick(e); let isVisible = TouchController.toggleVisibility(); this.$content.dataset.activated = (!isVisible).toString(); - } + }; reset() { this.$content.dataset.activated = "false"; } @@ -7671,25 +8464,25 @@ class MicrophoneAction extends BaseGameBarAction { constructor() { super(); let $btnDefault = createButton({ - style: 4, + style: 8, icon: BxIcon.MICROPHONE, - onClick: this.onClick.bind(this), + onClick: this.onClick, classes: ["bx-activated"] }), $btnMuted = createButton({ - style: 4, + style: 8, icon: BxIcon.MICROPHONE_MUTED, - onClick: this.onClick.bind(this) + onClick: this.onClick }); this.$content = CE("div", {}, $btnMuted, $btnDefault), window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, (e) => { let enabled = e.microphoneState === "Enabled"; this.$content.dataset.activated = enabled.toString(), this.$content.classList.remove("bx-gone"); }); } - onClick(e) { + onClick = (e) => { super.onClick(e); let enabled = MicrophoneShortcut.toggle(!1); this.$content.dataset.activated = enabled.toString(); - } + }; reset() { this.$content.classList.add("bx-gone"), this.$content.dataset.activated = "false"; } @@ -7699,27 +8492,27 @@ class TrueAchievementsAction extends BaseGameBarAction { constructor() { super(); this.$content = createButton({ - style: 4, + style: 8, icon: BxIcon.TRUE_ACHIEVEMENTS, - onClick: this.onClick.bind(this) + onClick: this.onClick }); } - onClick(e) { + onClick = (e) => { super.onClick(e), TrueAchievements.getInstance().open(!1); - } + }; } class SpeakerAction extends BaseGameBarAction { $content; constructor() { super(); let $btnEnable = createButton({ - style: 4, + style: 8, icon: BxIcon.AUDIO, - onClick: this.onClick.bind(this) + onClick: this.onClick }), $btnMuted = createButton({ - style: 4, + style: 8, icon: BxIcon.SPEAKER_MUTED, - onClick: this.onClick.bind(this), + onClick: this.onClick, classes: ["bx-activated"] }); this.$content = CE("div", {}, $btnEnable, $btnMuted), window.addEventListener(BxEvent.SPEAKER_STATE_CHANGED, (e) => { @@ -7727,50 +8520,46 @@ class SpeakerAction extends BaseGameBarAction { this.$content.dataset.activated = (!enabled).toString(); }); } - onClick(e) { + onClick = (e) => { super.onClick(e), SoundShortcut.muteUnmute(); - } + }; reset() { this.$content.dataset.activated = "false"; } } -class RendererShortcut { - static toggleVisibility() { - let $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]'); - if (!$mediaContainer) return !0; - $mediaContainer.classList.toggle("bx-gone"); - let isShowing = !$mediaContainer.classList.contains("bx-gone"); - return limitVideoPlayerFps(isShowing ? getPref("video_max_fps") : 0), isShowing; - } -} class RendererAction extends BaseGameBarAction { $content; constructor() { super(); let $btnDefault = createButton({ - style: 4, + style: 8, icon: BxIcon.EYE, - onClick: this.onClick.bind(this) + onClick: this.onClick }), $btnActivated = createButton({ - style: 4, + style: 8, icon: BxIcon.EYE_SLASH, - onClick: this.onClick.bind(this), + onClick: this.onClick, classes: ["bx-activated"] }); - this.$content = CE("div", {}, $btnDefault, $btnActivated); - } - onClick(e) { - super.onClick(e); - let isVisible = RendererShortcut.toggleVisibility(); - this.$content.dataset.activated = (!isVisible).toString(); + this.$content = CE("div", {}, $btnDefault, $btnActivated), window.addEventListener(BxEvent.VIDEO_VISIBILITY_CHANGED, (e) => { + let isShowing = e.isShowing; + this.$content.dataset.activated = (!isShowing).toString(); + }); } + onClick = (e) => { + super.onClick(e), RendererShortcut.toggleVisibility(); + }; reset() { this.$content.dataset.activated = "false"; } } class GameBar { static instance; - static getInstance = () => GameBar.instance ?? (GameBar.instance = new GameBar); + static getInstance() { + if (typeof GameBar.instance === "undefined") if (getPref("gameBarPosition") !== "off") GameBar.instance = new GameBar; + else GameBar.instance = null; + return GameBar.instance; + } LOG_TAG = "GameBar"; static VISIBLE_DURATION = 2000; $gameBar; @@ -7779,10 +8568,10 @@ class GameBar { actions = []; constructor() { BxLogger.info(this.LOG_TAG, "constructor()"); - let $container, position = getPref("game_bar_position"), $gameBar = CE("div", { id: "bx-game-bar", class: "bx-gone", "data-position": position }, $container = CE("div", { class: "bx-game-bar-container bx-offscreen" }), createSvgIcon(position === "bottom-left" ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT)); + let $container, position = getPref("gameBarPosition"), $gameBar = CE("div", { id: "bx-game-bar", class: "bx-gone", "data-position": position }, $container = CE("div", { class: "bx-game-bar-container bx-offscreen" }), createSvgIcon(position === "bottom-left" ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT)); if (this.actions = [ new ScreenshotAction, - ...STATES.userAgent.capabilities.touch && getPref("stream_touch_controller") !== "off" ? [new TouchControlAction] : [], + ...STATES.userAgent.capabilities.touch && getPref("touchControllerMode") !== "off" ? [new TouchControlAction] : [], new SpeakerAction, new RendererAction, new MicrophoneAction, @@ -7794,20 +8583,20 @@ class GameBar { $gameBar.addEventListener("click", (e) => { if (e.target !== $gameBar) return; $container.classList.contains("bx-show") ? this.hideBar() : this.showBar(); - }), window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar.bind(this)), $container.addEventListener("pointerover", this.clearHideTimeout.bind(this)), $container.addEventListener("pointerout", this.beginHideTimeout.bind(this)), $container.addEventListener("transitionend", (e) => { + }), window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar), $container.addEventListener("pointerover", this.clearHideTimeout), $container.addEventListener("pointerout", this.beginHideTimeout), $container.addEventListener("transitionend", (e) => { $container.classList.replace("bx-hide", "bx-offscreen"); - }), document.documentElement.appendChild($gameBar), this.$gameBar = $gameBar, this.$container = $container, getPref("game_bar_position") !== "off" && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e) => { - if (STATES.isPlaying) e.mode !== "none" ? this.disable() : this.enable(); + }), document.documentElement.appendChild($gameBar), this.$gameBar = $gameBar, this.$container = $container, position !== "off" && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e) => { + if (STATES.isPlaying) window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none" ? this.disable() : this.enable(); }).bind(this)); } - beginHideTimeout() { + beginHideTimeout = () => { this.clearHideTimeout(), this.timeoutId = window.setTimeout(() => { this.timeoutId = null, this.hideBar(); }, GameBar.VISIBLE_DURATION); - } - clearHideTimeout() { + }; + clearHideTimeout = () => { this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null; - } + }; enable() { this.$gameBar.classList.remove("bx-gone"); } @@ -7817,9 +8606,9 @@ class GameBar { showBar() { this.$container.classList.remove("bx-offscreen", "bx-hide", "bx-gone"), this.$container.classList.add("bx-show"), this.beginHideTimeout(); } - hideBar() { + hideBar = () => { this.clearHideTimeout(), this.$container.classList.replace("bx-show", "bx-hide"); - } + }; reset() { for (let action of this.actions) action.reset(); @@ -7834,8 +8623,8 @@ class XcloudApi { constructor() { BxLogger.info(this.LOG_TAG, "constructor()"); } - async getTitleInfo(id2) { - if (id2 in this.CACHE_TITLES) return this.CACHE_TITLES[id2]; + async getTitleInfo(id) { + if (id in this.CACHE_TITLES) return this.CACHE_TITLES[id]; let baseUri = STATES.selectedRegion.baseUri; if (!baseUri || !STATES.gsToken) return null; let json; @@ -7847,22 +8636,22 @@ class XcloudApi { "Content-Type": "application/json" }, body: JSON.stringify({ - alternateIds: [id2], + alternateIds: [id], alternateIdType: "productId" }) })).json()).results[0]; } catch (e) { json = {}; } - return this.CACHE_TITLES[id2] = json, json; + return this.CACHE_TITLES[id] = json, json; } - async getWaitTime(id2) { - if (id2 in this.CACHE_WAIT_TIME) return this.CACHE_WAIT_TIME[id2]; + async getWaitTime(id) { + if (id in this.CACHE_WAIT_TIME) return this.CACHE_WAIT_TIME[id]; let baseUri = STATES.selectedRegion.baseUri; if (!baseUri || !STATES.gsToken) return null; let json; try { - json = await (await NATIVE_FETCH(`${baseUri}/v1/waittime/${id2}`, { + json = await (await NATIVE_FETCH(`${baseUri}/v1/waittime/${id}`, { method: "GET", headers: { Authorization: `Bearer ${STATES.gsToken}` @@ -7871,12 +8660,12 @@ class XcloudApi { } catch (e) { json = {}; } - return this.CACHE_WAIT_TIME[id2] = json, json; + return this.CACHE_WAIT_TIME[id] = json, json; } } class GameTile { - static #timeout; - static async#showWaitTime($elm, productId) { + static timeoutId; + static async showWaitTime($elm, productId) { if ($elm.hasWaitTime) return; $elm.hasWaitTime = !0; let totalWaitTime, api = XcloudApi.getInstance(), info = await api.getTitleInfo(productId); @@ -7889,12 +8678,12 @@ class GameTile { $elm.insertAdjacentElement("afterbegin", $div); } } - static #requestWaitTime($elm, productId) { - GameTile.#timeout && clearTimeout(GameTile.#timeout), GameTile.#timeout = window.setTimeout(async () => { - GameTile.#showWaitTime($elm, productId); + static requestWaitTime($elm, productId) { + GameTile.timeoutId && clearTimeout(GameTile.timeoutId), GameTile.timeoutId = window.setTimeout(async () => { + GameTile.showWaitTime($elm, productId); }, 500); } - static #findProductId($elm) { + static findProductId($elm) { let productId = null; try { if ($elm.tagName === "BUTTON" && $elm.className.includes("MruGameCard") || $elm.tagName === "A" && $elm.className.includes("GameCard")) { @@ -7915,12 +8704,12 @@ class GameTile { if (($elm.className || "").includes("MruGameCard")) { let $ol = $elm.closest("ol"); if ($ol && !$ol.hasWaitTime) $ol.hasWaitTime = !0, $ol.querySelectorAll("button[class*=MruGameCard]").forEach(($elm2) => { - let productId = GameTile.#findProductId($elm2); - productId && GameTile.#showWaitTime($elm2, productId); + let productId = GameTile.findProductId($elm2); + productId && GameTile.showWaitTime($elm2, productId); }); } else { - let productId = GameTile.#findProductId($elm); - productId && GameTile.#requestWaitTime($elm, productId); + let productId = GameTile.findProductId($elm); + productId && GameTile.requestWaitTime($elm, productId); } }); } @@ -7929,8 +8718,7 @@ class ProductDetailsPage { static $btnShortcut = AppInterface && createButton({ icon: BxIcon.CREATE_SHORTCUT, label: t("create-shortcut"), - style: 32, - tabIndex: 0, + style: 64, onClick: (e) => { AppInterface.createShortcut(window.location.pathname.substring(6)); } @@ -7938,8 +8726,7 @@ class ProductDetailsPage { static $btnWallpaper = AppInterface && createButton({ icon: BxIcon.DOWNLOAD, label: t("wallpaper"), - style: 32, - tabIndex: 0, + style: 64, onClick: (e) => { let details = parseDetailsPath(window.location.pathname); details && AppInterface.downloadWallpapers(details.titleSlug, details.productId); @@ -8016,7 +8803,7 @@ class StreamUiHandler { if ($gripHandle && $gripHandle.ariaExpanded === "true") $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(), $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(); }, $btnStreamSettings = StreamUiHandler.$btnStreamSettings; if (typeof $btnStreamSettings === "undefined") $btnStreamSettings = StreamUiHandler.cloneStreamHudButton($orgButton, t("better-xcloud"), BxIcon.BETTER_XCLOUD), $btnStreamSettings?.addEventListener("click", (e) => { - hideGripHandle(), e.preventDefault(), SettingsNavigationDialog.getInstance().show(); + hideGripHandle(), e.preventDefault(), SettingsDialog.getInstance().show(); }), StreamUiHandler.$btnStreamSettings = $btnStreamSettings; let streamStats = StreamStats.getInstance(), $btnStreamStats = StreamUiHandler.$btnStreamStats; if (typeof $btnStreamStats === "undefined") $btnStreamStats = StreamUiHandler.cloneStreamHudButton($orgButton, t("stream-stats"), BxIcon.STREAM_STATS), $btnStreamStats?.addEventListener("click", async (e) => { @@ -8080,8 +8867,7 @@ class RootDialogObserver { static $btnShortcut = AppInterface && createButton({ icon: BxIcon.CREATE_SHORTCUT, label: t("create-shortcut"), - style: 32 | 4 | 64 | 1024 | 2048, - tabIndex: 0, + style: 64 | 8 | 128 | 2048 | 4096, onClick: (e) => { window.BX_EXPOSED.dialogRoutes?.closeAll(); let $btn = e.target.closest("button"); @@ -8091,8 +8877,7 @@ class RootDialogObserver { static $btnWallpaper = AppInterface && createButton({ icon: BxIcon.DOWNLOAD, label: t("wallpaper"), - style: 32 | 4 | 64 | 1024 | 2048, - tabIndex: 0, + style: 64 | 8 | 128 | 2048 | 4096, onClick: (e) => { window.BX_EXPOSED.dialogRoutes?.closeAll(); let $btn = e.target.closest("button"), details = parseDetailsPath($btn.dataset.path); @@ -8140,6 +8925,90 @@ class RootDialogObserver { observer.observe(document.documentElement, { subtree: !0, childList: !0 }); } } +class KeyboardShortcutHandler { + static instance; + static getInstance = () => KeyboardShortcutHandler.instance ?? (KeyboardShortcutHandler.instance = new KeyboardShortcutHandler); + start() { + window.addEventListener("keydown", this.onKeyDown); + } + stop() { + window.removeEventListener("keydown", this.onKeyDown); + } + onKeyDown = (e) => { + if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return; + if (e.repeat) return; + let fullKeyCode = KeyHelper.getFullKeyCodeFromEvent(e); + if (!fullKeyCode) return; + let action = window.BX_STREAM_SETTINGS.keyboardShortcuts[fullKeyCode]; + if (action) e.preventDefault(), e.stopPropagation(), ShortcutHandler.runAction(action); + }; +} +var VIBRATION_DATA_MAP = { + gamepadIndex: 8, + leftMotorPercent: 8, + rightMotorPercent: 8, + leftTriggerMotorPercent: 8, + rightTriggerMotorPercent: 8, + durationMs: 16 +}; +class DeviceVibrationManager { + static instance; + static getInstance() { + if (typeof DeviceVibrationManager.instance === "undefined") if (STATES.browser.capabilities.deviceVibration) DeviceVibrationManager.instance = new DeviceVibrationManager; + else DeviceVibrationManager.instance = null; + return DeviceVibrationManager.instance; + } + dataChannel = null; + boundOnMessage; + constructor() { + this.boundOnMessage = this.onMessage.bind(this), window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { + let dataChannel = e.dataChannel; + if (dataChannel?.label === "input") this.reset(), this.dataChannel = dataChannel, this.setupDataChannel(); + }), window.addEventListener(BxEvent.DEVICE_VIBRATION_CHANGED, (e) => { + this.setupDataChannel(); + }); + } + setupDataChannel() { + if (!this.dataChannel) return; + if (this.removeEventListeners(), window.BX_STREAM_SETTINGS.deviceVibrationIntensity > 0) this.dataChannel.addEventListener("message", this.boundOnMessage); + } + playVibration(data) { + let vibrationIntensity = StreamSettings.settings.deviceVibrationIntensity; + if (AppInterface) { + AppInterface.vibrate(JSON.stringify(data), vibrationIntensity); + return; + } + let realIntensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * vibrationIntensity; + if (realIntensity === 0 || realIntensity === 100) { + window.navigator.vibrate(realIntensity ? data.durationMs : 0); + return; + } + let pulseDuration = 200, onDuration = Math.floor(pulseDuration * realIntensity / 100), offDuration = pulseDuration - onDuration, repeats = Math.ceil(data.durationMs / pulseDuration), pulses = Array(repeats).fill([onDuration, offDuration]).flat(); + window.navigator.vibrate(pulses); + } + onMessage(e) { + if (typeof e !== "object" || !(e.data instanceof ArrayBuffer)) return; + let dataView = new DataView(e.data), offset = 0, messageType; + if (dataView.byteLength === 13) messageType = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT; + else messageType = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT; + if (!(messageType & 128)) return; + let vibrationType = dataView.getUint8(offset); + if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0) return; + let data = {}, key; + for (key in VIBRATION_DATA_MAP) + if (VIBRATION_DATA_MAP[key] === 16) data[key] = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT; + else data[key] = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT; + this.playVibration(data); + } + removeEventListeners() { + try { + this.dataChannel?.removeEventListener("message", this.boundOnMessage); + } catch (e) {} + } + reset() { + this.removeEventListeners(), this.dataChannel = null; + } +} if (window.location.pathname.includes("/auth/msa")) { let nativePushState = window.history.pushState; throw window.history.pushState = function(...args) { @@ -8174,9 +9043,9 @@ window.addEventListener("load", (e) => { }); document.addEventListener("readystatechange", (e) => { if (document.readyState !== "interactive") return; - if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) getPref("xhome_enabled") && RemotePlayManager.getInstance().initialize(); + if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) RemotePlayManager.getInstance()?.initialize(); else window.setTimeout(HeaderSection.watchHeader, 2000); - if (getPref("ui_hide_sections").includes("friends")) { + if (getPref("uiHideSections").includes("friends")) { let $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]"); $parent && ($parent.style.display = "none"); } @@ -8188,7 +9057,7 @@ window.addEventListener("popstate", onHistoryChanged); window.history.pushState = patchHistoryMethod("pushState"); window.history.replaceState = patchHistoryMethod("replaceState"); window.addEventListener(BxEvent.XCLOUD_SERVERS_UNAVAILABLE, (e) => { - if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsNavigationDialog.getInstance().show(); + if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show(); }, { once: !0 }); window.addEventListener(BxEvent.XCLOUD_SERVERS_READY, (e) => { STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000); @@ -8197,18 +9066,22 @@ 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.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(); + LoadingScreen.hide(); + { + let cursorHider = MouseCursorHider.getInstance(); + if (cursorHider) cursorHider.start(), cursorHider.hide(); + } }); window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { - if (STATES.isPlaying = !0, StreamUiHandler.observe(), getPref("game_bar_position") !== "off") { - let gameBar = GameBar.getInstance(); - gameBar.reset(), gameBar.enable(), gameBar.showBar(); - } + window.BX_STREAM_SETTINGS = StreamSettings.settings, StreamSettings.refreshAllSettings(), STATES.isPlaying = !0, StreamUiHandler.observe(); { + let gameBar = GameBar.getInstance(); + if (gameBar) gameBar.reset(), gameBar.enable(), gameBar.showBar(); + KeyboardShortcutHandler.getInstance().start(); let $video = e.$video; - ScreenshotManager.getInstance().updateCanvasSize($video.videoWidth, $video.videoHeight); + ScreenshotManager.getInstance().updateCanvasSize($video.videoWidth, $video.videoHeight), BxExposed.toggleLocalCoOp(getPref("localCoOpEnabled")); } updateVideoPlayer(); }); @@ -8236,7 +9109,7 @@ window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { }); function unload() { if (!STATES.isPlaying) return; - EmulatedMkbHandler.getInstance().destroy(), NativeMkbHandler.getInstance().destroy(), STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().destroy(), StreamBadges.getInstance().destroy(), MouseCursorHider.stop(), TouchController.reset(), getPref("game_bar_position") !== "off" && GameBar.getInstance().disable(); + KeyboardShortcutHandler.getInstance().stop(), EmulatedMkbHandler.getInstance()?.destroy(), NativeMkbHandler.getInstance()?.destroy(), DeviceVibrationManager.getInstance()?.reset(), STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().destroy(), StreamBadges.getInstance().destroy(), MouseCursorHider.getInstance()?.stop(), TouchController.reset(), GameBar.getInstance()?.disable(); } window.addEventListener(BxEvent.STREAM_STOPPED, unload); window.addEventListener("pagehide", (e) => { @@ -8246,11 +9119,14 @@ window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, (e) => { ScreenshotManager.getInstance().takeScreenshot(); }); function main() { - if (getPref("game_msfs2020_force_native_mkb")) BX_FLAGS.ForceNativeMkbTitles.push("9PMQDM08SNK9"); - if (patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getPref("audio_enable_volume_control") && patchAudioContext(), getPref("block_tracking")) patchMeControl(), disableAdobeAudienceManager(); - if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), updatePollingRate(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), VibrationManager.initialSetup(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getPref("xhome_enabled")) RemotePlayManager.detect(); - if (getPref("stream_touch_controller") === "all") TouchController.setup(); - if (getPref("mkb_enabled") && AppInterface) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString()); - if (getPref("ui_game_card_show_wait_time") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getPref("controller_show_connection_status")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); + if (GhPagesUtils.fetchLatestCommit(), getPref("nativeMkbMode") === "on") { + let customList = getPref("forceNativeMkbGames"); + BX_FLAGS.ForceNativeMkbTitles.push(...customList); + } + if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getPref("audioEnableVolumeControl") && patchAudioContext(), getPref("blockTracking")) patchMeControl(), disableAdobeAudienceManager(); + if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getPref("xhomeEnabled")) RemotePlayManager.detect(); + if (getPref("touchControllerMode") === "all") TouchController.setup(); + if (getPref("mkbEnabled") && AppInterface) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString()); + if (getPref("uiGameCardShowWaitTime") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getPref("uiShowControllerStatus")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); } main();