diff --git a/dist/better-xcloud.pretty.user.js b/dist/better-xcloud.pretty.user.js index 520fbe3..f54cb37 100644 --- a/dist/better-xcloud.pretty.user.js +++ b/dist/better-xcloud.pretty.user.js @@ -68,6 +68,7 @@ var ALL_PREFS = { "stream.video.maxBitrate", "stream.locale", "stream.video.resolution", + "stream.video.preventResolutionDrops", "touchController.autoOff", "touchController.opacity.default", "touchController.mode", @@ -640,6 +641,7 @@ var SUPPORTED_LANGUAGES = { e => `按下 ${e.key} 來啟用此功能` ], "press-to-bind": "Press a key or do a mouse click to bind...", + "prevent-resolution-drops": "Prevent resolution drops", "prompt-preset-name": "Preset's name:", quality: "Quality", recommended: "Recommended", @@ -1375,6 +1377,11 @@ class GlobalSettingsStorage extends BaseSettingsStorage { highest: "1080p-hq" } }, + "stream.video.preventResolutionDrops": { + label: t("prevent-resolution-drops"), + default: !1, + note: CE("a", { href: "https://github.com/redphx/better-xcloud/issues/791", target: "_blank" }, "⚠️ " + t("unexpected-behavior")) + }, "stream.video.codecProfile": { label: t("visual-quality"), default: "default", @@ -5739,15 +5746,17 @@ ${subsVar} = subs; patchStreamMetadata(str) { let index = str.indexOf("}onVideoFrame("); if (index >= 0 && (index = PatcherUtils.indexOf(str, "){", index, 30, !0)), index < 0) return !1; - let code = ` + let maxDt = 10, code = ` try { const obj = arguments[0]; - const baseMs = obj.frameSubmittedTimeMs; - const renderMs = obj.frameRenderedTimeMs - obj.frameDecodedTimeMs; - obj.frameDecodedTimeMs = baseMs + ${1}; - obj.frameRenderedTimeMs = obj.frameDecodedTimeMs + renderMs; - obj.expectedDisplayTime = obj.frameRenderedTimeMs; - arguments[0] = obj; + if (true || obj.frameDecodedTimeMs - obj.frameSubmittedTimeMs > ${maxDt}) { + const baseMs = obj.frameSubmittedTimeMs; + const renderMs = obj.frameRenderedTimeMs - obj.frameDecodedTimeMs; + obj.frameDecodedTimeMs = baseMs + ${maxDt}; + obj.frameRenderedTimeMs = obj.frameDecodedTimeMs + renderMs; + obj.expectedDisplayTime = obj.frameRenderedTimeMs; + arguments[0] = obj; + } } catch (e) { alert(e) } `; return str = PatcherUtils.insertAt(str, index, code), str; @@ -5826,7 +5835,7 @@ try { "playVibration", "alwaysShowStreamHud", "injectStreamMenuUseEffect", - "patchStreamMetadata", + getGlobalPref("stream.video.preventResolutionDrops") && "patchStreamMetadata", getGlobalPref("audio.volume.booster.enabled") && !getGlobalPref("stream.video.combineAudio") && "patchAudioMediaStream", getGlobalPref("audio.volume.booster.enabled") && getGlobalPref("stream.video.combineAudio") && "patchCombinedAudioVideoMediaStream", getGlobalPref("ui.feedbackDialog.disabled") && "skipFeedbackDialog", @@ -7280,6 +7289,7 @@ class SettingsDialog extends NavigationDialog { "stream.video.resolution", "stream.video.codecProfile", "stream.video.maxBitrate", + "stream.video.preventResolutionDrops", "audio.volume.booster.enabled", "screenshot.applyFilters", "audio.mic.onPlaying", diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index a619ab0..5389d0e 100755 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -22,7 +22,7 @@ try {delete window.BX_FLAGS;} catch (e) {} if (!BX_FLAGS.DeviceInfo.userAgent) BX_FLAGS.DeviceInfo.userAgent = window.navigator.userAgent; BxLogger.info("BxFlags", BX_FLAGS); var NATIVE_FETCH = window.fetch; -var ALL_PREFS = {global: ["audio.mic.onPlaying","audio.volume.booster.enabled","block.features","block.tracking","gameBar.position","game.fortnite.forceConsole","loadingScreen.gameArt.show","loadingScreen.rocket","loadingScreen.waitTime.show","mkb.enabled","mkb.cursor.hideIdle","nativeMkb.forcedGames","nativeMkb.mode","xhome.video.resolution","xhome.ipv6.prefer","screenshot.applyFilters","server.bypassRestriction","server.ipv6.prefer","server.region","stream.video.codecProfile","stream.video.combineAudio","stream.video.maxBitrate","stream.locale","stream.video.resolution","touchController.autoOff","touchController.opacity.default","touchController.mode","touchController.style.custom","touchController.style.standard","ui.controllerFriendly","ui.controllerStatus.show","ui.feedbackDialog.disabled","ui.gameCard.waitTime.show","ui.hideSections","ui.systemMenu.hideHandle","ui.imageQuality","ui.layout","ui.reduceAnimations","ui.hideScrollbar","ui.streamMenu.simplify","ui.splashVideo.skip","ui.theme","version.current","version.lastCheck","version.latest","bx.locale","userAgent.profile"],stream: ["audio.volume","controller.pollingRate","controller.settings","deviceVibration.intensity","deviceVibration.mode","keyboardShortcuts.preset.inGameId","localCoOp.enabled","mkb.p1.preset.mappingId","mkb.p1.slot","mkb.p2.preset.mappingId","mkb.p2.slot","nativeMkb.scroll.sensitivityX","nativeMkb.scroll.sensitivityY","stats.colors","stats.items","stats.opacity.all","stats.opacity.background","stats.position","stats.quickGlance.enabled","stats.showWhenPlaying","stats.textSize","video.brightness","video.contrast","video.maxFps","video.player.type","video.position","video.player.powerPreference","video.processing","video.processing.mode","video.ratio","video.saturation","video.processing.sharpness"]}; +var ALL_PREFS = {global: ["audio.mic.onPlaying","audio.volume.booster.enabled","block.features","block.tracking","gameBar.position","game.fortnite.forceConsole","loadingScreen.gameArt.show","loadingScreen.rocket","loadingScreen.waitTime.show","mkb.enabled","mkb.cursor.hideIdle","nativeMkb.forcedGames","nativeMkb.mode","xhome.video.resolution","xhome.ipv6.prefer","screenshot.applyFilters","server.bypassRestriction","server.ipv6.prefer","server.region","stream.video.codecProfile","stream.video.combineAudio","stream.video.maxBitrate","stream.locale","stream.video.resolution","stream.video.preventResolutionDrops","touchController.autoOff","touchController.opacity.default","touchController.mode","touchController.style.custom","touchController.style.standard","ui.controllerFriendly","ui.controllerStatus.show","ui.feedbackDialog.disabled","ui.gameCard.waitTime.show","ui.hideSections","ui.systemMenu.hideHandle","ui.imageQuality","ui.layout","ui.reduceAnimations","ui.hideScrollbar","ui.streamMenu.simplify","ui.splashVideo.skip","ui.theme","version.current","version.lastCheck","version.latest","bx.locale","userAgent.profile"],stream: ["audio.volume","controller.pollingRate","controller.settings","deviceVibration.intensity","deviceVibration.mode","keyboardShortcuts.preset.inGameId","localCoOp.enabled","mkb.p1.preset.mappingId","mkb.p1.slot","mkb.p2.preset.mappingId","mkb.p2.slot","nativeMkb.scroll.sensitivityX","nativeMkb.scroll.sensitivityY","stats.colors","stats.items","stats.opacity.all","stats.opacity.background","stats.position","stats.quickGlance.enabled","stats.showWhenPlaying","stats.textSize","video.brightness","video.contrast","video.maxFps","video.player.type","video.position","video.player.powerPreference","video.processing","video.processing.mode","video.ratio","video.saturation","video.processing.sharpness"]}; var SMART_TV_UNIQUE_ID = "FC4A1DA2-711C-4E9C-BC7F-047AF8A672EA", CHROMIUM_VERSION = "140.0.3485.54"; if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) {let match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/);if (match) CHROMIUM_VERSION = match[1];} class UserAgent {static STORAGE_KEY = "BetterXcloud.UserAgent";static #config;static #isMobile = null;static #isSafari = null;static #isSafariMobile = null;static #USER_AGENTS = {"windows-edge": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,"macos-safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1","smarttv-generic": `${window.navigator.userAgent} Smart-TV`,"smarttv-tizen": `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`,"vr-oculus": window.navigator.userAgent + " OculusBrowser VR"};static init() {if (UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}"), !UserAgent.#config.profile) UserAgent.#config.profile = BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default";if (!UserAgent.#config.custom) UserAgent.#config.custom = "";UserAgent.spoof();}static updateStorage(profile, custom) {let config = UserAgent.#config;if (config.profile = profile, profile === "custom" && typeof custom !== "undefined") config.custom = custom;window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(config));}static getDefault() {return window.navigator.orgUserAgent || window.navigator.userAgent;}static get(profile) {let defaultUserAgent = window.navigator.userAgent;switch (profile) {case "default":return defaultUserAgent;case "custom":return UserAgent.#config.custom || defaultUserAgent;default:return UserAgent.#USER_AGENTS[profile] || defaultUserAgent;}}static isSafari() {if (this.#isSafari !== null) return this.#isSafari;let userAgent = UserAgent.getDefault().toLowerCase(), result = userAgent.includes("safari") && !userAgent.includes("chrom");return this.#isSafari = result, result;}static isSafariMobile() {if (this.#isSafariMobile !== null) return this.#isSafariMobile;let userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile");return this.#isSafariMobile = result, result;}static isMobile() {if (this.#isMobile !== null) return this.#isMobile;let userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent);return this.#isMobile = result, result;}static spoof() {let profile = UserAgent.#config.profile;if (profile === "default") return;let newUserAgent = UserAgent.get(profile);if ("userAgentData" in window.navigator) window.navigator.orgUserAgentData = window.navigator.userAgentData, Object.defineProperty(window.navigator, "userAgentData", {});window.navigator.orgUserAgent = window.navigator.userAgent, Object.defineProperty(window.navigator, "userAgent", {value: newUserAgent});}} @@ -37,7 +37,7 @@ var GamepadKeyName = {0: ["A", "⇓"],1: ["B", "⇒"],2: ["X", "⇐"],3: ["Y", " class BxEventBus {listeners = new Map;group;appJsInterfaces;static Script = new BxEventBus("script", {"dialog.shown": "onDialogShown","dialog.dismissed": "onDialogDismissed"});static Stream = new BxEventBus("stream", {"state.loading": "onStreamPlaying","state.playing": "onStreamPlaying","state.stopped": "onStreamStopped"});constructor(group, appJsInterfaces) {this.group = group, this.appJsInterfaces = appJsInterfaces;}on(event, callback) {if (!this.listeners.has(event)) this.listeners.set(event, new Set);this.listeners.get(event).add(callback), BX_FLAGS.Debug && BxLogger.warning("EventBus", "on", event, callback);}once(event, callback) {let wrapper = (...args) => {callback(...args), this.off(event, wrapper);};this.on(event, wrapper);}off(event, callback) {if (BX_FLAGS.Debug && BxLogger.warning("EventBus", "off", event, callback), !callback) {this.listeners.delete(event);return;}let callbacks = this.listeners.get(event);if (!callbacks) return;if (callbacks.delete(callback), callbacks.size === 0) this.listeners.delete(event);}offAll() {this.listeners.clear();}emit(event, payload) {let callbacks = this.listeners.get(event) || [];for (let callback of callbacks)callback(payload);if (AppInterface) try {if (event in this.appJsInterfaces) {let method = this.appJsInterfaces[event];if (method && method in AppInterface) AppInterface[method]();} else AppInterface.onEventBus(this.group + "." + event);} catch (e) {console.log(e);}BX_FLAGS.Debug && BxLogger.warning("EventBus", "emit", `${this.group}.${event}`, payload);}} window.BxEventBus = BxEventBus; class GhPagesUtils {static fetchLatestCommit() {NATIVE_FETCH("https://api.github.com/repos/redphx/better-xcloud/branches/gh-pages", {method: "GET",headers: {Accept: "application/vnd.github.v3+json"}}).then((response) => response.json()).then((data) => {let latestCommitHash = data.commit.sha;window.localStorage.setItem("BetterXcloud.GhPages.CommitHash", latestCommitHash);}).catch((error) => {BxLogger.error("GhPagesUtils", "Error fetching the latest commit:", error);});}static getUrl(path) {if (path[0] === "/") alert('`path` must not starts with "/"');let prefix = "https://raw.githubusercontent.com/redphx/better-xcloud", latestCommitHash = window.localStorage.getItem("BetterXcloud.GhPages.CommitHash");if (latestCommitHash) return `${prefix}/${latestCommitHash}/${path}`;else return `${prefix}/refs/heads/gh-pages/${path}`;}static getNativeMkbCustomList(update = !1) {let key = "BetterXcloud.GhPages.ForceNativeMkb";update && NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => {if (json.$schemaVersion === 1) window.localStorage.setItem(key, JSON.stringify(json)), BxEventBus.Script.emit("list.forcedNativeMkb.updated", {data: json});else window.localStorage.removeItem(key);});let info = JSON.parse(window.localStorage.getItem(key) || "{}");if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), {};return info.data;}static getTouchControlCustomList() {let key = "BetterXcloud.GhPages.CustomTouchLayouts";return NATIVE_FETCH(GhPagesUtils.getUrl("touch-layouts/ids.json")).then((response) => response.json()).then((json) => {if (Array.isArray(json)) window.localStorage.setItem(key, JSON.stringify(json));}), JSON.parse(window.localStorage.getItem(key) || "[]");}static getLocalCoOpList() {let key = "BetterXcloud.GhPages.LocalCoOp";NATIVE_FETCH(GhPagesUtils.getUrl("local-co-op/ids.json")).then((response) => response.json()).then((json) => {if (json.$schemaVersion === 1) {window.localStorage.setItem(key, JSON.stringify(json));let ids = new Set(Object.keys(json.data));BxEventBus.Script.emit("list.localCoOp.updated", { ids });} else window.localStorage.removeItem(key), BxEventBus.Script.emit("list.localCoOp.updated", { ids: new Set });});let info = JSON.parse(window.localStorage.getItem(key) || "{}");if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), new Set;return new Set(Object.keys(info.data || {}));}} -var SUPPORTED_LANGUAGES = {"en-US": "English (US)","ca-CA": "Català","cs-CZ": "čeština","da-DK": "dansk","de-DE": "Deutsch","en-ID": "Bahasa Indonesia","es-ES": "español (España)","fr-FR": "français","it-IT": "italiano","ja-JP": "日本語","ko-KR": "한국어","pl-PL": "polski","pt-BR": "português (Brasil)","ru-RU": "русский","th-TH": "ภาษาไทย","tr-TR": "Türkçe","uk-UA": "українська","vi-VN": "Tiếng Việt","zh-CN": "中文(简体)","zh-TW": "中文(繁體)"}, Texts = {achievements: "Achievements",activate: "Activate",activated: "Activated",active: "Active",advanced: "Advanced","all-games": "All games","always-off": "Always off","always-on": "Always on","amd-fidelity-cas": "AMD FidelityFX CAS","app-settings": "App settings",apply: "Apply","aspect-ratio": "Aspect ratio","aspect-ratio-note": "Don't use with native touch games",audio: "Audio",auto: "Auto",availability: "Availability","back-to-home": "Back to home","back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?","background-opacity": "Background opacity",battery: "Battery","battery-saving": "Battery saving","better-xcloud": "Better xCloud","bitrate-audio-maximum": "Maximum audio bitrate","bitrate-video-maximum": "Maximum video bitrate",bottom: "Bottom","bottom-half": "Bottom half","bottom-left": "Bottom-left","bottom-right": "Bottom-right",brazil: "Brazil",brightness: "Brightness","browser-unsupported-feature": "Your browser doesn't support this feature","button-xbox": "Xbox button","bypass-region-restriction": "Bypass region restriction",cancel: "Cancel",center: "Center",chat: "Chat","clarity-boost": "Clarity boost","clarity-boost-mode": "Clarity boost mode","clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",clear: "Clear","clear-data": "Clear data","clear-data-confirm": "Do you want to clear all Better xCloud settings and data?","clear-data-success": "Data cleared! Refresh the page to apply the changes.",clock: "Clock",close: "Close","close-app": "Close app","combine-audio-video-streams": "Combine audio & video streams","combine-audio-video-streams-summary": "May fix the laggy audio problem","conditional-formatting": "Conditional formatting text color","confirm-delete-preset": "Do you want to delete this preset?","confirm-reload-stream": "Do you want to refresh the stream?",connected: "Connected","console-connect": "Connect","continent-asia": "Asia","continent-australia": "Australia","continent-europe": "Europe","continent-north-america": "North America","continent-south-america": "South America",contrast: "Contrast",controller: "Controller","controller-customization": "Controller customization","controller-customization-input-latency-note": "May slightly increase input latency","controller-friendly-ui": "Controller-friendly UI","controller-shortcuts": "Controller shortcuts","controller-shortcuts-connect-note": "Connect a controller to use this feature","controller-shortcuts-xbox-note": "Button to open the Guide menu","controller-vibration": "Controller vibration",copy: "Copy","create-shortcut": "Shortcut",custom: "Custom","deadzone-counterweight": "Deadzone counterweight",decrease: "Decrease",default: "Default","default-opacity": "Default opacity","default-preset-note": "You can't modify default presets. Create a new one to customize it.",delete: "Delete","detect-controller-button": "Detect controller button",device: "Device","device-unsupported-touch": "Your device doesn't have touch support","device-vibration": "Device vibration","device-vibration-not-using-gamepad": "On when not using gamepad",disable: "Disable","disable-features": "Disable features","disable-home-context-menu": "Disable context menu in Home page","disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog","disable-social-features": "Disable social features","disable-xcloud-analytics": "Disable xCloud analytics",disabled: "Disabled",disconnected: "Disconnected",download: "Download",downloaded: "Downloaded",edit: "Edit","enable-controller-shortcuts": "Enable controller shortcuts","enable-local-co-op-support": "Enable local co-op support","enable-local-co-op-support-note": "Only works with some games","enable-mic-on-startup": "Enable microphone on game launch","enable-mkb": "Emulate controller with Mouse & Keyboard","enable-quick-glance-mode": 'Enable "Quick Glance" mode',"enable-remote-play-feature": 'Enable the "Remote Play" feature',"enable-volume-control": "Enable volume control feature",enabled: "Enabled",experimental: "Experimental",export: "Export",fast: "Fast","force-native-mkb-games": "Force native Mouse & Keyboard for these games","fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile',"fortnite-force-console-version": "Fortnite: force console version","friends-followers": "Friends and followers","game-bar": "Game Bar","getting-consoles-list": "Getting the list of consoles...",guide: "Guide",help: "Help",hide: "Hide","hide-idle-cursor": "Hide mouse cursor on idle","hide-scrollbar": "Hide web page's scrollbar","hide-sections": "Hide sections","hide-system-menu-icon": "Hide System menu's icon","hide-touch-controller": "Hide touch controller","high-performance": "High performance","highest-quality": "Highest quality","highest-quality-note": "Your device may not be powerful enough to use these settings","horizontal-scroll-sensitivity": "Horizontal scroll sensitivity","horizontal-sensitivity": "Horizontal sensitivity","how-to-fix": "How to fix","how-to-improve-app-performance": "How to improve app's performance",ignore: "Ignore","image-quality": "Website's image quality",import: "Import","in-game-controller-customization": "In-game controller customization","in-game-controller-shortcuts": "In-game controller shortcuts","in-game-keyboard-shortcuts": "In-game keyboard shortcuts","in-game-shortcuts": "In-game shortcuts",increase: "Increase","install-android": "Better xCloud app for Android",invites: "Invites",japan: "Japan",jitter: "Jitter","keyboard-key": "Keyboard key","keyboard-shortcuts": "Keyboard shortcuts",korea: "Korea",language: "Language",large: "Large",layout: "Layout","left-stick": "Left stick","left-stick-deadzone": "Left stick deadzone","left-trigger-range": "Left trigger range","limit-fps": "Limit FPS","load-failed-message": "Failed to run Better xCloud","loading-screen": "Loading screen","local-co-op": "Local co-op","lowest-quality": "Lowest quality",manage: "Manage","map-mouse-to": "Map mouse to","may-not-work-properly": "May not work properly!",menu: "Menu",microphone: "Microphone","mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings","mkb-click-to-activate": "Click to activate","mkb-disclaimer": "This could be viewed as cheating when playing online","modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.","mouse-and-keyboard": "Mouse & Keyboard","mouse-click": "Mouse click","mouse-wheel": "Mouse wheel",muted: "Muted",name: "Name","native-mkb": "Native Mouse & Keyboard",new: "New","new-version-available": [e => `Version ${e.version} available`,e => `Versió ${e.version} disponible`,e => `Verze ${e.version} dostupná`,,e => `Version ${e.version} verfügbar`,e => `Versi ${e.version} tersedia`,e => `Versión ${e.version} disponible`,e => `Version ${e.version} disponible`,e => `Disponibile la versione ${e.version}`,e => `Ver ${e.version} が利用可能です`,e => `${e.version} 버전 사용가능`,e => `Dostępna jest nowa wersja ${e.version}`,e => `Versão ${e.version} disponível`,e => `Версия ${e.version} доступна`,e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`,e => `${e.version} sayılı yeni sürüm mevcut`,e => `Доступна версія ${e.version}`,e => `Đã có phiên bản ${e.version}`,e => `版本 ${e.version} 可供更新`,e => `已可更新為 ${e.version} 版`],"no-consoles-found": "No consoles found","no-controllers-connected": "No controllers connected",normal: "Normal",notifications: "Notifications",off: "Off",official: "Official",oled: "OLED",on: "On","only-supports-some-games": "Only supports some games",opacity: "Opacity",other: "Other",performance: "Performance",playing: "Playing",playtime: "Playtime",poland: "Poland","polling-rate": "Polling rate",position: "Position","powered-off": "Powered off","powered-on": "Powered on","prefer-ipv6-server": "Prefer IPv6 server","preferred-game-language": "Preferred game's language",preset: "Preset",press: "Press","press-any-button": "Press any button...","press-esc-to-cancel": "Press Esc to cancel","press-key-to-toggle-mkb": [e => `Press ${e.key} to toggle this feature`,e => `Premeu ${e.key} per alternar aquesta funció`,e => `Zmáčknete ${e.key} pro přepnutí této funkce`,e => `Tryk på ${e.key} for at slå denne funktion til`,e => `${e.key}: Funktion an-/ausschalten`,e => `Tekan ${e.key} untuk mengaktifkan fitur ini`,e => `Pulsa ${e.key} para alternar esta función`,e => `Appuyez sur ${e.key} pour activer cette fonctionnalité`,e => `Premi ${e.key} per attivare questa funzionalità`,e => `${e.key} でこの機能を切替`,e => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`,e => `Naciśnij ${e.key} aby przełączyć tę funkcję`,e => `Pressione ${e.key} para alternar este recurso`,e => `Нажмите ${e.key} для переключения этой функции`,e => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`,e => `Etkinleştirmek için ${e.key} tuşuna basın`,e => `Натисніть ${e.key} щоб перемкнути цю функцію`,e => `Nhấn ${e.key} để bật/tắt tính năng này`,e => `按下 ${e.key} 来切换此功能`,e => `按下 ${e.key} 來啟用此功能`],"press-to-bind": "Press a key or do a mouse click to bind...","prompt-preset-name": "Preset's name:",quality: "Quality",recommended: "Recommended","recommended-settings-for-device": [e => `Recommended settings for ${e.device}`,e => `Configuració recomanada per a ${e.device}`,,,e => `Empfohlene Einstellungen für ${e.device}`,e => `Rekomendasi pengaturan untuk ${e.device}`,e => `Ajustes recomendados para ${e.device}`,e => `Paramètres recommandés pour ${e.device}`,e => `Configurazioni consigliate per ${e.device}`,e => `${e.device} の推奨設定`,e => `다음 기기에서 권장되는 설정: ${e.device}`,e => `Zalecane ustawienia dla ${e.device}`,e => `Configurações recomendadas para ${e.device}`,e => `Рекомендуемые настройки для ${e.device}`,e => `การตั้งค่าที่แนะนำสำหรับ ${e.device}`,e => `${e.device} için önerilen ayarlar`,e => `Рекомендовані налаштування для ${e.device}`,e => `Cấu hình được đề xuất cho ${e.device}`,e => `${e.device} 的推荐设置`,e => `${e.device} 推薦的設定`],"reduce-animations": "Reduce UI animations",region: "Region","reload-page": "Reload page","remote-play": "Remote Play",rename: "Rename",renderer: "Renderer","renderer-configuration": "Renderer configuration","reset-highlighted-setting": "Reset highlighted setting",resolution: "Resolution","right-click-to-unbind": "Right-click on a key to unbind it","right-stick": "Right stick","right-stick-deadzone": "Right stick deadzone","right-trigger-range": "Right trigger range","rocket-always-hide": "Always hide","rocket-always-show": "Always show","rocket-animation": "Rocket animation","rocket-hide-queue": "Hide when queuing",saturation: "Saturation",save: "Save",screen: "Screen","screenshot-apply-filters": "Apply video filters to screenshots","section-all-games": "All games","section-genres": "Genres","section-leaving-soon": "Leaving soon","section-most-popular": "Most popular","section-native-mkb": "Play with mouse & keyboard","section-news": "News","section-play-with-friends": "Play with friends","section-recently-added": "Recently added","section-touch": "Play with touch","separate-touch-controller": "Separate Touch controller & Controller #1","separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",server: "Server","server-list-error": "Can't get the server list","server-locations": "Server locations",settings: "Settings","settings-for": "Settings for","settings-reload": "Reload page to reflect changes","settings-reload-note": "Settings in this tab only go into effect on the next page load","settings-reloading": "Reloading...",sharpness: "Sharpness","shortcut-keys": "Shortcut keys",show: "Show","show-controller-connection-status": "Show controller connection status","show-game-art": "Show game art","show-hide": "Show/hide","show-stats-on-startup": "Show stats when starting the game","show-touch-controller": "Show touch controller","show-wait-time": "Show the estimated wait time","show-wait-time-in-game-card": "Show wait time in game card","simplify-stream-menu": "Simplify Stream's menu","skip-splash-video": "Skip Xbox splash video",slow: "Slow",small: "Small","smart-tv": "Smart TV",sound: "Sound",standard: "Standard",standby: "Standby","stat-bitrate": "Bitrate","stat-decode-time": "Decode time","stat-fps": "FPS","stat-frames-lost": "Frames lost","stat-packets-lost": "Packets lost","stat-ping": "Ping",stats: "Stats","stick-decay-minimum": "Stick decay minimum","stick-decay-strength": "Stick decay strength",stream: "Stream","stream-settings": "Stream settings","stream-stats": "Stream stats","stream-your-own-game": "Stream your own game",stretch: "Stretch","suggest-settings": "Suggest settings","suggest-settings-link": "Suggest recommended settings for this device","support-better-xcloud": "Support Better xCloud","swap-buttons": "Swap buttons","take-screenshot": "Take screenshot","target-resolution": "Target resolution","tc-all-white": "All white","tc-auto-off": "Off when controller found","tc-custom-layout-style": "Custom layout's button style","tc-muted-colors": "Muted colors","tc-standard-layout-style": "Standard layout's button style","test-controller": "Test controller","text-size": "Text size",theme: "Theme",toggle: "Toggle",top: "Top","top-center": "Top-center","top-half": "Top half","top-left": "Top-left","top-right": "Top-right","touch-control-layout": "Touch control layout","touch-control-layout-by": [e => `Touch control layout by ${e.name}`,e => `Format del control tàctil per ${e.name}`,e => `Rozložení dotykového ovládání ${e.name}`,e => `Touch-kontrol layout af ${e.name}`,e => `Touch-Steuerungslayout von ${e.name}`,e => `Tata letak Sentuhan layar oleh ${e.name}`,e => `Disposición del control táctil por ${e.nombre}`,e => `Disposition du contrôleur tactile par ${e.name}`,e => `Configurazione dei comandi su schermo creata da ${e.name}`,e => `タッチ操作レイアウト作成者: ${e.name}`,e => `${e.name} 제작, 터치 컨트롤 레이아웃`,e => `Układ sterowania dotykowego stworzony przez ${e.name}`,e => `Disposição de controle por toque feito por ${e.name}`,e => `Сенсорная раскладка по ${e.name}`,e => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`,e => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,e => `Розташування сенсорного керування від ${e.name}`,e => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,e => `由 ${e.name} 提供的虚拟按键样式`,e => `觸控遊玩佈局由 ${e.name} 提供`],"touch-controller": "Touch controller","true-achievements": "TrueAchievements",ui: "UI","unexpected-behavior": "May cause unexpected behavior","united-states": "United States",unknown: "Unknown",unlimited: "Unlimited",unmuted: "Unmuted",unofficial: "Unofficial","unofficial-game-list": "Unofficial game list","unsharp-masking": "Unsharp masking",upload: "Upload",uploaded: "Uploaded","use-mouse-absolute-position": "Use mouse's absolute position","use-this-at-your-own-risk": "Use this at your own risk","user-agent-profile": "User-Agent profile","vertical-scroll-sensitivity": "Vertical scroll sensitivity","vertical-sensitivity": "Vertical sensitivity","vibration-intensity": "Vibration intensity","vibration-status": "Vibration",video: "Video","virtual-controller": "Virtual controller","virtual-controller-slot": "Virtual controller slot","visual-quality": "Visual quality","visual-quality-high": "High","visual-quality-low": "Low","visual-quality-normal": "Normal",volume: "Volume","wait-time-countdown": "Countdown","wait-time-estimated": "Estimated finish time","waiting-for-input": "Waiting for input...",wallpaper: "Wallpaper",webgl2: "WebGL2",webgpu: "WebGPU","xbox-360-games": "Xbox 360 games","xbox-apps": "Xbox apps"}; +var SUPPORTED_LANGUAGES = {"en-US": "English (US)","ca-CA": "Català","cs-CZ": "čeština","da-DK": "dansk","de-DE": "Deutsch","en-ID": "Bahasa Indonesia","es-ES": "español (España)","fr-FR": "français","it-IT": "italiano","ja-JP": "日本語","ko-KR": "한국어","pl-PL": "polski","pt-BR": "português (Brasil)","ru-RU": "русский","th-TH": "ภาษาไทย","tr-TR": "Türkçe","uk-UA": "українська","vi-VN": "Tiếng Việt","zh-CN": "中文(简体)","zh-TW": "中文(繁體)"}, Texts = {achievements: "Achievements",activate: "Activate",activated: "Activated",active: "Active",advanced: "Advanced","all-games": "All games","always-off": "Always off","always-on": "Always on","amd-fidelity-cas": "AMD FidelityFX CAS","app-settings": "App settings",apply: "Apply","aspect-ratio": "Aspect ratio","aspect-ratio-note": "Don't use with native touch games",audio: "Audio",auto: "Auto",availability: "Availability","back-to-home": "Back to home","back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?","background-opacity": "Background opacity",battery: "Battery","battery-saving": "Battery saving","better-xcloud": "Better xCloud","bitrate-audio-maximum": "Maximum audio bitrate","bitrate-video-maximum": "Maximum video bitrate",bottom: "Bottom","bottom-half": "Bottom half","bottom-left": "Bottom-left","bottom-right": "Bottom-right",brazil: "Brazil",brightness: "Brightness","browser-unsupported-feature": "Your browser doesn't support this feature","button-xbox": "Xbox button","bypass-region-restriction": "Bypass region restriction",cancel: "Cancel",center: "Center",chat: "Chat","clarity-boost": "Clarity boost","clarity-boost-mode": "Clarity boost mode","clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",clear: "Clear","clear-data": "Clear data","clear-data-confirm": "Do you want to clear all Better xCloud settings and data?","clear-data-success": "Data cleared! Refresh the page to apply the changes.",clock: "Clock",close: "Close","close-app": "Close app","combine-audio-video-streams": "Combine audio & video streams","combine-audio-video-streams-summary": "May fix the laggy audio problem","conditional-formatting": "Conditional formatting text color","confirm-delete-preset": "Do you want to delete this preset?","confirm-reload-stream": "Do you want to refresh the stream?",connected: "Connected","console-connect": "Connect","continent-asia": "Asia","continent-australia": "Australia","continent-europe": "Europe","continent-north-america": "North America","continent-south-america": "South America",contrast: "Contrast",controller: "Controller","controller-customization": "Controller customization","controller-customization-input-latency-note": "May slightly increase input latency","controller-friendly-ui": "Controller-friendly UI","controller-shortcuts": "Controller shortcuts","controller-shortcuts-connect-note": "Connect a controller to use this feature","controller-shortcuts-xbox-note": "Button to open the Guide menu","controller-vibration": "Controller vibration",copy: "Copy","create-shortcut": "Shortcut",custom: "Custom","deadzone-counterweight": "Deadzone counterweight",decrease: "Decrease",default: "Default","default-opacity": "Default opacity","default-preset-note": "You can't modify default presets. Create a new one to customize it.",delete: "Delete","detect-controller-button": "Detect controller button",device: "Device","device-unsupported-touch": "Your device doesn't have touch support","device-vibration": "Device vibration","device-vibration-not-using-gamepad": "On when not using gamepad",disable: "Disable","disable-features": "Disable features","disable-home-context-menu": "Disable context menu in Home page","disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog","disable-social-features": "Disable social features","disable-xcloud-analytics": "Disable xCloud analytics",disabled: "Disabled",disconnected: "Disconnected",download: "Download",downloaded: "Downloaded",edit: "Edit","enable-controller-shortcuts": "Enable controller shortcuts","enable-local-co-op-support": "Enable local co-op support","enable-local-co-op-support-note": "Only works with some games","enable-mic-on-startup": "Enable microphone on game launch","enable-mkb": "Emulate controller with Mouse & Keyboard","enable-quick-glance-mode": 'Enable "Quick Glance" mode',"enable-remote-play-feature": 'Enable the "Remote Play" feature',"enable-volume-control": "Enable volume control feature",enabled: "Enabled",experimental: "Experimental",export: "Export",fast: "Fast","force-native-mkb-games": "Force native Mouse & Keyboard for these games","fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile',"fortnite-force-console-version": "Fortnite: force console version","friends-followers": "Friends and followers","game-bar": "Game Bar","getting-consoles-list": "Getting the list of consoles...",guide: "Guide",help: "Help",hide: "Hide","hide-idle-cursor": "Hide mouse cursor on idle","hide-scrollbar": "Hide web page's scrollbar","hide-sections": "Hide sections","hide-system-menu-icon": "Hide System menu's icon","hide-touch-controller": "Hide touch controller","high-performance": "High performance","highest-quality": "Highest quality","highest-quality-note": "Your device may not be powerful enough to use these settings","horizontal-scroll-sensitivity": "Horizontal scroll sensitivity","horizontal-sensitivity": "Horizontal sensitivity","how-to-fix": "How to fix","how-to-improve-app-performance": "How to improve app's performance",ignore: "Ignore","image-quality": "Website's image quality",import: "Import","in-game-controller-customization": "In-game controller customization","in-game-controller-shortcuts": "In-game controller shortcuts","in-game-keyboard-shortcuts": "In-game keyboard shortcuts","in-game-shortcuts": "In-game shortcuts",increase: "Increase","install-android": "Better xCloud app for Android",invites: "Invites",japan: "Japan",jitter: "Jitter","keyboard-key": "Keyboard key","keyboard-shortcuts": "Keyboard shortcuts",korea: "Korea",language: "Language",large: "Large",layout: "Layout","left-stick": "Left stick","left-stick-deadzone": "Left stick deadzone","left-trigger-range": "Left trigger range","limit-fps": "Limit FPS","load-failed-message": "Failed to run Better xCloud","loading-screen": "Loading screen","local-co-op": "Local co-op","lowest-quality": "Lowest quality",manage: "Manage","map-mouse-to": "Map mouse to","may-not-work-properly": "May not work properly!",menu: "Menu",microphone: "Microphone","mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings","mkb-click-to-activate": "Click to activate","mkb-disclaimer": "This could be viewed as cheating when playing online","modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.","mouse-and-keyboard": "Mouse & Keyboard","mouse-click": "Mouse click","mouse-wheel": "Mouse wheel",muted: "Muted",name: "Name","native-mkb": "Native Mouse & Keyboard",new: "New","new-version-available": [e => `Version ${e.version} available`,e => `Versió ${e.version} disponible`,e => `Verze ${e.version} dostupná`,,e => `Version ${e.version} verfügbar`,e => `Versi ${e.version} tersedia`,e => `Versión ${e.version} disponible`,e => `Version ${e.version} disponible`,e => `Disponibile la versione ${e.version}`,e => `Ver ${e.version} が利用可能です`,e => `${e.version} 버전 사용가능`,e => `Dostępna jest nowa wersja ${e.version}`,e => `Versão ${e.version} disponível`,e => `Версия ${e.version} доступна`,e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`,e => `${e.version} sayılı yeni sürüm mevcut`,e => `Доступна версія ${e.version}`,e => `Đã có phiên bản ${e.version}`,e => `版本 ${e.version} 可供更新`,e => `已可更新為 ${e.version} 版`],"no-consoles-found": "No consoles found","no-controllers-connected": "No controllers connected",normal: "Normal",notifications: "Notifications",off: "Off",official: "Official",oled: "OLED",on: "On","only-supports-some-games": "Only supports some games",opacity: "Opacity",other: "Other",performance: "Performance",playing: "Playing",playtime: "Playtime",poland: "Poland","polling-rate": "Polling rate",position: "Position","powered-off": "Powered off","powered-on": "Powered on","prefer-ipv6-server": "Prefer IPv6 server","preferred-game-language": "Preferred game's language",preset: "Preset",press: "Press","press-any-button": "Press any button...","press-esc-to-cancel": "Press Esc to cancel","press-key-to-toggle-mkb": [e => `Press ${e.key} to toggle this feature`,e => `Premeu ${e.key} per alternar aquesta funció`,e => `Zmáčknete ${e.key} pro přepnutí této funkce`,e => `Tryk på ${e.key} for at slå denne funktion til`,e => `${e.key}: Funktion an-/ausschalten`,e => `Tekan ${e.key} untuk mengaktifkan fitur ini`,e => `Pulsa ${e.key} para alternar esta función`,e => `Appuyez sur ${e.key} pour activer cette fonctionnalité`,e => `Premi ${e.key} per attivare questa funzionalità`,e => `${e.key} でこの機能を切替`,e => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`,e => `Naciśnij ${e.key} aby przełączyć tę funkcję`,e => `Pressione ${e.key} para alternar este recurso`,e => `Нажмите ${e.key} для переключения этой функции`,e => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`,e => `Etkinleştirmek için ${e.key} tuşuna basın`,e => `Натисніть ${e.key} щоб перемкнути цю функцію`,e => `Nhấn ${e.key} để bật/tắt tính năng này`,e => `按下 ${e.key} 来切换此功能`,e => `按下 ${e.key} 來啟用此功能`],"press-to-bind": "Press a key or do a mouse click to bind...","prevent-resolution-drops": "Prevent resolution drops","prompt-preset-name": "Preset's name:",quality: "Quality",recommended: "Recommended","recommended-settings-for-device": [e => `Recommended settings for ${e.device}`,e => `Configuració recomanada per a ${e.device}`,,,e => `Empfohlene Einstellungen für ${e.device}`,e => `Rekomendasi pengaturan untuk ${e.device}`,e => `Ajustes recomendados para ${e.device}`,e => `Paramètres recommandés pour ${e.device}`,e => `Configurazioni consigliate per ${e.device}`,e => `${e.device} の推奨設定`,e => `다음 기기에서 권장되는 설정: ${e.device}`,e => `Zalecane ustawienia dla ${e.device}`,e => `Configurações recomendadas para ${e.device}`,e => `Рекомендуемые настройки для ${e.device}`,e => `การตั้งค่าที่แนะนำสำหรับ ${e.device}`,e => `${e.device} için önerilen ayarlar`,e => `Рекомендовані налаштування для ${e.device}`,e => `Cấu hình được đề xuất cho ${e.device}`,e => `${e.device} 的推荐设置`,e => `${e.device} 推薦的設定`],"reduce-animations": "Reduce UI animations",region: "Region","reload-page": "Reload page","remote-play": "Remote Play",rename: "Rename",renderer: "Renderer","renderer-configuration": "Renderer configuration","reset-highlighted-setting": "Reset highlighted setting",resolution: "Resolution","right-click-to-unbind": "Right-click on a key to unbind it","right-stick": "Right stick","right-stick-deadzone": "Right stick deadzone","right-trigger-range": "Right trigger range","rocket-always-hide": "Always hide","rocket-always-show": "Always show","rocket-animation": "Rocket animation","rocket-hide-queue": "Hide when queuing",saturation: "Saturation",save: "Save",screen: "Screen","screenshot-apply-filters": "Apply video filters to screenshots","section-all-games": "All games","section-genres": "Genres","section-leaving-soon": "Leaving soon","section-most-popular": "Most popular","section-native-mkb": "Play with mouse & keyboard","section-news": "News","section-play-with-friends": "Play with friends","section-recently-added": "Recently added","section-touch": "Play with touch","separate-touch-controller": "Separate Touch controller & Controller #1","separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",server: "Server","server-list-error": "Can't get the server list","server-locations": "Server locations",settings: "Settings","settings-for": "Settings for","settings-reload": "Reload page to reflect changes","settings-reload-note": "Settings in this tab only go into effect on the next page load","settings-reloading": "Reloading...",sharpness: "Sharpness","shortcut-keys": "Shortcut keys",show: "Show","show-controller-connection-status": "Show controller connection status","show-game-art": "Show game art","show-hide": "Show/hide","show-stats-on-startup": "Show stats when starting the game","show-touch-controller": "Show touch controller","show-wait-time": "Show the estimated wait time","show-wait-time-in-game-card": "Show wait time in game card","simplify-stream-menu": "Simplify Stream's menu","skip-splash-video": "Skip Xbox splash video",slow: "Slow",small: "Small","smart-tv": "Smart TV",sound: "Sound",standard: "Standard",standby: "Standby","stat-bitrate": "Bitrate","stat-decode-time": "Decode time","stat-fps": "FPS","stat-frames-lost": "Frames lost","stat-packets-lost": "Packets lost","stat-ping": "Ping",stats: "Stats","stick-decay-minimum": "Stick decay minimum","stick-decay-strength": "Stick decay strength",stream: "Stream","stream-settings": "Stream settings","stream-stats": "Stream stats","stream-your-own-game": "Stream your own game",stretch: "Stretch","suggest-settings": "Suggest settings","suggest-settings-link": "Suggest recommended settings for this device","support-better-xcloud": "Support Better xCloud","swap-buttons": "Swap buttons","take-screenshot": "Take screenshot","target-resolution": "Target resolution","tc-all-white": "All white","tc-auto-off": "Off when controller found","tc-custom-layout-style": "Custom layout's button style","tc-muted-colors": "Muted colors","tc-standard-layout-style": "Standard layout's button style","test-controller": "Test controller","text-size": "Text size",theme: "Theme",toggle: "Toggle",top: "Top","top-center": "Top-center","top-half": "Top half","top-left": "Top-left","top-right": "Top-right","touch-control-layout": "Touch control layout","touch-control-layout-by": [e => `Touch control layout by ${e.name}`,e => `Format del control tàctil per ${e.name}`,e => `Rozložení dotykového ovládání ${e.name}`,e => `Touch-kontrol layout af ${e.name}`,e => `Touch-Steuerungslayout von ${e.name}`,e => `Tata letak Sentuhan layar oleh ${e.name}`,e => `Disposición del control táctil por ${e.nombre}`,e => `Disposition du contrôleur tactile par ${e.name}`,e => `Configurazione dei comandi su schermo creata da ${e.name}`,e => `タッチ操作レイアウト作成者: ${e.name}`,e => `${e.name} 제작, 터치 컨트롤 레이아웃`,e => `Układ sterowania dotykowego stworzony przez ${e.name}`,e => `Disposição de controle por toque feito por ${e.name}`,e => `Сенсорная раскладка по ${e.name}`,e => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`,e => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,e => `Розташування сенсорного керування від ${e.name}`,e => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,e => `由 ${e.name} 提供的虚拟按键样式`,e => `觸控遊玩佈局由 ${e.name} 提供`],"touch-controller": "Touch controller","true-achievements": "TrueAchievements",ui: "UI","unexpected-behavior": "May cause unexpected behavior","united-states": "United States",unknown: "Unknown",unlimited: "Unlimited",unmuted: "Unmuted",unofficial: "Unofficial","unofficial-game-list": "Unofficial game list","unsharp-masking": "Unsharp masking",upload: "Upload",uploaded: "Uploaded","use-mouse-absolute-position": "Use mouse's absolute position","use-this-at-your-own-risk": "Use this at your own risk","user-agent-profile": "User-Agent profile","vertical-scroll-sensitivity": "Vertical scroll sensitivity","vertical-sensitivity": "Vertical sensitivity","vibration-intensity": "Vibration intensity","vibration-status": "Vibration",video: "Video","virtual-controller": "Virtual controller","virtual-controller-slot": "Virtual controller slot","visual-quality": "Visual quality","visual-quality-high": "High","visual-quality-low": "Low","visual-quality-normal": "Normal",volume: "Volume","wait-time-countdown": "Countdown","wait-time-estimated": "Estimated finish time","waiting-for-input": "Waiting for input...",wallpaper: "Wallpaper",webgl2: "WebGL2",webgpu: "WebGPU","xbox-360-games": "Xbox 360 games","xbox-apps": "Xbox apps"}; class Translations {static EN_US = "en-US";static KEY_LOCALE = "BetterXcloud.Locale";static KEY_TRANSLATIONS = "BetterXcloud.Locale.Translations";static selectedLocaleIndex = -1;static selectedLocale = "en-US";static supportedLocales = Object.keys(SUPPORTED_LANGUAGES);static foreignTranslations = {};static enUsIndex = Translations.supportedLocales.indexOf(Translations.EN_US);static async init() {Translations.refreshLocale(), await Translations.loadTranslations();}static refreshLocale(newLocale) {let locale;if (newLocale) localStorage.setItem(Translations.KEY_LOCALE, newLocale), locale = newLocale;else locale = localStorage.getItem(Translations.KEY_LOCALE);let supportedLocales = Translations.supportedLocales;if (!locale) {if (locale = window.navigator.language || Translations.EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.EN_US;localStorage.setItem(Translations.KEY_LOCALE, locale);}Translations.selectedLocale = locale, Translations.selectedLocaleIndex = supportedLocales.indexOf(locale);}static get(key, values) {let text = null;if (Translations.foreignTranslations && Translations.selectedLocale !== Translations.EN_US) text = Translations.foreignTranslations[key];if (!text) text = Texts[key] || alert(`Missing translation key: ${key}`);let translation;if (Array.isArray(text)) return translation = text[Translations.selectedLocaleIndex] || text[Translations.enUsIndex], translation(values);return translation = text, translation;}static async loadTranslations() {if (Translations.selectedLocale === Translations.EN_US) return;try {Translations.foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.KEY_TRANSLATIONS));} catch (e) {}if (!Translations.foreignTranslations) await this.downloadTranslations(Translations.selectedLocale);}static async updateTranslations(async = !1) {if (Translations.selectedLocale === Translations.EN_US) {localStorage.removeItem(Translations.KEY_TRANSLATIONS);return;}if (async) Translations.downloadTranslationsAsync(Translations.selectedLocale);else await Translations.downloadTranslations(Translations.selectedLocale);}static async downloadTranslations(locale) {try {let translations = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`))).json();if (localStorage.getItem(Translations.KEY_LOCALE) === locale) window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;return !0;} catch (e) {debugger;}return !1;}static downloadTranslationsAsync(locale) {NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`)).then((resp) => resp.json()).then((translations) => {window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;});}static switchLocale(locale) {localStorage.setItem(Translations.KEY_LOCALE, locale);}} var t = Translations.get; Translations.init(); @@ -69,7 +69,7 @@ class LocalDb {static instance;static getInstance = () => LocalDb.instance ?? (L var BypassServers = {br: t("brazil"),jp: t("japan"),kr: t("korea"),pl: t("poland"),us: t("united-states")}, BypassServerIps = {br: "169.150.198.66",kr: "121.125.60.151",jp: "138.199.21.239",pl: "45.134.212.66",us: "143.244.47.65"}; class BaseSettingsStorage {storage;storageKey;_settings;definitions;constructor(storageKey, definitions) {this.storage = window.localStorage, this.storageKey = storageKey;for (let [_, setting] of Object.entries(definitions)) {if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants];if (setting.ready) setting.ready.call(this, setting), delete setting.ready;}this.definitions = definitions, this._settings = null;}get settings() {if (this._settings) return this._settings;let settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}");for (let key in settings)settings[key] = this.validateValue("get", key, settings[key]);return this._settings = settings, settings;}getDefinition(key) {if (!this.definitions[key]) return alert("Request invalid definition: " + key), {};return this.definitions[key];}hasSetting(key) {return key in this.settings;}getSetting(key, checkUnsupported = !0) {let definition = this.definitions[key];if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default;if (checkUnsupported && definition.unsupported) if ("unsupportedValue" in definition) return definition.unsupportedValue;else return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default;if (!(key in this.settings)) this.settings[key] = this.validateValue("get", key, null);return isPlainObject(this.settings[key]) ? deepClone(this.settings[key]) : this.settings[key];}setSetting(key, value, origin) {if (value = this.validateValue("set", key, value), this.settings[key] = this.validateValue("get", key, value), this.saveSettings(), origin === "ui") if (isStreamPref(key)) BxEventBus.Stream.emit("setting.changed", {storageKey: this.storageKey,settingKey: key});else BxEventBus.Script.emit("setting.changed", {storageKey: this.storageKey,settingKey: key});return value;}saveSettings() {this.storage.setItem(this.storageKey, JSON.stringify(this.settings));}validateValue(action, key, value) {let def = this.definitions[key];if (!def) return value;if (typeof value === "undefined" || value === null) value = def.default;if (def.transformValue && action === "get") value = def.transformValue.get.call(def, value);if ("min" in def) value = Math.max(def.min, value);if ("max" in def) value = Math.min(def.max, value);if ("options" in def) {if (!(value in def.options)) value = def.default;} else if ("multipleOptions" in def) {if (value.length) {let validOptions = Object.keys(def.multipleOptions);value.forEach((item2, idx) => {validOptions.indexOf(item2) === -1 && value.splice(idx, 1);});}if (!value.length) value = def.default;}if (def.transformValue && action === "set") value = def.transformValue.set.call(def, value);return value;}getLabel(key) {return this.definitions[key].label || key;}getValueText(key, value) {let definition = this.definitions[key];if ("min" in definition) {let params = definition.params;if (params.customTextValue) {if (definition.transformValue) value = definition.transformValue.get.call(definition, value);let text = params.customTextValue(value, definition.min, definition.max);if (text) return text;}return value.toString();} else if ("options" in definition) {let options = definition.options;if (value in options) return options[value];} else if (typeof value === "boolean") return value ? t("on") : t("off");return value.toString();}deleteSetting(pref) {if (this.hasSetting(pref)) return delete this.settings[pref], this.saveSettings(), !0;return !1;}} function getSupportedCodecProfiles() {let options = {default: t("default")};if (!("getCapabilities" in RTCRtpReceiver)) return options;let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1, codecs = RTCRtpReceiver.getCapabilities("video").codecs;for (let codec of codecs) {if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue;let fmtp = codec.sdpFmtpLine.toLowerCase();if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0;else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0;else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0;}if (hasLowCodec) if (!hasNormalCodec && !hasHighCodec) options["default"] = `${t("visual-quality-low")} (${t("default")})`;else options["low"] = t("visual-quality-low");if (hasNormalCodec) if (!hasLowCodec && !hasHighCodec) options["default"] = `${t("visual-quality-normal")} (${t("default")})`;else options["normal"] = t("visual-quality-normal");if (hasHighCodec) if (!hasLowCodec && !hasNormalCodec) options["default"] = `${t("visual-quality-high")} (${t("default")})`;else options["high"] = t("visual-quality-high");return options;} -class GlobalSettingsStorage extends BaseSettingsStorage {static DEFINITIONS = {"version.lastCheck": {default: 0},"version.latest": {default: ""},"version.current": {default: ""},"bx.locale": {label: t("language"),default: localStorage.getItem("BetterXcloud.Locale") || "en-US",options: SUPPORTED_LANGUAGES},"server.region": {label: t("region"),note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")),default: "default"},"server.bypassRestriction": {label: t("bypass-region-restriction"),note: "⚠️ " + t("use-this-at-your-own-risk"),default: "off",optionsGroup: t("region"),options: Object.assign({off: t("off")}, BypassServers)},"stream.locale": {label: t("preferred-game-language"),default: "default",options: {default: t("default"),"ar-SA": "العربية","bg-BG": "Български","cs-CZ": "čeština","da-DK": "dansk","de-DE": "Deutsch","el-GR": "Ελληνικά","en-GB": "English (UK)","en-US": "English (US)","es-ES": "español (España)","es-MX": "español (Latinoamérica)","fi-FI": "suomi","fr-FR": "français","he-IL": "עברית","hu-HU": "magyar","it-IT": "italiano","ja-JP": "日本語","ko-KR": "한국어","nb-NO": "norsk bokmål","nl-NL": "Nederlands","pl-PL": "polski","pt-BR": "português (Brasil)","pt-PT": "português (Portugal)","ro-RO": "Română","ru-RU": "русский","sk-SK": "slovenčina","sv-SE": "svenska","th-TH": "ไทย","tr-TR": "Türkçe","zh-CN": "中文(简体)","zh-TW": "中文 (繁體)"}},"stream.video.resolution": {label: t("target-resolution"),default: "auto",options: {auto: t("default"),"720p": "720p","1080p": "1080p","1080p-hq": "1080p (HQ)"},suggest: {lowest: "720p",highest: "1080p-hq"}},"stream.video.codecProfile": {label: t("visual-quality"),default: "default",options: getSupportedCodecProfiles(),ready: (setting) => {let options = setting.options, keys = Object.keys(options);if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature");setting.suggest = {lowest: keys.length === 1 ? keys[0] : keys[1],highest: keys[keys.length - 1]};}},"server.ipv6.prefer": {label: t("prefer-ipv6-server"),default: !1},"screenshot.applyFilters": {requiredVariants: "full",label: t("screenshot-apply-filters"),default: !1},"ui.splashVideo.skip": {label: t("skip-splash-video"),default: !1},"ui.systemMenu.hideHandle": {label: "⣿ " + t("hide-system-menu-icon"),default: !1},"ui.imageQuality": {requiredVariants: "full",label: t("image-quality"),default: 90,min: 10,max: 90,params: {steps: 10,exactTicks: 20,hideSlider: !0,customTextValue(value, min, max) {if (value === 90) return t("default");return value + "%";}}},"ui.theme": {label: t("theme"),default: "default",options: {default: t("default"),"dark-oled": t("oled")}},"stream.video.combineAudio": {requiredVariants: "full",label: t("combine-audio-video-streams"),default: !1,experimental: !0,note: t("combine-audio-video-streams-summary")},"touchController.mode": {requiredVariants: "full",label: t("availability"),default: "all",options: {default: t("default"),off: t("off"),all: t("all-games")},unsupported: !STATES.userAgent.capabilities.touch,unsupportedValue: "default"},"touchController.autoOff": {requiredVariants: "full",label: t("tc-auto-off"),default: !1,unsupported: !STATES.userAgent.capabilities.touch},"touchController.opacity.default": {requiredVariants: "full",label: t("default-opacity"),default: 100,min: 10,max: 100,params: {steps: 10,suffix: "%",ticks: 10,hideSlider: !0},unsupported: !STATES.userAgent.capabilities.touch},"touchController.style.standard": {requiredVariants: "full",label: t("tc-standard-layout-style"),default: "default",options: {default: t("default"),white: t("tc-all-white"),muted: t("tc-muted-colors")},unsupported: !STATES.userAgent.capabilities.touch},"touchController.style.custom": {requiredVariants: "full",label: t("tc-custom-layout-style"),default: "default",options: {default: t("default"),muted: t("tc-muted-colors")},unsupported: !STATES.userAgent.capabilities.touch},"ui.streamMenu.simplify": {label: t("simplify-stream-menu"),default: !1},"mkb.cursor.hideIdle": {requiredVariants: "full",label: t("hide-idle-cursor"),default: !1},"ui.feedbackDialog.disabled": {requiredVariants: "full",label: t("disable-post-stream-feedback-dialog"),default: !1},"stream.video.maxBitrate": {requiredVariants: "full",label: t("bitrate-video-maximum"),note: "⚠️ " + t("unexpected-behavior"),default: 0,min: 102400,max: 15360000,transformValue: {get(value) {return value === 0 ? this.max : value;},set(value) {return value === this.max ? 0 : value;}},params: {steps: 102400,exactTicks: 5120000,customTextValue: (value, min, max) => {if (value = parseInt(value), value === max) return t("unlimited");else return (value / 1024000).toFixed(1) + " Mb/s";}},suggest: {highest: 0}},"gameBar.position": {requiredVariants: "full",label: t("position"),default: "bottom-left",options: {off: t("off"),"bottom-left": t("bottom-left"),"bottom-right": t("bottom-right")}},"ui.controllerStatus.show": {label: t("show-controller-connection-status"),default: !0},"mkb.enabled": {requiredVariants: "full",label: t("enable-mkb"),default: !1,unsupported: !STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb,ready: (setting) => {let note, url;if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657";else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer";setting.unsupportedNote = () => CE("a", {href: url,target: "_blank"}, "⚠️ " + note);}},"nativeMkb.mode": {requiredVariants: "full",label: t("native-mkb"),default: "default",options: {default: t("default"),off: t("off"),on: t("on")},ready: (setting) => {if (STATES.browser.capabilities.emulatedNativeMkb) ;else if (UserAgent.isMobile()) setting.unsupported = !0, setting.unsupportedValue = "off", delete setting.options["default"], delete setting.options["on"];else delete setting.options["on"];}},"nativeMkb.forcedGames": {label: t("force-native-mkb-games"),default: [],unsupported: !AppInterface && UserAgent.isMobile(),ready: (setting) => {if (!setting.unsupported) setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(!0), BxEventBus.Script.once("list.forcedNativeMkb.updated", (payload) => {setting.multipleOptions = payload.data.data;});},params: {size: 6}},"ui.reduceAnimations": {label: t("reduce-animations"),default: !1},"loadingScreen.gameArt.show": {requiredVariants: "full",label: t("show-game-art"),default: !0},"loadingScreen.waitTime.show": {label: t("show-wait-time"),default: !0},"loadingScreen.rocket": {label: t("rocket-animation"),default: "show",options: {show: t("rocket-always-show"),"hide-queue": t("rocket-hide-queue"),hide: t("rocket-always-hide")}},"ui.controllerFriendly": {label: t("controller-friendly-ui"),default: BX_FLAGS.DeviceInfo.deviceType !== "unknown"},"ui.layout": {requiredVariants: "full",label: t("layout"),default: "default",options: {default: t("default"),normal: t("normal"),tv: t("smart-tv")}},"ui.hideScrollbar": {label: t("hide-scrollbar"),default: !1},"ui.hideSections": {requiredVariants: "full",label: t("hide-sections"),default: [],multipleOptions: {news: t("section-news"),friends: t("section-play-with-friends"),"native-mkb": t("section-native-mkb"),touch: t("section-touch"),"most-popular": t("section-most-popular"),byog: t("stream-your-own-game"),"recently-added": t("section-recently-added"),"leaving-soon": t("section-leaving-soon"),genres: t("section-genres"),"all-games": t("section-all-games")},params: {size: 0}},"ui.gameCard.waitTime.show": {requiredVariants: "full",label: t("show-wait-time-in-game-card"),default: !0},"block.tracking": {label: t("disable-xcloud-analytics"),default: !1},"block.features": {requiredVariants: "full",label: t("disable-features"),default: [],multipleOptions: {chat: t("chat"),friends: t("friends-followers"),"notifications-invites": t("notifications") + ": " + t("invites"),"notifications-achievements": t("notifications") + ": " + t("achievements"),"remote-play": t("remote-play")}},"userAgent.profile": {label: t("user-agent-profile"),note: "⚠️ " + t("unexpected-behavior"),default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default",options: {default: t("default"),"windows-edge": "Edge + Windows","macos-safari": "Safari + macOS","vr-oculus": "Android TV","smarttv-generic": "Smart TV","smarttv-tizen": "Samsung Smart TV",custom: t("custom")}},"audio.mic.onPlaying": {label: t("enable-mic-on-startup"),default: !1},"audio.volume.booster.enabled": {requiredVariants: "full",label: t("enable-volume-control"),default: !1},"xhome.video.resolution": {requiredVariants: "full",default: "1080p",options: {"720p": "720p","1080p": "1080p","1080p-hq": "1080p (HQ)"}},"xhome.ipv6.prefer": {requiredVariants: "full",default: !1},"game.fortnite.forceConsole": {requiredVariants: "full",label: "🎮 " + t("fortnite-force-console-version"),default: !1,note: t("fortnite-allow-stw-mode")}};constructor() {super("BetterXcloud", GlobalSettingsStorage.DEFINITIONS);}} +class GlobalSettingsStorage extends BaseSettingsStorage {static DEFINITIONS = {"version.lastCheck": {default: 0},"version.latest": {default: ""},"version.current": {default: ""},"bx.locale": {label: t("language"),default: localStorage.getItem("BetterXcloud.Locale") || "en-US",options: SUPPORTED_LANGUAGES},"server.region": {label: t("region"),note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")),default: "default"},"server.bypassRestriction": {label: t("bypass-region-restriction"),note: "⚠️ " + t("use-this-at-your-own-risk"),default: "off",optionsGroup: t("region"),options: Object.assign({off: t("off")}, BypassServers)},"stream.locale": {label: t("preferred-game-language"),default: "default",options: {default: t("default"),"ar-SA": "العربية","bg-BG": "Български","cs-CZ": "čeština","da-DK": "dansk","de-DE": "Deutsch","el-GR": "Ελληνικά","en-GB": "English (UK)","en-US": "English (US)","es-ES": "español (España)","es-MX": "español (Latinoamérica)","fi-FI": "suomi","fr-FR": "français","he-IL": "עברית","hu-HU": "magyar","it-IT": "italiano","ja-JP": "日本語","ko-KR": "한국어","nb-NO": "norsk bokmål","nl-NL": "Nederlands","pl-PL": "polski","pt-BR": "português (Brasil)","pt-PT": "português (Portugal)","ro-RO": "Română","ru-RU": "русский","sk-SK": "slovenčina","sv-SE": "svenska","th-TH": "ไทย","tr-TR": "Türkçe","zh-CN": "中文(简体)","zh-TW": "中文 (繁體)"}},"stream.video.resolution": {label: t("target-resolution"),default: "auto",options: {auto: t("default"),"720p": "720p","1080p": "1080p","1080p-hq": "1080p (HQ)"},suggest: {lowest: "720p",highest: "1080p-hq"}},"stream.video.preventResolutionDrops": {label: t("prevent-resolution-drops"),default: !1,note: CE("a", { href: "https://github.com/redphx/better-xcloud/issues/791", target: "_blank" }, "⚠️ " + t("unexpected-behavior"))},"stream.video.codecProfile": {label: t("visual-quality"),default: "default",options: getSupportedCodecProfiles(),ready: (setting) => {let options = setting.options, keys = Object.keys(options);if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature");setting.suggest = {lowest: keys.length === 1 ? keys[0] : keys[1],highest: keys[keys.length - 1]};}},"server.ipv6.prefer": {label: t("prefer-ipv6-server"),default: !1},"screenshot.applyFilters": {requiredVariants: "full",label: t("screenshot-apply-filters"),default: !1},"ui.splashVideo.skip": {label: t("skip-splash-video"),default: !1},"ui.systemMenu.hideHandle": {label: "⣿ " + t("hide-system-menu-icon"),default: !1},"ui.imageQuality": {requiredVariants: "full",label: t("image-quality"),default: 90,min: 10,max: 90,params: {steps: 10,exactTicks: 20,hideSlider: !0,customTextValue(value, min, max) {if (value === 90) return t("default");return value + "%";}}},"ui.theme": {label: t("theme"),default: "default",options: {default: t("default"),"dark-oled": t("oled")}},"stream.video.combineAudio": {requiredVariants: "full",label: t("combine-audio-video-streams"),default: !1,experimental: !0,note: t("combine-audio-video-streams-summary")},"touchController.mode": {requiredVariants: "full",label: t("availability"),default: "all",options: {default: t("default"),off: t("off"),all: t("all-games")},unsupported: !STATES.userAgent.capabilities.touch,unsupportedValue: "default"},"touchController.autoOff": {requiredVariants: "full",label: t("tc-auto-off"),default: !1,unsupported: !STATES.userAgent.capabilities.touch},"touchController.opacity.default": {requiredVariants: "full",label: t("default-opacity"),default: 100,min: 10,max: 100,params: {steps: 10,suffix: "%",ticks: 10,hideSlider: !0},unsupported: !STATES.userAgent.capabilities.touch},"touchController.style.standard": {requiredVariants: "full",label: t("tc-standard-layout-style"),default: "default",options: {default: t("default"),white: t("tc-all-white"),muted: t("tc-muted-colors")},unsupported: !STATES.userAgent.capabilities.touch},"touchController.style.custom": {requiredVariants: "full",label: t("tc-custom-layout-style"),default: "default",options: {default: t("default"),muted: t("tc-muted-colors")},unsupported: !STATES.userAgent.capabilities.touch},"ui.streamMenu.simplify": {label: t("simplify-stream-menu"),default: !1},"mkb.cursor.hideIdle": {requiredVariants: "full",label: t("hide-idle-cursor"),default: !1},"ui.feedbackDialog.disabled": {requiredVariants: "full",label: t("disable-post-stream-feedback-dialog"),default: !1},"stream.video.maxBitrate": {requiredVariants: "full",label: t("bitrate-video-maximum"),note: "⚠️ " + t("unexpected-behavior"),default: 0,min: 102400,max: 15360000,transformValue: {get(value) {return value === 0 ? this.max : value;},set(value) {return value === this.max ? 0 : value;}},params: {steps: 102400,exactTicks: 5120000,customTextValue: (value, min, max) => {if (value = parseInt(value), value === max) return t("unlimited");else return (value / 1024000).toFixed(1) + " Mb/s";}},suggest: {highest: 0}},"gameBar.position": {requiredVariants: "full",label: t("position"),default: "bottom-left",options: {off: t("off"),"bottom-left": t("bottom-left"),"bottom-right": t("bottom-right")}},"ui.controllerStatus.show": {label: t("show-controller-connection-status"),default: !0},"mkb.enabled": {requiredVariants: "full",label: t("enable-mkb"),default: !1,unsupported: !STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb,ready: (setting) => {let note, url;if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657";else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer";setting.unsupportedNote = () => CE("a", {href: url,target: "_blank"}, "⚠️ " + note);}},"nativeMkb.mode": {requiredVariants: "full",label: t("native-mkb"),default: "default",options: {default: t("default"),off: t("off"),on: t("on")},ready: (setting) => {if (STATES.browser.capabilities.emulatedNativeMkb) ;else if (UserAgent.isMobile()) setting.unsupported = !0, setting.unsupportedValue = "off", delete setting.options["default"], delete setting.options["on"];else delete setting.options["on"];}},"nativeMkb.forcedGames": {label: t("force-native-mkb-games"),default: [],unsupported: !AppInterface && UserAgent.isMobile(),ready: (setting) => {if (!setting.unsupported) setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(!0), BxEventBus.Script.once("list.forcedNativeMkb.updated", (payload) => {setting.multipleOptions = payload.data.data;});},params: {size: 6}},"ui.reduceAnimations": {label: t("reduce-animations"),default: !1},"loadingScreen.gameArt.show": {requiredVariants: "full",label: t("show-game-art"),default: !0},"loadingScreen.waitTime.show": {label: t("show-wait-time"),default: !0},"loadingScreen.rocket": {label: t("rocket-animation"),default: "show",options: {show: t("rocket-always-show"),"hide-queue": t("rocket-hide-queue"),hide: t("rocket-always-hide")}},"ui.controllerFriendly": {label: t("controller-friendly-ui"),default: BX_FLAGS.DeviceInfo.deviceType !== "unknown"},"ui.layout": {requiredVariants: "full",label: t("layout"),default: "default",options: {default: t("default"),normal: t("normal"),tv: t("smart-tv")}},"ui.hideScrollbar": {label: t("hide-scrollbar"),default: !1},"ui.hideSections": {requiredVariants: "full",label: t("hide-sections"),default: [],multipleOptions: {news: t("section-news"),friends: t("section-play-with-friends"),"native-mkb": t("section-native-mkb"),touch: t("section-touch"),"most-popular": t("section-most-popular"),byog: t("stream-your-own-game"),"recently-added": t("section-recently-added"),"leaving-soon": t("section-leaving-soon"),genres: t("section-genres"),"all-games": t("section-all-games")},params: {size: 0}},"ui.gameCard.waitTime.show": {requiredVariants: "full",label: t("show-wait-time-in-game-card"),default: !0},"block.tracking": {label: t("disable-xcloud-analytics"),default: !1},"block.features": {requiredVariants: "full",label: t("disable-features"),default: [],multipleOptions: {chat: t("chat"),friends: t("friends-followers"),"notifications-invites": t("notifications") + ": " + t("invites"),"notifications-achievements": t("notifications") + ": " + t("achievements"),"remote-play": t("remote-play")}},"userAgent.profile": {label: t("user-agent-profile"),note: "⚠️ " + t("unexpected-behavior"),default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default",options: {default: t("default"),"windows-edge": "Edge + Windows","macos-safari": "Safari + macOS","vr-oculus": "Android TV","smarttv-generic": "Smart TV","smarttv-tizen": "Samsung Smart TV",custom: t("custom")}},"audio.mic.onPlaying": {label: t("enable-mic-on-startup"),default: !1},"audio.volume.booster.enabled": {requiredVariants: "full",label: t("enable-volume-control"),default: !1},"xhome.video.resolution": {requiredVariants: "full",default: "1080p",options: {"720p": "720p","1080p": "1080p","1080p-hq": "1080p (HQ)"}},"xhome.ipv6.prefer": {requiredVariants: "full",default: !1},"game.fortnite.forceConsole": {requiredVariants: "full",label: "🎮 " + t("fortnite-force-console-version"),default: !1,note: t("fortnite-allow-stw-mode")}};constructor() {super("BetterXcloud", GlobalSettingsStorage.DEFINITIONS);}} class BaseLocalTable {tableName;constructor(tableName) {this.tableName = tableName;}async prepareTable(type = "readonly") {return (await LocalDb.getInstance().open()).transaction(this.tableName, type).objectStore(this.tableName);}call(method) {return new Promise((resolve) => {let request = method.call(null, ...Array.from(arguments).slice(1));request.onsuccess = (e) => {resolve(e.target.result);};});}async count() {let table = await this.prepareTable();return this.call(table.count.bind(table));}async add(data) {let table = await this.prepareTable("readwrite");return this.call(table.add.bind(table), ...arguments);}async put(data) {let table = await this.prepareTable("readwrite");return this.call(table.put.bind(table), ...arguments);}async delete(id) {let table = await this.prepareTable("readwrite");return this.call(table.delete.bind(table), ...arguments);}async get(id) {let table = await this.prepareTable();return this.call(table.get.bind(table), ...arguments);}async getAll() {let table = await this.prepareTable(), all = await this.call(table.getAll.bind(table), ...arguments), results = {};return all.forEach((item2) => {results[item2.id] = item2;}), results;}} 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 KeyboardShortcutsTable extends BasePresetsTable {static instance;static getInstance = () => KeyboardShortcutsTable.instance ?? (KeyboardShortcutsTable.instance = new KeyboardShortcutsTable);LOG_TAG = "KeyboardShortcutsTable";TABLE_PRESETS = LocalDb.TABLE_KEYBOARD_SHORTCUTS;DEFAULT_PRESETS = {[-1]: {id: -1,name: t("standard"),data: {mapping: {"mkb.toggle": {code: "F8"},"stream.screenshot.capture": {code: "Slash"}}}}};BLANK_PRESET_DATA = {mapping: {}};DEFAULT_PRESET_ID = -1;constructor() {super(LocalDb.TABLE_KEYBOARD_SHORTCUTS);BxLogger.info(this.LOG_TAG, "constructor()");}} @@ -194,11 +194,11 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {const settings = let subs = ${subsVar}; subs = subs.filter(val => !${JSON.stringify(filters)}.includes(val)); ${subsVar} = subs; -`;return str = PatcherUtils.insertAt(str, index, newCode), str;},exposeReactCreateComponent(str) {let index = str.indexOf(".prototype.isReactComponent={}");if (index > -1 && (index = PatcherUtils.indexOf(str, ".createElement=", index)), index < 0) return !1;if (str = PatcherUtils.insertAt(str, index - 1, "window.BX_EXPOSED.reactCreateElement="), index = PatcherUtils.indexOf(str, ".useEffect=", index), index < 0) return !1;return str = PatcherUtils.insertAt(str, index - 1, "window.BX_EXPOSED.reactUseEffect="), str;},gameCardCustomIcons(str) {let initialIndex = str.indexOf("const{supportedInputIcons:");if (initialIndex < 0) return !1;let returnIndex = PatcherUtils.lastIndexOf(str, "return ", str.indexOf("SupportedInputsBadge"));if (returnIndex < 0) return !1;let productIdIndex = PatcherUtils.lastIndexOf(str, ",productId:", initialIndex, 300);if (productIdIndex < 0) return !1;let params = PatcherUtils.findAndParseParams(str, productIdIndex - 200, 400);if (!params || !params.productId) return !1;let productIdVar = params.productId, supportedInputIconsVar = PatcherUtils.getVariableNameAfter(str, PatcherUtils.indexOf(str, "supportedInputIcons:", initialIndex, 100, !0));if (!supportedInputIconsVar) return !1;let newCode = renderString(game_card_icons_default, {productId: productIdVar,supportedInputIcons: supportedInputIconsVar});return str = PatcherUtils.insertAt(str, returnIndex, newCode), str;},setImageQuality(str) {let index = str.indexOf("const{size:{width:");if (index > -1 && (index = PatcherUtils.indexOf(str, "=new URLSearchParams", index, 500)), index < 0) return !1;let paramVar = PatcherUtils.getVariableNameBefore(str, index);if (!paramVar) return !1;index = PatcherUtils.indexOf(str, "return", index, 200);let newCode = `${paramVar}.set('q', ${getGlobalPref("ui.imageQuality")});`;return str = PatcherUtils.insertAt(str, index, newCode), str;},setBackgroundImageQuality(str) {let index = str.indexOf("}?w=${");if (index > -1 && (index = PatcherUtils.indexOf(str, "}", index + 1, 10, !0)), index < 0) return !1;return str = PatcherUtils.insertAt(str, index, `&q=${getGlobalPref("ui.imageQuality")}`), str;},injectHeaderUseEffect(str) {let index = str.indexOf('className:"Header-module__header');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 300)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Script", "ui.header.rendered");},injectErrorPageUseEffect(str) {let index = str.indexOf('"PureErrorPage-module__container');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "})=>(0,", index, 200)), index < 0) return !1;return str = PatcherUtils.insertAt(str, index + 4, "{return "), str = PatcherUtils.injectUseEffect(str, index + 5, "Script", "ui.error.rendered"), str += "}", str;},injectStreamMenuUseEffect(str) {let index = str.indexOf('"StreamMenu-module__container');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Stream", "ui.streamMenu.rendered");},injectGuideHomeUseEffect(str) {let index = str.indexOf('"HomeLandingPage-module__authenticatedContentContainer');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Script", "ui.guideHome.rendered");},injectCreatePortal(str) {let index = str.indexOf(".createPortal=function");if (index > -1 && (index = PatcherUtils.indexOf(str, "{", index, 50, !0)), index < 0) return !1;return str = PatcherUtils.insertAt(str, index, create_portal_default), str;},injectAchievementsProgressUseEffect(str) {let index = str.indexOf('"AchievementsButton-module__progressBarContainer');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Script", "ui.guideAchievementProgress.rendered");},injectAchievementsDetailUseEffect(str) {let index = str.indexOf("GuideAchievementDetail.useParams()");if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "const", index, 200)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Script", "ui.guideAchievementDetail.rendered");},patchCustomInputIcon(str) {let index = str.indexOf('.MouseAndKeyboard="MouseAndKeyboard"');if (index < 0) return !1;let productIdMatch = /const (\w+)=(\w+)=>{/.exec(str.substring(index, index + 200));if (!productIdMatch) return !1;str = str.replace(productIdMatch[0], productIdMatch[0] + `const productId = ${productIdMatch[2]};`);let match = /(\w+)&&(\w+\.push\(\w+\.Touch\))/.exec(str);if (!match) return !1;if (str = str.replace(match[0], `(${match[1]} || window.BX_EXPOSED.hasCustomTouchControl(productId)) && ${match[2]}`), match = /(\w+)&&(\w+\.push\(\w+\.MouseAndKeyboard\))/.exec(str), match) str = str.replace(match[0], `(${match[1]} || window.BX_EXPOSED.hasCustomNativeMkb(productId)) && ${match[2]}`);return str;},patchStreamMetadata(str) {let index = str.indexOf("}onVideoFrame(");if (index >= 0 && (index = PatcherUtils.indexOf(str, "){", index, 30, !0)), index < 0) return !1;let code = ` -try {const obj = arguments[0];const baseMs = obj.frameSubmittedTimeMs;const renderMs = obj.frameRenderedTimeMs - obj.frameDecodedTimeMs;obj.frameDecodedTimeMs = baseMs + ${1};obj.frameRenderedTimeMs = obj.frameDecodedTimeMs + renderMs;obj.expectedDisplayTime = obj.frameRenderedTimeMs;arguments[0] = obj;} catch (e) { alert(e) } +`;return str = PatcherUtils.insertAt(str, index, newCode), str;},exposeReactCreateComponent(str) {let index = str.indexOf(".prototype.isReactComponent={}");if (index > -1 && (index = PatcherUtils.indexOf(str, ".createElement=", index)), index < 0) return !1;if (str = PatcherUtils.insertAt(str, index - 1, "window.BX_EXPOSED.reactCreateElement="), index = PatcherUtils.indexOf(str, ".useEffect=", index), index < 0) return !1;return str = PatcherUtils.insertAt(str, index - 1, "window.BX_EXPOSED.reactUseEffect="), str;},gameCardCustomIcons(str) {let initialIndex = str.indexOf("const{supportedInputIcons:");if (initialIndex < 0) return !1;let returnIndex = PatcherUtils.lastIndexOf(str, "return ", str.indexOf("SupportedInputsBadge"));if (returnIndex < 0) return !1;let productIdIndex = PatcherUtils.lastIndexOf(str, ",productId:", initialIndex, 300);if (productIdIndex < 0) return !1;let params = PatcherUtils.findAndParseParams(str, productIdIndex - 200, 400);if (!params || !params.productId) return !1;let productIdVar = params.productId, supportedInputIconsVar = PatcherUtils.getVariableNameAfter(str, PatcherUtils.indexOf(str, "supportedInputIcons:", initialIndex, 100, !0));if (!supportedInputIconsVar) return !1;let newCode = renderString(game_card_icons_default, {productId: productIdVar,supportedInputIcons: supportedInputIconsVar});return str = PatcherUtils.insertAt(str, returnIndex, newCode), str;},setImageQuality(str) {let index = str.indexOf("const{size:{width:");if (index > -1 && (index = PatcherUtils.indexOf(str, "=new URLSearchParams", index, 500)), index < 0) return !1;let paramVar = PatcherUtils.getVariableNameBefore(str, index);if (!paramVar) return !1;index = PatcherUtils.indexOf(str, "return", index, 200);let newCode = `${paramVar}.set('q', ${getGlobalPref("ui.imageQuality")});`;return str = PatcherUtils.insertAt(str, index, newCode), str;},setBackgroundImageQuality(str) {let index = str.indexOf("}?w=${");if (index > -1 && (index = PatcherUtils.indexOf(str, "}", index + 1, 10, !0)), index < 0) return !1;return str = PatcherUtils.insertAt(str, index, `&q=${getGlobalPref("ui.imageQuality")}`), str;},injectHeaderUseEffect(str) {let index = str.indexOf('className:"Header-module__header');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 300)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Script", "ui.header.rendered");},injectErrorPageUseEffect(str) {let index = str.indexOf('"PureErrorPage-module__container');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "})=>(0,", index, 200)), index < 0) return !1;return str = PatcherUtils.insertAt(str, index + 4, "{return "), str = PatcherUtils.injectUseEffect(str, index + 5, "Script", "ui.error.rendered"), str += "}", str;},injectStreamMenuUseEffect(str) {let index = str.indexOf('"StreamMenu-module__container');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Stream", "ui.streamMenu.rendered");},injectGuideHomeUseEffect(str) {let index = str.indexOf('"HomeLandingPage-module__authenticatedContentContainer');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Script", "ui.guideHome.rendered");},injectCreatePortal(str) {let index = str.indexOf(".createPortal=function");if (index > -1 && (index = PatcherUtils.indexOf(str, "{", index, 50, !0)), index < 0) return !1;return str = PatcherUtils.insertAt(str, index, create_portal_default), str;},injectAchievementsProgressUseEffect(str) {let index = str.indexOf('"AchievementsButton-module__progressBarContainer');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Script", "ui.guideAchievementProgress.rendered");},injectAchievementsDetailUseEffect(str) {let index = str.indexOf("GuideAchievementDetail.useParams()");if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "const", index, 200)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Script", "ui.guideAchievementDetail.rendered");},patchCustomInputIcon(str) {let index = str.indexOf('.MouseAndKeyboard="MouseAndKeyboard"');if (index < 0) return !1;let productIdMatch = /const (\w+)=(\w+)=>{/.exec(str.substring(index, index + 200));if (!productIdMatch) return !1;str = str.replace(productIdMatch[0], productIdMatch[0] + `const productId = ${productIdMatch[2]};`);let match = /(\w+)&&(\w+\.push\(\w+\.Touch\))/.exec(str);if (!match) return !1;if (str = str.replace(match[0], `(${match[1]} || window.BX_EXPOSED.hasCustomTouchControl(productId)) && ${match[2]}`), match = /(\w+)&&(\w+\.push\(\w+\.MouseAndKeyboard\))/.exec(str), match) str = str.replace(match[0], `(${match[1]} || window.BX_EXPOSED.hasCustomNativeMkb(productId)) && ${match[2]}`);return str;},patchStreamMetadata(str) {let index = str.indexOf("}onVideoFrame(");if (index >= 0 && (index = PatcherUtils.indexOf(str, "){", index, 30, !0)), index < 0) return !1;let maxDt = 10, code = ` +try {const obj = arguments[0];if (true || obj.frameDecodedTimeMs - obj.frameSubmittedTimeMs > ${maxDt}) {const baseMs = obj.frameSubmittedTimeMs;const renderMs = obj.frameRenderedTimeMs - obj.frameDecodedTimeMs;obj.frameDecodedTimeMs = baseMs + ${maxDt};obj.frameRenderedTimeMs = obj.frameDecodedTimeMs + renderMs;obj.expectedDisplayTime = obj.frameRenderedTimeMs;arguments[0] = obj;}} catch (e) { alert(e) } `;return str = PatcherUtils.insertAt(str, index, code), str;}}, PATCH_ORDERS = PatcherUtils.filterPatches([...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? ["enableNativeMkb","disableAbsoluteMouse"] : [],"exposeReactCreateComponent","injectCreatePortal","broadcastPollingMode",getGlobalPref("ui.gameCard.waitTime.show") && "patchSetCurrentFocus","patchGamepadPolling","modifyPreloadedState","detectBrowserRouterReady","exposeStreamSession","supportLocalCoOp","disableStreamGate","exposeDialogRoutes",...getGlobalPref("ui.imageQuality") < 90 ? ["setImageQuality"] : [],"patchRequestInfoCrash","injectErrorPageUseEffect","streamPageBeforeLoad","remotePlayStreamPageBeforeLoad","injectGuideHomeUseEffect","injectAchievementsProgressUseEffect","injectAchievementsDetailUseEffect","guideAchievementsDefaultLocked","injectHeaderUseEffect","homePageBeforeLoad","patchCustomInputIcon","gameCardCustomIcons","enableTvRoutes","overrideStorageGetSettings","detectProductDetailPage",getGlobalPref("ui.layout") !== "default" && "websiteLayout",getGlobalPref("game.fortnite.forceConsole") && "forceFortniteConsole",...STATES.userAgent.capabilities.touch ? ["disableTouchContextMenu"] : [],...getGlobalPref("block.tracking") ? ["disableAiTrack","blockWebRtcStatsCollector","disableIndexDbLogging","disableTelemetryProvider"] : [],...!getGlobalPref("block.features").includes("remote-play") ? ["remotePlayKeepAlive","remotePlayDisableAchievementToast",STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync"] : [],...BX_FLAGS.EnableXcloudLogging ? ["enableConsoleLogging","enableXcloudLogger"] : [] ]), hideSections = getGlobalPref("ui.hideSections"), HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([hideSections.includes("genres") && "ignoreGenresSection",hideSections.includes("byog") && "ignoreByogSection",STATES.browser.capabilities.touch && hideSections.includes("touch") && "ignorePlayWithTouchSection",getGlobalPref("ui.imageQuality") < 90 && "setBackgroundImageQuality",hideSections.some((value) => ["native-mkb", "most-popular"].includes(value)) && "ignoreSiglSections",hideSections.includes("news") && "ignoreNewsSection",(getGlobalPref("block.features").includes("friends") || hideSections.includes("friends")) && "ignorePlayWithFriendsSection",hideSections.includes("all-games") && "ignoreAllGamesSection",...blockSomeNotifications() ? ["changeNotificationsSubscription"] : [] -]), STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches(["exposeInputChannel","patchXcloudTitleInfo","disableGamepadDisconnectedScreen","patchStreamHud","playVibration","alwaysShowStreamHud","injectStreamMenuUseEffect","patchStreamMetadata",getGlobalPref("audio.volume.booster.enabled") && !getGlobalPref("stream.video.combineAudio") && "patchAudioMediaStream",getGlobalPref("audio.volume.booster.enabled") && getGlobalPref("stream.video.combineAudio") && "patchCombinedAudioVideoMediaStream",getGlobalPref("ui.feedbackDialog.disabled") && "skipFeedbackDialog",...STATES.userAgent.capabilities.touch ? [getGlobalPref("touchController.mode") === "all" && "patchShowSensorControls",getGlobalPref("touchController.mode") === "all" && "exposeTouchLayoutManager",(getGlobalPref("touchController.mode") === "off" || getGlobalPref("touchController.autoOff")) && "disableTakRenderer",getGlobalPref("touchController.opacity.default") !== 100 && "patchTouchControlDefaultOpacity",getGlobalPref("touchController.mode") !== "off" && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on") && "patchBabylonRendererClass"] : [],"patchPollGamepads",getGlobalPref("stream.video.combineAudio") && "streamCombineSources",...!getGlobalPref("block.features").includes("remote-play") ? ["remotePlayPostStreamRedirectUrl","patchRemotePlayMkb"] : [],...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? ["patchMouseAndKeyboardEnabled","disableNativeRequestPointerLock"] : [] +]), STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches(["exposeInputChannel","patchXcloudTitleInfo","disableGamepadDisconnectedScreen","patchStreamHud","playVibration","alwaysShowStreamHud","injectStreamMenuUseEffect",getGlobalPref("stream.video.preventResolutionDrops") && "patchStreamMetadata",getGlobalPref("audio.volume.booster.enabled") && !getGlobalPref("stream.video.combineAudio") && "patchAudioMediaStream",getGlobalPref("audio.volume.booster.enabled") && getGlobalPref("stream.video.combineAudio") && "patchCombinedAudioVideoMediaStream",getGlobalPref("ui.feedbackDialog.disabled") && "skipFeedbackDialog",...STATES.userAgent.capabilities.touch ? [getGlobalPref("touchController.mode") === "all" && "patchShowSensorControls",getGlobalPref("touchController.mode") === "all" && "exposeTouchLayoutManager",(getGlobalPref("touchController.mode") === "off" || getGlobalPref("touchController.autoOff")) && "disableTakRenderer",getGlobalPref("touchController.opacity.default") !== 100 && "patchTouchControlDefaultOpacity",getGlobalPref("touchController.mode") !== "off" && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on") && "patchBabylonRendererClass"] : [],"patchPollGamepads",getGlobalPref("stream.video.combineAudio") && "streamCombineSources",...!getGlobalPref("block.features").includes("remote-play") ? ["remotePlayPostStreamRedirectUrl","patchRemotePlayMkb"] : [],...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? ["patchMouseAndKeyboardEnabled","disableNativeRequestPointerLock"] : [] ]), ALL_PATCHES = [...PATCH_ORDERS, ...HOME_PAGE_PATCH_ORDERS, ...STREAM_PAGE_PATCH_ORDERS]; class Patcher {static remainingPatches = {home: HOME_PAGE_PATCH_ORDERS,stream: STREAM_PAGE_PATCH_ORDERS,"remote-play-stream": STREAM_PAGE_PATCH_ORDERS};static patchPage(page) {let remaining = Patcher.remainingPatches[page];if (!remaining) return;PATCH_ORDERS = PATCH_ORDERS.concat(remaining), delete Patcher.remainingPatches[page];}static patchNativeBind() {let nativeBind = Function.prototype.bind;Function.prototype.bind = function() {let valid = !1;if (this.name.length <= 2 && arguments.length === 2 && arguments[0] === null) {if (arguments[1] === 0 || typeof arguments[1] === "function") valid = !0;}if (!valid) return nativeBind.apply(this, arguments);if (typeof arguments[1] === "function") BxLogger.info(LOG_TAG2, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind;let orgFunc = this, newFunc = (a, item2) => {Patcher.checkChunks(item2), orgFunc(a, item2);};return nativeBind.apply(newFunc, arguments);};}static checkChunks(item) {let patchesToCheck, appliedPatches, chunkData = item[1], patchesMap = {}, patcherCache = PatcherCache.getInstance();for (let chunkId in chunkData) {appliedPatches = [];let cachedPatches = patcherCache.getPatches(chunkId);if (cachedPatches) patchesToCheck = cachedPatches.slice(0), patchesToCheck.push(...PATCH_ORDERS);else patchesToCheck = PATCH_ORDERS.slice(0);if (!patchesToCheck.length) continue;let func = chunkData[chunkId], funcStr = func.toString(), patchedFuncStr = funcStr, modified = !1, chunkAppliedPatches = [];for (let patchIndex = 0;patchIndex < patchesToCheck.length; patchIndex++) {let patchName = patchesToCheck[patchIndex];if (appliedPatches.indexOf(patchName) > -1) continue;if (!PATCHES[patchName]) continue;let tmpStr = PATCHES[patchName].call(null, patchedFuncStr);if (!tmpStr) continue;modified = !0, patchedFuncStr = tmpStr, appliedPatches.push(patchName), chunkAppliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName);}if (modified) {BxLogger.info(LOG_TAG2, `✅ [${chunkId}] ${chunkAppliedPatches.join(", ")}`), PATCH_ORDERS.length && BxLogger.info(LOG_TAG2, "Remaining patches", PATCH_ORDERS), BX_FLAGS.Debug && console.time(LOG_TAG2);try {chunkData[chunkId] = eval(patchedFuncStr);} catch (e) {if (e instanceof Error) BxLogger.error(LOG_TAG2, "Error", appliedPatches, e.message, patchedFuncStr);}BX_FLAGS.Debug && console.timeEnd(LOG_TAG2);}if (appliedPatches.length) patchesMap[chunkId] = appliedPatches;}if (Object.keys(patchesMap).length) patcherCache.saveToCache(patchesMap);}static init() {Patcher.patchNativeBind();}} class PatcherCache {static instance;static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache);KEY_CACHE = "BetterXcloud.Patches.Cache";KEY_SIGNATURE = "BetterXcloud.Patches.Cache.Signature";CACHE;constructor() {this.checkSignature(), this.CACHE = JSON.parse(window.localStorage.getItem(this.KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG2, "Cache", this.CACHE);let pathName = window.location.pathname;if (pathName.includes("/play/consoles/launch/")) Patcher.patchPage("remote-play-stream");else if (pathName.includes("/play/launch/")) Patcher.patchPage("stream");else if (pathName.endsWith("/play") || pathName.endsWith("/play/")) Patcher.patchPage("home");PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS), STREAM_PAGE_PATCH_ORDERS = this.cleanupPatches(STREAM_PAGE_PATCH_ORDERS), BxLogger.info(LOG_TAG2, "PATCH_ORDERS", PATCH_ORDERS.slice(0));}getSignature() {let scriptVersion = SCRIPT_VERSION, patches = JSON.stringify(ALL_PATCHES), clientHash = "", $link = document.querySelector('link[data-chunk="client"][as="script"][href*="/client."]');if ($link) {let match = /\/client\.([^\.]+)\.js/.exec($link.href);match && (clientHash = match[1]);}let webVersion = document.querySelector("meta[name=gamepass-app-version]")?.content ?? "", webVersionDate = document.querySelector("meta[name=gamepass-app-date]")?.content ?? "";return `${scriptVersion}:${clientHash}:${webVersion}:${webVersionDate}:${hashCode(patches)}`;}clear() {window.localStorage.removeItem(this.KEY_CACHE), this.CACHE = {};}checkSignature() {let storedSig = window.localStorage.getItem(this.KEY_SIGNATURE) || 0, currentSig = this.getSignature();if (currentSig !== storedSig) BxLogger.warning(LOG_TAG2, "Signature changed"), window.localStorage.setItem(this.KEY_SIGNATURE, currentSig.toString()), this.clear();else BxLogger.info(LOG_TAG2, "Signature unchanged");}cleanupPatches(patches) {return patches.filter((item2) => {for (let id in this.CACHE)if (this.CACHE[id].includes(item2)) return !1;return !0;});}getPatches(id) {return this.CACHE[id];}saveToCache(subCache) {for (let id in subCache) {let patchNames = subCache[id], data = this.CACHE[id];if (!data) this.CACHE[id] = patchNames;else for (let patchName of patchNames)if (!data.includes(patchName)) data.push(patchName);}window.localStorage.setItem(this.KEY_CACHE, JSON.stringify(this.CACHE));}} @@ -215,7 +215,7 @@ class KeyBindingDialog {static instance;static getInstance = () => KeyBindingDia class MkbMappingManagerDialog extends BaseProfileManagerDialog {static instance;static getInstance = () => MkbMappingManagerDialog.instance ?? (MkbMappingManagerDialog.instance = new MkbMappingManagerDialog(t("virtual-controller")));KEYS_PER_BUTTON = 2;BUTTONS_ORDER = [16,12,13,14,15,0,1,2,3,4,5,6,7,8,9,10,100,101,102,103,11,200,201,202,203];allKeyElements = [];$mouseMapTo;$mouseSensitivityX;$mouseSensitivityY;$mouseDeadzone;$unbindNote;constructor(title) {super(title, MkbMappingPresetsTable.getInstance());this.render();}onBindingKey = (e) => {if (e.target.disabled) return;if (e.button !== 0) return;};parseDataset($btn) {let dataset = $btn.dataset;return {keySlot: parseInt(dataset.keySlot),buttonIndex: parseInt(dataset.buttonIndex)};}onKeyChanged = (e) => {let $current = e.target, keyInfo = $current.keyInfo;if (keyInfo) {for (let $elm of this.allKeyElements)if ($elm !== $current && $elm.keyInfo?.code === keyInfo.code) $elm.unbindKey(!0);}this.savePreset();};render() {let $rows = CE("div", !1, this.$unbindNote = CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind")));for (let buttonIndex of this.BUTTONS_ORDER) {let [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex], $elm, $fragment = document.createDocumentFragment();for (let i = 0;i < this.KEYS_PER_BUTTON; i++)$elm = BxKeyBindingButton.create({title: buttonPrompt,isPrompt: !0,allowedFlags: [1, 4, 8],onChanged: this.onKeyChanged}), $elm.dataset.buttonIndex = buttonIndex.toString(), $elm.dataset.keySlot = i.toString(), $elm.addEventListener("mouseup", this.onBindingKey), $fragment.appendChild($elm), this.allKeyElements.push($elm);let $keyRow = CE("div", {class: "bx-mkb-key-row",_nearby: { orientation: "horizontal" }}, CE("label", { title: buttonName }, buttonPrompt), $fragment);$rows.appendChild($keyRow);}let savePreset = () => this.savePreset(), $extraSettings = CE("div", !1, createSettingRow(t("map-mouse-to"), this.$mouseMapTo = BxSelectElement.create(CE("select", { _on: { input: savePreset } }, CE("option", { value: 2 }, t("right-stick")), CE("option", { value: 1 }, t("left-stick")), CE("option", { value: 0 }, t("off"))))), createSettingRow(t("horizontal-sensitivity"), this.$mouseSensitivityX = BxNumberStepper.create("hor_sensitivity", 0, 1, 300, {suffix: "%",exactTicks: 50}, savePreset)), createSettingRow(t("vertical-sensitivity"), this.$mouseSensitivityY = BxNumberStepper.create("ver_sensitivity", 0, 1, 300, {suffix: "%",exactTicks: 50}, savePreset)), createSettingRow(t("deadzone-counterweight"), this.$mouseDeadzone = BxNumberStepper.create("deadzone_counterweight", 0, 1, 50, {suffix: "%",exactTicks: 10}, savePreset)));this.$content = CE("div", !1, $rows, $extraSettings);}switchPreset(id) {let preset = this.allPresets.data[id];if (!preset) {this.currentPresetId = 0;return;}let presetData = preset.data;this.currentPresetId = id;let isDefaultPreset = id <= 0;this.updateButtonStates(), this.$unbindNote.classList.toggle("bx-gone", isDefaultPreset);for (let $elm of this.allKeyElements) {let { buttonIndex, keySlot } = this.parseDataset($elm), buttonKeys = presetData.mapping[buttonIndex];if (buttonKeys && buttonKeys[keySlot]) $elm.bindKey({code: buttonKeys[keySlot]}, !0);else $elm.unbindKey(!0);$elm.disabled = isDefaultPreset;}let mouse = presetData.mouse;this.$mouseMapTo.value = mouse.mapTo.toString(), this.$mouseSensitivityX.value = mouse.sensitivityX.toString(), this.$mouseSensitivityY.value = mouse.sensitivityY.toString(), this.$mouseDeadzone.value = mouse.deadzoneCounterweight.toString(), this.$mouseMapTo.disabled = isDefaultPreset, this.$mouseSensitivityX.dataset.disabled = isDefaultPreset.toString(), this.$mouseSensitivityY.dataset.disabled = isDefaultPreset.toString(), this.$mouseDeadzone.dataset.disabled = isDefaultPreset.toString();}savePreset() {let presetData = deepClone(this.presetsDb.BLANK_PRESET_DATA);for (let $elm of this.allKeyElements) {let { buttonIndex, keySlot } = this.parseDataset($elm), mapping = presetData.mapping;if (!mapping[buttonIndex]) mapping[buttonIndex] = [];if (!$elm.keyInfo) delete mapping[buttonIndex][keySlot];else mapping[buttonIndex][keySlot] = $elm.keyInfo.code;}let mouse = presetData.mouse;mouse.mapTo = parseInt(this.$mouseMapTo.value), mouse.sensitivityX = parseInt(this.$mouseSensitivityX.value), mouse.sensitivityY = parseInt(this.$mouseSensitivityY.value), mouse.deadzoneCounterweight = parseInt(this.$mouseDeadzone.value);let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = {id: this.currentPresetId,name: oldPreset.name,data: presetData};this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset;}onBeforeUnmount() {StreamSettings.refreshMkbSettings(), super.onBeforeUnmount();}} class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog {static instance;static getInstance = () => KeyboardShortcutsManagerDialog.instance ?? (KeyboardShortcutsManagerDialog.instance = new KeyboardShortcutsManagerDialog(t("keyboard-shortcuts")));$content;$unbindNote;allKeyElements = [];constructor(title) {super(title, KeyboardShortcutsTable.getInstance());let $rows = CE("div", { class: "bx-keyboard-shortcuts-manager-container" });for (let groupLabel in SHORTCUT_ACTIONS) {let items = SHORTCUT_ACTIONS[groupLabel];if (!items) continue;let $fieldSet = CE("fieldset", !1, CE("legend", !1, groupLabel));for (let action in items) {let crumbs = items[action];if (!crumbs) continue;let label = crumbs.join(" ❯ "), $btn = BxKeyBindingButton.create({title: label,isPrompt: !1,onChanged: this.onKeyChanged,allowedFlags: [1, 2]});$btn.classList.add("bx-full-width"), $btn.dataset.action = action, this.allKeyElements.push($btn);let $row = createSettingRow(label, CE("div", { class: "bx-binding-button-wrapper" }, $btn));$fieldSet.appendChild($row);}if ($fieldSet.childElementCount > 1) $rows.appendChild($fieldSet);}this.$content = CE("div", !1, this.$unbindNote = CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind")), $rows);}onKeyChanged = (e) => {let $current = e.target, keyInfo = $current.keyInfo;if (keyInfo) for (let $elm of this.allKeyElements) {if ($elm === $current) continue;if ($elm.keyInfo?.code === keyInfo.code && $elm.keyInfo?.modifiers === keyInfo.modifiers) $elm.unbindKey(!0);}this.savePreset();};parseDataset($btn) {return {action: $btn.dataset.action};}switchPreset(id) {let preset = this.allPresets.data[id];if (!preset) {this.currentPresetId = 0;return;}let presetData = preset.data;this.currentPresetId = id;let isDefaultPreset = id <= 0;this.updateButtonStates(), this.$unbindNote.classList.toggle("bx-gone", isDefaultPreset);for (let $elm of this.allKeyElements) {let { action } = this.parseDataset($elm), keyInfo = presetData.mapping[action];if (keyInfo) $elm.bindKey(keyInfo, !0);else $elm.unbindKey(!0);$elm.disabled = isDefaultPreset;}}savePreset() {let presetData = deepClone(this.presetsDb.BLANK_PRESET_DATA);for (let $elm of this.allKeyElements) {let { action } = this.parseDataset($elm), mapping = presetData.mapping;if ($elm.keyInfo) mapping[action] = $elm.keyInfo;}let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = {id: this.currentPresetId,name: oldPreset.name,data: presetData};this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset;}onBeforeUnmount() {StreamSettings.refreshKeyboardShortcuts(), super.onBeforeUnmount();}} class MkbExtraSettings extends HTMLElement {$mappingPresets;$shortcutsPresets;updateLayout;saveMkbSettings;saveShortcutsSettings;static renderSettings() {let $container = document.createDocumentFragment();$container.updateLayout = MkbExtraSettings.updateLayout.bind($container), $container.saveMkbSettings = MkbExtraSettings.saveMkbSettings.bind($container), $container.saveShortcutsSettings = MkbExtraSettings.saveShortcutsSettings.bind($container);let $mappingPresets = BxSelectElement.create(CE("select", {autocomplete: "off",_on: {input: $container.saveMkbSettings}})), $shortcutsPresets = BxSelectElement.create(CE("select", {autocomplete: "off",_on: {input: $container.saveShortcutsSettings}}));return $container.append(...getGlobalPref("mkb.enabled") ? [createSettingRow(t("virtual-controller"), CE("div", {class: "bx-preset-row",_nearby: {orientation: "horizontal"}}, $mappingPresets, createButton({title: t("manage"),icon: BxIcon.MANAGE,style: 64 | 1 | 512,onClick: () => MkbMappingManagerDialog.getInstance().show({id: parseInt($container.$mappingPresets.value)})})), {multiLines: !0,onContextMenu: this.boundOnContextMenu,pref: "mkb.p1.preset.mappingId"}),createSettingRow(t("virtual-controller-slot"), this.settingsManager.getElement("mkb.p1.slot"), {onContextMenu: this.boundOnContextMenu,pref: "mkb.p1.slot"})] : [], createSettingRow(t("in-game-keyboard-shortcuts"), CE("div", {class: "bx-preset-row",_nearby: {orientation: "horizontal"}}, $shortcutsPresets, createButton({title: t("manage"),icon: BxIcon.MANAGE,style: 64 | 1 | 512,onClick: () => KeyboardShortcutsManagerDialog.getInstance().show({id: parseInt($container.$shortcutsPresets.value)})})), {multiLines: !0,onContextMenu: this.boundOnContextMenu,pref: "keyboardShortcuts.preset.inGameId"})), $container.$mappingPresets = $mappingPresets, $container.$shortcutsPresets = $shortcutsPresets, this.settingsManager.setElement("keyboardShortcuts.preset.inGameId", $shortcutsPresets), this.settingsManager.setElement("mkb.p1.preset.mappingId", $mappingPresets), $container.updateLayout(), this.onMountedCallbacks.push(() => {$container.updateLayout();}), $container;}static async updateLayout() {let mappingPresets = await MkbMappingPresetsTable.getInstance().getPresets();renderPresetsList(this.$mappingPresets, mappingPresets, getStreamPref("mkb.p1.preset.mappingId"));let shortcutsPresets = await KeyboardShortcutsTable.getInstance().getPresets();renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getStreamPref("keyboardShortcuts.preset.inGameId"), { addOffValue: !0 });}static async saveMkbSettings() {let presetId = parseInt(this.$mappingPresets.value);setStreamPref("mkb.p1.preset.mappingId", presetId, "ui");}static async saveShortcutsSettings() {let presetId = parseInt(this.$shortcutsPresets.value);setStreamPref("keyboardShortcuts.preset.inGameId", presetId, "ui");}} -class SettingsDialog extends NavigationDialog {static instance;static getInstance = () => SettingsDialog.instance ?? (SettingsDialog.instance = new SettingsDialog);LOG_TAG = "SettingsNavigationDialog";$container;$tabs;$tabContents;$btnReload;$btnGlobalReload;$noteGlobalReload;$btnSuggestion;$streamSettingsSelection;renderFullSettings;boundOnContextMenu;suggestedSettings = {recommended: {},default: {},lowest: {},highest: {}};settingLabels = {};settingsManager;TAB_GLOBAL_ITEMS = [{group: "general",label: t("better-xcloud"),helpUrl: "https://better-xcloud.github.io/features/",items: [($parent) => {let PREF_LATEST_VERSION = getGlobalPref("version.latest"), topButtons = [];if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {let opts = {label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }),style: 1 | 64 | 128};if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript();else opts.url = "https://github.com/redphx/better-xcloud/releases/latest";topButtons.push(createButton(opts));}if (AppInterface) topButtons.push(createButton({label: t("app-settings"),icon: BxIcon.STREAM_SETTINGS,style: 128 | 64,onClick: (e) => {AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide();}}));else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({label: "🔥 " + t("install-android"),style: 128 | 64,url: "https://better-xcloud.github.io/android"}));this.$btnGlobalReload = createButton({label: t("settings-reload"),classes: ["bx-settings-reload-button", "bx-gone"],style: 64 | 128,onClick: (e) => {this.reloadPage();}}), topButtons.push(this.$btnGlobalReload), this.$noteGlobalReload = CE("span", {class: "bx-settings-reload-note"}, t("settings-reload-note")), topButtons.push(this.$noteGlobalReload), this.$btnSuggestion = CE("div", {class: "bx-suggest-toggler bx-focusable",tabindex: 0}, CE("label", !1, t("suggest-settings")), CE("span", !1, "❯")), this.$btnSuggestion.addEventListener("click", SuggestionsSetting.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion);let $div = CE("div", {class: "bx-top-buttons",_nearby: {orientation: "vertical"}}, ...topButtons);$parent.appendChild($div);},{pref: "bx.locale",multiLines: !0},"server.bypassRestriction","ui.controllerFriendly"]}, {group: "server",label: t("server"),items: [{pref: "server.region",multiLines: !0},{pref: "stream.locale",multiLines: !0},"server.ipv6.prefer"]}, {group: "stream",label: t("stream"),items: ["stream.video.resolution","stream.video.codecProfile","stream.video.maxBitrate","audio.volume.booster.enabled","screenshot.applyFilters","audio.mic.onPlaying","game.fortnite.forceConsole","stream.video.combineAudio"]}, {requiredVariants: "full",group: "mkb",label: t("mouse-and-keyboard"),items: ["nativeMkb.mode",{pref: "nativeMkb.forcedGames",multiLines: !0,note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/574", target: "_blank" }, t("unofficial-game-list"))},"mkb.enabled","mkb.cursor.hideIdle"],...!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? {unsupported: !0,unsupportedNote: CE("a", {href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657",target: "_blank"}, "⚠️ " + t("browser-unsupported-feature"))} : {}}, {requiredVariants: "full",group: "touch-control",label: t("touch-controller"),items: [{pref: "touchController.mode",note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/241", target: "_blank" }, t("unofficial-game-list"))},"touchController.autoOff","touchController.opacity.default","touchController.style.standard","touchController.style.custom"],...!STATES.userAgent.capabilities.touch ? {unsupported: !0,unsupportedNote: "⚠️ " + t("device-unsupported-touch")} : {}}, {group: "ui",label: t("ui"),items: ["ui.layout","ui.theme","ui.imageQuality","ui.gameCard.waitTime.show","ui.controllerStatus.show","ui.streamMenu.simplify","ui.splashVideo.skip",!AppInterface && "ui.hideScrollbar","ui.systemMenu.hideHandle","ui.feedbackDialog.disabled","ui.reduceAnimations",{pref: "ui.hideSections",multiLines: !0},{pref: "block.features",multiLines: !0}]}, {requiredVariants: "full",group: "game-bar",label: t("game-bar"),items: ["gameBar.position"]}, {group: "loading-screen",label: t("loading-screen"),items: ["loadingScreen.gameArt.show","loadingScreen.waitTime.show","loadingScreen.rocket"]}, {group: "other",label: t("other"),items: ["block.tracking"]}, {group: "advanced",label: t("advanced"),items: [{pref: "userAgent.profile",multiLines: !0,onCreated: (setting, $control) => {let defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", {type: "text",placeholder: defaultUserAgent,autocomplete: "off",class: "bx-settings-custom-user-agent",tabindex: 0});$inpCustomUserAgent.addEventListener("input", (e) => {let profile = $control.value, custom = e.target.value.trim();UserAgent.updateStorage(profile, custom), this.onGlobalSettingChanged(e);}), $control.insertAdjacentElement("afterend", $inpCustomUserAgent), setNearby($inpCustomUserAgent.parentElement, {orientation: "vertical"});}}]}, {group: "footer",items: [($parent) => {try {let appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10);$parent.appendChild(CE("div", {class: "bx-settings-app-version"}, `xCloud website version ${appVersion} (${appDate})`));} catch (e) {}},($parent) => {$parent.appendChild(CE("a", {class: "bx-donation-link",href: "https://ko-fi.com/redphx",target: "_blank",tabindex: 0}, `❤️ ${t("support-better-xcloud")}`));},($parent) => {$parent.appendChild(createButton({label: t("clear-data"),style: 4 | 16 | 128 | 64,onClick: (e) => {if (confirm(t("clear-data-confirm"))) clearAllData();}}));},($parent) => {$parent.appendChild(CE("div", { class: "bx-debug-info" }, createButton({label: "Debug info",style: 8 | 128 | 64,onClick: (e) => {let $button = e.target.closest("button");if (!$button) return;let $pre = $button.nextElementSibling;if (!$pre) {let debugInfo = deepClone(BX_FLAGS.DeviceInfo);debugInfo.settings = JSON.parse(window.localStorage.getItem("BetterXcloud") || "{}"), $pre = CE("pre", {class: "bx-focusable bx-gone",tabindex: 0,_on: {click: async (e2) => {await copyToClipboard(e2.target.innerText);}}}, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```"), $button.insertAdjacentElement("afterend", $pre);}$pre.classList.toggle("bx-gone"), $pre.scrollIntoView();}})));}]}];TAB_DISPLAY_ITEMS = [{requiredVariants: "full",group: "audio",label: t("audio"),helpUrl: "https://better-xcloud.github.io/ingame-features/#audio",items: [{pref: "audio.volume",params: {disabled: !getGlobalPref("audio.volume.booster.enabled")},onCreated: (setting, $elm) => {let $range = $elm.querySelector("input[type=range");BxEventBus.Stream.on("setting.changed", (payload) => {let { settingKey } = payload;if (settingKey === "audio.volume") $range.value = getStreamPref(settingKey).toString(), BxEvent.dispatch($range, "input", { ignoreOnChange: !0 });});}}]}, {group: "video",label: t("video"),helpUrl: "https://better-xcloud.github.io/ingame-features/#video",items: ["video.player.type","video.maxFps","video.player.powerPreference","video.processing","video.processing.mode","video.ratio","video.position","video.processing.sharpness","video.saturation","video.contrast","video.brightness"]}];TAB_CONTROLLER_ITEMS = [{group: "controller",label: t("controller"),helpUrl: "https://better-xcloud.github.io/ingame-features/#controller",items: ["localCoOp.enabled","controller.pollingRate",($parent) => {$parent.appendChild(ControllerExtraSettings.renderSettings.apply(this));}]},STATES.userAgent.capabilities.touch && {group: "touch-control",label: t("touch-controller"),items: [{label: t("layout"),content: CE("select", {disabled: !0}, CE("option", !1, t("default"))),onCreated: (setting, $elm) => {$elm.addEventListener("input", (e) => {TouchController.applyCustomLayout($elm.value, 1000);}), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => {let customLayouts = TouchController.getCustomLayouts();while ($elm.firstChild)$elm.removeChild($elm.firstChild);if ($elm.disabled = !customLayouts, !customLayouts) {$elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("input"));return;}let $fragment = document.createDocumentFragment();for (let key in customLayouts.layouts) {let layout = customLayouts.layouts[key], name;if (layout.author) name = `${layout.name} (${layout.author})`;else name = layout.name;let $option = CE("option", { value: key }, name);$fragment.appendChild($option);}$elm.appendChild($fragment), $elm.value = customLayouts.default_layout;});}}]},STATES.browser.capabilities.deviceVibration && {group: "device",label: t("device"),items: [{pref: "deviceVibration.mode",multiLines: !0,unsupported: !STATES.browser.capabilities.deviceVibration}, {pref: "deviceVibration.intensity",unsupported: !STATES.browser.capabilities.deviceVibration}]}];TAB_MKB_ITEMS = [{requiredVariants: "full",group: "mkb",label: t("mouse-and-keyboard"),helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/",items: [($parent) => {$parent.appendChild(MkbExtraSettings.renderSettings.apply(this));}]},NativeMkbHandler.isAllowed() && {requiredVariants: "full",group: "native-mkb",label: t("native-mkb"),items: ["nativeMkb.scroll.sensitivityY","nativeMkb.scroll.sensitivityX"]}];TAB_STATS_ITEMS = [{group: "stats",label: t("stream-stats"),helpUrl: "https://better-xcloud.github.io/stream-stats/",items: ["stats.showWhenPlaying","stats.quickGlance.enabled","stats.items","stats.position","stats.textSize","stats.opacity.all","stats.opacity.background","stats.colors"]}];SETTINGS_UI = {global: {group: "global",icon: BxIcon.HOME,items: this.TAB_GLOBAL_ITEMS},stream: {group: "stream",icon: BxIcon.DISPLAY,items: this.TAB_DISPLAY_ITEMS},controller: {group: "controller",icon: BxIcon.CONTROLLER,items: this.TAB_CONTROLLER_ITEMS,requiredVariants: "full"},mkb: {group: "mkb",icon: BxIcon.NATIVE_MKB,items: this.TAB_MKB_ITEMS,requiredVariants: "full"},stats: {group: "stats",icon: BxIcon.STREAM_STATS,items: this.TAB_STATS_ITEMS}};constructor() {super();BxLogger.info(this.LOG_TAG, "constructor()"), this.boundOnContextMenu = this.onContextMenu.bind(this), this.settingsManager = SettingsManager.getInstance(), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(), this.onMountedCallbacks.push(() => {if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);let $selectUserAgent = document.querySelector(`#bx_setting_${escapeCssSelector("userAgent.profile")}`);if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1;}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => {this.$tabContents.dataset.gameId = id.toString();});}getDialog() {return this;}getContent() {return this.$container;}onMounted() {super.onMounted();}isOverlayVisible() {return !STATES.isPlaying;}reloadPage() {this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload();}isSupportedVariant(requiredVariants) {if (typeof requiredVariants === "undefined") return !0;return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT);}onTabClicked = (e) => {let $svg = e.target.closest("svg"), $child, children = Array.from(this.$tabContents.children);for ($child of children)if ($child.dataset.tabGroup === $svg.dataset.group) $child.classList.remove("bx-gone"), calculateSelectBoxes($child);else if ($child.dataset.tabGroup) $child.classList.add("bx-gone");this.$streamSettingsSelection.classList.toggle("bx-gone", $svg.dataset.group === "global");for (let $child2 of Array.from(this.$tabs.children))$child2.classList.remove("bx-active");$svg.classList.add("bx-active");};renderTab(settingTab) {let $svg = createSvgIcon(settingTab.icon);return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", this.onTabClicked), $svg;}onGlobalSettingChanged = (e) => {PatcherCache.getInstance().clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger");};onContextMenu(e) {e.preventDefault();let $elm = e.target;$elm instanceof HTMLElement && this.resetHighlightedSetting($elm);}renderServerSetting(setting) {let selectedValue = getGlobalPref("server.region"), continents = {"america-north": {label: t("continent-north-america")},"america-south": {label: t("continent-south-america")},asia: {label: t("continent-asia")},australia: {label: t("continent-australia")},europe: {label: t("continent-europe")},other: {label: t("other")}}, $control = CE("select", {id: `bx_setting_${escapeCssSelector(setting.pref)}`,tabindex: 0});$control.name = $control.id, $control.addEventListener("input", (e) => {setGlobalPref(setting.pref, e.target.value, "ui"), this.onGlobalSettingChanged(e);}), setting.options = {};for (let regionName in STATES.serverRegions) {let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`;if (region.isDefault) {if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default";}setting.options[value] = label;let $option = CE("option", { value }, label), continent = continents[region.contintent];if (!continent.children) continent.children = [];continent.children.push($option);}let fragment = document.createDocumentFragment(), key;for (key in continents) {let continent = continents[key];if (!continent.children) continue;fragment.appendChild(CE("optgroup", {label: continent.label}, ...continent.children));}return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;}renderSettingRow(settingTab, $tabContent, settingTabContent, setting) {if (typeof setting === "string") setting = {pref: setting};let pref = setting.pref, $control;if (setting.content) if (typeof setting.content === "function") $control = setting.content.apply(this);else $control = setting.content;else if (!setting.unsupported) {if (pref === "server.region") $control = this.renderServerSetting(setting);else if (pref === "bx.locale") $control = SettingElement.fromPref(pref, async (e) => {let newLocale = e.target.value;if (getGlobalPref("ui.controllerFriendly")) {let timeoutId = e.target.timeoutId;timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => {Translations.refreshLocale(newLocale), Translations.updateTranslations();}, 500);} else Translations.refreshLocale(newLocale), Translations.updateTranslations();this.onGlobalSettingChanged(e);});else if (pref === "userAgent.profile") $control = SettingElement.fromPref("userAgent.profile", (e) => {let value = e.target.value, isCustom = value === "custom", userAgent2 = UserAgent.get(value);UserAgent.updateStorage(value);let $inp = $control.nextElementSibling;$inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e);});else if ($control = this.settingsManager.getElement(pref, setting.params), settingTab.group === "global") $control.addEventListener("input", this.onGlobalSettingChanged);if ($control instanceof HTMLSelectElement) $control = BxSelectElement.create($control);}let prefDefinition = null;if (pref) prefDefinition = getPrefInfo(pref).definition;if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return;let label = prefDefinition?.label || setting.label || "", note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental;if (typeof note === "function") note = note();if (typeof unsupportedNote === "function") unsupportedNote = unsupportedNote();if (settingTabContent.label && setting.pref) {if (prefDefinition?.suggest) typeof prefDefinition.suggest.lowest !== "undefined" && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest), typeof prefDefinition.suggest.highest !== "undefined" && (this.suggestedSettings.highest[setting.pref] = prefDefinition.suggest.highest);}if (experimental) if (label = "🧪 " + label, !note) note = t("experimental");else note = `${t("experimental")}: ${note}`;let $note;if (unsupportedNote) $note = CE("div", { class: "bx-settings-dialog-note" }, unsupportedNote);else if (note) $note = CE("div", { class: "bx-settings-dialog-note" }, note);let $row = createSettingRow(label, !prefDefinition?.unsupported && $control, {$note,multiLines: setting.multiLines,icon: prefDefinition?.labelIcon,onContextMenu: this.boundOnContextMenu,pref});if (pref) $row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`;if ($row.dataset.type = settingTabContent.group, !STATES.supportedRegion && setting.pref === "server.bypassRestriction") $row.classList.add("bx-settings-important-row");$tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control);}renderSettingsSection(settingTab, sections) {let $tabContent = CE("div", {class: "bx-gone",_dataset: {tabGroup: settingTab.group}});for (let section of sections) {if (!section) continue;if (section instanceof HTMLElement) {$tabContent.appendChild(section);continue;}if (!this.isSupportedVariant(section.requiredVariants)) continue;if (!this.renderFullSettings && settingTab.group === "global" && section.group !== "general" && section.group !== "footer") continue;let label = section.label;if (label === t("better-xcloud")) {if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)";label = createButton({label,url: "https://github.com/redphx/better-xcloud/releases",style: 4096 | 16 | 64});}if (label) {let $title = CE("h2", {_nearby: {orientation: "horizontal"}}, CE("span", !1, label), section.helpUrl && createButton({icon: BxIcon.QUESTION,style: 8 | 64,url: section.helpUrl,title: t("help")}));$tabContent.appendChild($title);}if (section.unsupportedNote) {let $note = CE("b", { class: "bx-note-unsupported" }, section.unsupportedNote);$tabContent.appendChild($note);}if (section.unsupported) continue;if (section.content) {$tabContent.appendChild(section.content);continue;}section.items = section.items || [];for (let setting of section.items) {if (setting === !1) continue;if (typeof setting === "function") {setting.apply(this, [$tabContent]);continue;}this.renderSettingRow(settingTab, $tabContent, section, setting);}}return $tabContent;}setupDialog() {let $tabs, $tabContents, $container = CE("div", {class: "bx-settings-dialog",_nearby: {orientation: "horizontal"}}, CE("div", {class: "bx-settings-tabs-container",_nearby: {orientation: "vertical",focus: () => {return this.dialogManager.focus($tabs);},loop: (direction) => {if (direction === 1 || direction === 3) return this.focusVisibleTab(direction === 1 ? "last" : "first"), !0;return !1;}}}, $tabs = CE("div", {class: "bx-settings-tabs bx-hide-scroll-bar",_nearby: {focus: () => this.focusActiveTab()}}), CE("div", !1, this.$btnReload = createButton({icon: BxIcon.REFRESH,style: 64 | 32,onClick: (e) => {this.reloadPage();}}), createButton({icon: BxIcon.CLOSE,style: 64 | 32,onClick: (e) => {this.dialogManager.hide();}}))), CE("div", {class: "bx-settings-tab-contents",_nearby: {orientation: "vertical",loop: (direction) => {if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0;return !1;}}}, this.$streamSettingsSelection = SettingsManager.getInstance().getStreamSettingsSelection(), $tabContents = CE("div", {class: "bx-settings-tab-content",_nearby: {orientation: "vertical",focus: () => this.jumpToSettingGroup("next")}})));this.$container = $container, this.$tabs = $tabs, this.$tabContents = $tabContents, $container.addEventListener("click", (e) => {if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide();});let settingTabGroup;for (settingTabGroup in this.SETTINGS_UI) {let settingTab = this.SETTINGS_UI[settingTabGroup];if (!settingTab) continue;if (!this.isSupportedVariant(settingTab.requiredVariants)) continue;if (settingTab.group !== "global" && !this.renderFullSettings) continue;let $svg = this.renderTab(settingTab);$tabs.appendChild($svg);let $tabContent = this.renderSettingsSection.call(this, settingTab, settingTab.items);$tabContents.appendChild($tabContent);}$tabs.firstElementChild.dispatchEvent(new Event("click"));}focusTab(tabId) {let $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`);$tab && $tab.dispatchEvent(new Event("click"));}focusIfNeeded() {this.jumpToSettingGroup("next");}focusActiveTab() {let $currentTab = this.$tabs.querySelector(".bx-active");return $currentTab && $currentTab.focus(), !0;}focusVisibleSetting(type = "first") {let controls = Array.from(this.$tabContents.querySelectorAll("div[data-tab-group]:not(.bx-gone) > *"));if (!controls.length) return !1;if (type === "last") controls.reverse();for (let $control of controls) {if (!($control instanceof HTMLElement)) continue;let $focusable = this.dialogManager.findFocusableElement($control);if ($focusable) {if (this.dialogManager.focus($focusable)) return !0;}}return !1;}focusVisibleTab(type = "first") {let tabs = Array.from(this.$tabs.querySelectorAll("svg:not(.bx-gone)"));if (!tabs.length) return !1;if (type === "last") tabs.reverse();for (let $tab of tabs)if (this.dialogManager.focus($tab)) return !0;return !1;}jumpToSettingGroup(direction) {let $tabContent = this.$tabContents.querySelector("div[data-tab-group]:not(.bx-gone)");if (!$tabContent) return !1;let $header, $focusing = document.activeElement;if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2");else {let $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling", $tmp = $parent, times = 0;while (!0) {if (!$tmp) break;if ($tmp.tagName === "H2") {if ($header = $tmp, !$tmp.nextElementSibling?.classList.contains("bx-note-unsupported")) {if (++times, direction === "next" || times >= 2) break;}}$tmp = $tmp[siblingProperty];}}let $target;if ($header) $target = this.dialogManager.findNextTarget($header, 3, !1);if ($target) return this.dialogManager.focus($target);return !1;}resetHighlightedSetting($elm) {let targetGameId = SettingsManager.getInstance().getTargetGameId();if (targetGameId < 0) return;if (!$elm) $elm = document.activeElement instanceof HTMLElement ? document.activeElement : void 0;let $row = $elm?.closest("div[data-tab-group] > .bx-settings-row");if (!$row) return;let pref = $row.prefKey;if (!pref) alert("Pref not found: " + $row.id);if (!isStreamPref(pref)) return;let deleted = STORAGE.Stream.deleteSettingByGame(targetGameId, pref);if (deleted) BxEventBus.Stream.emit("setting.changed", {storageKey: `${"BetterXcloud.Stream"}.${targetGameId}`,settingKey: pref});return deleted;}handleKeyPress(key) {let handled = !0;switch (key) {case "Tab":this.focusActiveTab();break;case "Home":this.focusVisibleSetting("first");break;case "End":this.focusVisibleSetting("last");break;case "PageUp":this.jumpToSettingGroup("previous");break;case "PageDown":this.jumpToSettingGroup("next");break;case "KeyQ":this.resetHighlightedSetting();break;default:handled = !1;break;}return handled;}handleGamepad(button) {let handled = !0;switch (button) {case 1:let $focusing = document.activeElement;if ($focusing && this.$tabs.contains($focusing)) this.hide();else this.focusActiveTab();break;case 4:case 5:this.focusActiveTab();break;case 6:this.jumpToSettingGroup("previous");break;case 7:this.jumpToSettingGroup("next");break;case 2:this.resetHighlightedSetting();break;default:handled = !1;break;}return handled;}} +class SettingsDialog extends NavigationDialog {static instance;static getInstance = () => SettingsDialog.instance ?? (SettingsDialog.instance = new SettingsDialog);LOG_TAG = "SettingsNavigationDialog";$container;$tabs;$tabContents;$btnReload;$btnGlobalReload;$noteGlobalReload;$btnSuggestion;$streamSettingsSelection;renderFullSettings;boundOnContextMenu;suggestedSettings = {recommended: {},default: {},lowest: {},highest: {}};settingLabels = {};settingsManager;TAB_GLOBAL_ITEMS = [{group: "general",label: t("better-xcloud"),helpUrl: "https://better-xcloud.github.io/features/",items: [($parent) => {let PREF_LATEST_VERSION = getGlobalPref("version.latest"), topButtons = [];if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {let opts = {label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }),style: 1 | 64 | 128};if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript();else opts.url = "https://github.com/redphx/better-xcloud/releases/latest";topButtons.push(createButton(opts));}if (AppInterface) topButtons.push(createButton({label: t("app-settings"),icon: BxIcon.STREAM_SETTINGS,style: 128 | 64,onClick: (e) => {AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide();}}));else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({label: "🔥 " + t("install-android"),style: 128 | 64,url: "https://better-xcloud.github.io/android"}));this.$btnGlobalReload = createButton({label: t("settings-reload"),classes: ["bx-settings-reload-button", "bx-gone"],style: 64 | 128,onClick: (e) => {this.reloadPage();}}), topButtons.push(this.$btnGlobalReload), this.$noteGlobalReload = CE("span", {class: "bx-settings-reload-note"}, t("settings-reload-note")), topButtons.push(this.$noteGlobalReload), this.$btnSuggestion = CE("div", {class: "bx-suggest-toggler bx-focusable",tabindex: 0}, CE("label", !1, t("suggest-settings")), CE("span", !1, "❯")), this.$btnSuggestion.addEventListener("click", SuggestionsSetting.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion);let $div = CE("div", {class: "bx-top-buttons",_nearby: {orientation: "vertical"}}, ...topButtons);$parent.appendChild($div);},{pref: "bx.locale",multiLines: !0},"server.bypassRestriction","ui.controllerFriendly"]}, {group: "server",label: t("server"),items: [{pref: "server.region",multiLines: !0},{pref: "stream.locale",multiLines: !0},"server.ipv6.prefer"]}, {group: "stream",label: t("stream"),items: ["stream.video.resolution","stream.video.codecProfile","stream.video.maxBitrate","stream.video.preventResolutionDrops","audio.volume.booster.enabled","screenshot.applyFilters","audio.mic.onPlaying","game.fortnite.forceConsole","stream.video.combineAudio"]}, {requiredVariants: "full",group: "mkb",label: t("mouse-and-keyboard"),items: ["nativeMkb.mode",{pref: "nativeMkb.forcedGames",multiLines: !0,note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/574", target: "_blank" }, t("unofficial-game-list"))},"mkb.enabled","mkb.cursor.hideIdle"],...!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? {unsupported: !0,unsupportedNote: CE("a", {href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657",target: "_blank"}, "⚠️ " + t("browser-unsupported-feature"))} : {}}, {requiredVariants: "full",group: "touch-control",label: t("touch-controller"),items: [{pref: "touchController.mode",note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/241", target: "_blank" }, t("unofficial-game-list"))},"touchController.autoOff","touchController.opacity.default","touchController.style.standard","touchController.style.custom"],...!STATES.userAgent.capabilities.touch ? {unsupported: !0,unsupportedNote: "⚠️ " + t("device-unsupported-touch")} : {}}, {group: "ui",label: t("ui"),items: ["ui.layout","ui.theme","ui.imageQuality","ui.gameCard.waitTime.show","ui.controllerStatus.show","ui.streamMenu.simplify","ui.splashVideo.skip",!AppInterface && "ui.hideScrollbar","ui.systemMenu.hideHandle","ui.feedbackDialog.disabled","ui.reduceAnimations",{pref: "ui.hideSections",multiLines: !0},{pref: "block.features",multiLines: !0}]}, {requiredVariants: "full",group: "game-bar",label: t("game-bar"),items: ["gameBar.position"]}, {group: "loading-screen",label: t("loading-screen"),items: ["loadingScreen.gameArt.show","loadingScreen.waitTime.show","loadingScreen.rocket"]}, {group: "other",label: t("other"),items: ["block.tracking"]}, {group: "advanced",label: t("advanced"),items: [{pref: "userAgent.profile",multiLines: !0,onCreated: (setting, $control) => {let defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", {type: "text",placeholder: defaultUserAgent,autocomplete: "off",class: "bx-settings-custom-user-agent",tabindex: 0});$inpCustomUserAgent.addEventListener("input", (e) => {let profile = $control.value, custom = e.target.value.trim();UserAgent.updateStorage(profile, custom), this.onGlobalSettingChanged(e);}), $control.insertAdjacentElement("afterend", $inpCustomUserAgent), setNearby($inpCustomUserAgent.parentElement, {orientation: "vertical"});}}]}, {group: "footer",items: [($parent) => {try {let appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10);$parent.appendChild(CE("div", {class: "bx-settings-app-version"}, `xCloud website version ${appVersion} (${appDate})`));} catch (e) {}},($parent) => {$parent.appendChild(CE("a", {class: "bx-donation-link",href: "https://ko-fi.com/redphx",target: "_blank",tabindex: 0}, `❤️ ${t("support-better-xcloud")}`));},($parent) => {$parent.appendChild(createButton({label: t("clear-data"),style: 4 | 16 | 128 | 64,onClick: (e) => {if (confirm(t("clear-data-confirm"))) clearAllData();}}));},($parent) => {$parent.appendChild(CE("div", { class: "bx-debug-info" }, createButton({label: "Debug info",style: 8 | 128 | 64,onClick: (e) => {let $button = e.target.closest("button");if (!$button) return;let $pre = $button.nextElementSibling;if (!$pre) {let debugInfo = deepClone(BX_FLAGS.DeviceInfo);debugInfo.settings = JSON.parse(window.localStorage.getItem("BetterXcloud") || "{}"), $pre = CE("pre", {class: "bx-focusable bx-gone",tabindex: 0,_on: {click: async (e2) => {await copyToClipboard(e2.target.innerText);}}}, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```"), $button.insertAdjacentElement("afterend", $pre);}$pre.classList.toggle("bx-gone"), $pre.scrollIntoView();}})));}]}];TAB_DISPLAY_ITEMS = [{requiredVariants: "full",group: "audio",label: t("audio"),helpUrl: "https://better-xcloud.github.io/ingame-features/#audio",items: [{pref: "audio.volume",params: {disabled: !getGlobalPref("audio.volume.booster.enabled")},onCreated: (setting, $elm) => {let $range = $elm.querySelector("input[type=range");BxEventBus.Stream.on("setting.changed", (payload) => {let { settingKey } = payload;if (settingKey === "audio.volume") $range.value = getStreamPref(settingKey).toString(), BxEvent.dispatch($range, "input", { ignoreOnChange: !0 });});}}]}, {group: "video",label: t("video"),helpUrl: "https://better-xcloud.github.io/ingame-features/#video",items: ["video.player.type","video.maxFps","video.player.powerPreference","video.processing","video.processing.mode","video.ratio","video.position","video.processing.sharpness","video.saturation","video.contrast","video.brightness"]}];TAB_CONTROLLER_ITEMS = [{group: "controller",label: t("controller"),helpUrl: "https://better-xcloud.github.io/ingame-features/#controller",items: ["localCoOp.enabled","controller.pollingRate",($parent) => {$parent.appendChild(ControllerExtraSettings.renderSettings.apply(this));}]},STATES.userAgent.capabilities.touch && {group: "touch-control",label: t("touch-controller"),items: [{label: t("layout"),content: CE("select", {disabled: !0}, CE("option", !1, t("default"))),onCreated: (setting, $elm) => {$elm.addEventListener("input", (e) => {TouchController.applyCustomLayout($elm.value, 1000);}), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => {let customLayouts = TouchController.getCustomLayouts();while ($elm.firstChild)$elm.removeChild($elm.firstChild);if ($elm.disabled = !customLayouts, !customLayouts) {$elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("input"));return;}let $fragment = document.createDocumentFragment();for (let key in customLayouts.layouts) {let layout = customLayouts.layouts[key], name;if (layout.author) name = `${layout.name} (${layout.author})`;else name = layout.name;let $option = CE("option", { value: key }, name);$fragment.appendChild($option);}$elm.appendChild($fragment), $elm.value = customLayouts.default_layout;});}}]},STATES.browser.capabilities.deviceVibration && {group: "device",label: t("device"),items: [{pref: "deviceVibration.mode",multiLines: !0,unsupported: !STATES.browser.capabilities.deviceVibration}, {pref: "deviceVibration.intensity",unsupported: !STATES.browser.capabilities.deviceVibration}]}];TAB_MKB_ITEMS = [{requiredVariants: "full",group: "mkb",label: t("mouse-and-keyboard"),helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/",items: [($parent) => {$parent.appendChild(MkbExtraSettings.renderSettings.apply(this));}]},NativeMkbHandler.isAllowed() && {requiredVariants: "full",group: "native-mkb",label: t("native-mkb"),items: ["nativeMkb.scroll.sensitivityY","nativeMkb.scroll.sensitivityX"]}];TAB_STATS_ITEMS = [{group: "stats",label: t("stream-stats"),helpUrl: "https://better-xcloud.github.io/stream-stats/",items: ["stats.showWhenPlaying","stats.quickGlance.enabled","stats.items","stats.position","stats.textSize","stats.opacity.all","stats.opacity.background","stats.colors"]}];SETTINGS_UI = {global: {group: "global",icon: BxIcon.HOME,items: this.TAB_GLOBAL_ITEMS},stream: {group: "stream",icon: BxIcon.DISPLAY,items: this.TAB_DISPLAY_ITEMS},controller: {group: "controller",icon: BxIcon.CONTROLLER,items: this.TAB_CONTROLLER_ITEMS,requiredVariants: "full"},mkb: {group: "mkb",icon: BxIcon.NATIVE_MKB,items: this.TAB_MKB_ITEMS,requiredVariants: "full"},stats: {group: "stats",icon: BxIcon.STREAM_STATS,items: this.TAB_STATS_ITEMS}};constructor() {super();BxLogger.info(this.LOG_TAG, "constructor()"), this.boundOnContextMenu = this.onContextMenu.bind(this), this.settingsManager = SettingsManager.getInstance(), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(), this.onMountedCallbacks.push(() => {if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);let $selectUserAgent = document.querySelector(`#bx_setting_${escapeCssSelector("userAgent.profile")}`);if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1;}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => {this.$tabContents.dataset.gameId = id.toString();});}getDialog() {return this;}getContent() {return this.$container;}onMounted() {super.onMounted();}isOverlayVisible() {return !STATES.isPlaying;}reloadPage() {this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload();}isSupportedVariant(requiredVariants) {if (typeof requiredVariants === "undefined") return !0;return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT);}onTabClicked = (e) => {let $svg = e.target.closest("svg"), $child, children = Array.from(this.$tabContents.children);for ($child of children)if ($child.dataset.tabGroup === $svg.dataset.group) $child.classList.remove("bx-gone"), calculateSelectBoxes($child);else if ($child.dataset.tabGroup) $child.classList.add("bx-gone");this.$streamSettingsSelection.classList.toggle("bx-gone", $svg.dataset.group === "global");for (let $child2 of Array.from(this.$tabs.children))$child2.classList.remove("bx-active");$svg.classList.add("bx-active");};renderTab(settingTab) {let $svg = createSvgIcon(settingTab.icon);return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", this.onTabClicked), $svg;}onGlobalSettingChanged = (e) => {PatcherCache.getInstance().clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger");};onContextMenu(e) {e.preventDefault();let $elm = e.target;$elm instanceof HTMLElement && this.resetHighlightedSetting($elm);}renderServerSetting(setting) {let selectedValue = getGlobalPref("server.region"), continents = {"america-north": {label: t("continent-north-america")},"america-south": {label: t("continent-south-america")},asia: {label: t("continent-asia")},australia: {label: t("continent-australia")},europe: {label: t("continent-europe")},other: {label: t("other")}}, $control = CE("select", {id: `bx_setting_${escapeCssSelector(setting.pref)}`,tabindex: 0});$control.name = $control.id, $control.addEventListener("input", (e) => {setGlobalPref(setting.pref, e.target.value, "ui"), this.onGlobalSettingChanged(e);}), setting.options = {};for (let regionName in STATES.serverRegions) {let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`;if (region.isDefault) {if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default";}setting.options[value] = label;let $option = CE("option", { value }, label), continent = continents[region.contintent];if (!continent.children) continent.children = [];continent.children.push($option);}let fragment = document.createDocumentFragment(), key;for (key in continents) {let continent = continents[key];if (!continent.children) continue;fragment.appendChild(CE("optgroup", {label: continent.label}, ...continent.children));}return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;}renderSettingRow(settingTab, $tabContent, settingTabContent, setting) {if (typeof setting === "string") setting = {pref: setting};let pref = setting.pref, $control;if (setting.content) if (typeof setting.content === "function") $control = setting.content.apply(this);else $control = setting.content;else if (!setting.unsupported) {if (pref === "server.region") $control = this.renderServerSetting(setting);else if (pref === "bx.locale") $control = SettingElement.fromPref(pref, async (e) => {let newLocale = e.target.value;if (getGlobalPref("ui.controllerFriendly")) {let timeoutId = e.target.timeoutId;timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => {Translations.refreshLocale(newLocale), Translations.updateTranslations();}, 500);} else Translations.refreshLocale(newLocale), Translations.updateTranslations();this.onGlobalSettingChanged(e);});else if (pref === "userAgent.profile") $control = SettingElement.fromPref("userAgent.profile", (e) => {let value = e.target.value, isCustom = value === "custom", userAgent2 = UserAgent.get(value);UserAgent.updateStorage(value);let $inp = $control.nextElementSibling;$inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e);});else if ($control = this.settingsManager.getElement(pref, setting.params), settingTab.group === "global") $control.addEventListener("input", this.onGlobalSettingChanged);if ($control instanceof HTMLSelectElement) $control = BxSelectElement.create($control);}let prefDefinition = null;if (pref) prefDefinition = getPrefInfo(pref).definition;if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return;let label = prefDefinition?.label || setting.label || "", note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental;if (typeof note === "function") note = note();if (typeof unsupportedNote === "function") unsupportedNote = unsupportedNote();if (settingTabContent.label && setting.pref) {if (prefDefinition?.suggest) typeof prefDefinition.suggest.lowest !== "undefined" && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest), typeof prefDefinition.suggest.highest !== "undefined" && (this.suggestedSettings.highest[setting.pref] = prefDefinition.suggest.highest);}if (experimental) if (label = "🧪 " + label, !note) note = t("experimental");else note = `${t("experimental")}: ${note}`;let $note;if (unsupportedNote) $note = CE("div", { class: "bx-settings-dialog-note" }, unsupportedNote);else if (note) $note = CE("div", { class: "bx-settings-dialog-note" }, note);let $row = createSettingRow(label, !prefDefinition?.unsupported && $control, {$note,multiLines: setting.multiLines,icon: prefDefinition?.labelIcon,onContextMenu: this.boundOnContextMenu,pref});if (pref) $row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`;if ($row.dataset.type = settingTabContent.group, !STATES.supportedRegion && setting.pref === "server.bypassRestriction") $row.classList.add("bx-settings-important-row");$tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control);}renderSettingsSection(settingTab, sections) {let $tabContent = CE("div", {class: "bx-gone",_dataset: {tabGroup: settingTab.group}});for (let section of sections) {if (!section) continue;if (section instanceof HTMLElement) {$tabContent.appendChild(section);continue;}if (!this.isSupportedVariant(section.requiredVariants)) continue;if (!this.renderFullSettings && settingTab.group === "global" && section.group !== "general" && section.group !== "footer") continue;let label = section.label;if (label === t("better-xcloud")) {if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)";label = createButton({label,url: "https://github.com/redphx/better-xcloud/releases",style: 4096 | 16 | 64});}if (label) {let $title = CE("h2", {_nearby: {orientation: "horizontal"}}, CE("span", !1, label), section.helpUrl && createButton({icon: BxIcon.QUESTION,style: 8 | 64,url: section.helpUrl,title: t("help")}));$tabContent.appendChild($title);}if (section.unsupportedNote) {let $note = CE("b", { class: "bx-note-unsupported" }, section.unsupportedNote);$tabContent.appendChild($note);}if (section.unsupported) continue;if (section.content) {$tabContent.appendChild(section.content);continue;}section.items = section.items || [];for (let setting of section.items) {if (setting === !1) continue;if (typeof setting === "function") {setting.apply(this, [$tabContent]);continue;}this.renderSettingRow(settingTab, $tabContent, section, setting);}}return $tabContent;}setupDialog() {let $tabs, $tabContents, $container = CE("div", {class: "bx-settings-dialog",_nearby: {orientation: "horizontal"}}, CE("div", {class: "bx-settings-tabs-container",_nearby: {orientation: "vertical",focus: () => {return this.dialogManager.focus($tabs);},loop: (direction) => {if (direction === 1 || direction === 3) return this.focusVisibleTab(direction === 1 ? "last" : "first"), !0;return !1;}}}, $tabs = CE("div", {class: "bx-settings-tabs bx-hide-scroll-bar",_nearby: {focus: () => this.focusActiveTab()}}), CE("div", !1, this.$btnReload = createButton({icon: BxIcon.REFRESH,style: 64 | 32,onClick: (e) => {this.reloadPage();}}), createButton({icon: BxIcon.CLOSE,style: 64 | 32,onClick: (e) => {this.dialogManager.hide();}}))), CE("div", {class: "bx-settings-tab-contents",_nearby: {orientation: "vertical",loop: (direction) => {if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0;return !1;}}}, this.$streamSettingsSelection = SettingsManager.getInstance().getStreamSettingsSelection(), $tabContents = CE("div", {class: "bx-settings-tab-content",_nearby: {orientation: "vertical",focus: () => this.jumpToSettingGroup("next")}})));this.$container = $container, this.$tabs = $tabs, this.$tabContents = $tabContents, $container.addEventListener("click", (e) => {if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide();});let settingTabGroup;for (settingTabGroup in this.SETTINGS_UI) {let settingTab = this.SETTINGS_UI[settingTabGroup];if (!settingTab) continue;if (!this.isSupportedVariant(settingTab.requiredVariants)) continue;if (settingTab.group !== "global" && !this.renderFullSettings) continue;let $svg = this.renderTab(settingTab);$tabs.appendChild($svg);let $tabContent = this.renderSettingsSection.call(this, settingTab, settingTab.items);$tabContents.appendChild($tabContent);}$tabs.firstElementChild.dispatchEvent(new Event("click"));}focusTab(tabId) {let $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`);$tab && $tab.dispatchEvent(new Event("click"));}focusIfNeeded() {this.jumpToSettingGroup("next");}focusActiveTab() {let $currentTab = this.$tabs.querySelector(".bx-active");return $currentTab && $currentTab.focus(), !0;}focusVisibleSetting(type = "first") {let controls = Array.from(this.$tabContents.querySelectorAll("div[data-tab-group]:not(.bx-gone) > *"));if (!controls.length) return !1;if (type === "last") controls.reverse();for (let $control of controls) {if (!($control instanceof HTMLElement)) continue;let $focusable = this.dialogManager.findFocusableElement($control);if ($focusable) {if (this.dialogManager.focus($focusable)) return !0;}}return !1;}focusVisibleTab(type = "first") {let tabs = Array.from(this.$tabs.querySelectorAll("svg:not(.bx-gone)"));if (!tabs.length) return !1;if (type === "last") tabs.reverse();for (let $tab of tabs)if (this.dialogManager.focus($tab)) return !0;return !1;}jumpToSettingGroup(direction) {let $tabContent = this.$tabContents.querySelector("div[data-tab-group]:not(.bx-gone)");if (!$tabContent) return !1;let $header, $focusing = document.activeElement;if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2");else {let $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling", $tmp = $parent, times = 0;while (!0) {if (!$tmp) break;if ($tmp.tagName === "H2") {if ($header = $tmp, !$tmp.nextElementSibling?.classList.contains("bx-note-unsupported")) {if (++times, direction === "next" || times >= 2) break;}}$tmp = $tmp[siblingProperty];}}let $target;if ($header) $target = this.dialogManager.findNextTarget($header, 3, !1);if ($target) return this.dialogManager.focus($target);return !1;}resetHighlightedSetting($elm) {let targetGameId = SettingsManager.getInstance().getTargetGameId();if (targetGameId < 0) return;if (!$elm) $elm = document.activeElement instanceof HTMLElement ? document.activeElement : void 0;let $row = $elm?.closest("div[data-tab-group] > .bx-settings-row");if (!$row) return;let pref = $row.prefKey;if (!pref) alert("Pref not found: " + $row.id);if (!isStreamPref(pref)) return;let deleted = STORAGE.Stream.deleteSettingByGame(targetGameId, pref);if (deleted) BxEventBus.Stream.emit("setting.changed", {storageKey: `${"BetterXcloud.Stream"}.${targetGameId}`,settingKey: pref});return deleted;}handleKeyPress(key) {let handled = !0;switch (key) {case "Tab":this.focusActiveTab();break;case "Home":this.focusVisibleSetting("first");break;case "End":this.focusVisibleSetting("last");break;case "PageUp":this.jumpToSettingGroup("previous");break;case "PageDown":this.jumpToSettingGroup("next");break;case "KeyQ":this.resetHighlightedSetting();break;default:handled = !1;break;}return handled;}handleGamepad(button) {let handled = !0;switch (button) {case 1:let $focusing = document.activeElement;if ($focusing && this.$tabs.contains($focusing)) this.hide();else this.focusActiveTab();break;case 4:case 5:this.focusActiveTab();break;case 6:this.jumpToSettingGroup("previous");break;case 7:this.jumpToSettingGroup("next");break;case 2:this.resetHighlightedSetting();break;default:handled = !1;break;}return handled;}} class ScreenshotManager {static instance;static getInstance = () => ScreenshotManager.instance ?? (ScreenshotManager.instance = new ScreenshotManager);LOG_TAG = "ScreenshotManager";$download;$canvas;canvasContext;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$download = CE("a"), this.$canvas = CE("canvas", { class: "bx-gone" }), this.canvasContext = this.$canvas.getContext("2d", {alpha: !1,willReadFrequently: !1});}updateCanvasSize(width, height) {this.$canvas.width = width, this.$canvas.height = height;}updateCanvasFilters(filters) {this.canvasContext.filter = filters;}onAnimationEnd(e) {e.target.classList.remove("bx-taking-screenshot");}takeScreenshot(callback) {let currentStream = STATES.currentStream, streamPlayerManager = currentStream.streamPlayerManager, $canvas = this.$canvas;if (!streamPlayerManager || !$canvas) return;let $player;if (getGlobalPref("screenshot.applyFilters")) $player = streamPlayerManager.getPlayerElement();else $player = streamPlayerManager.getPlayerElement("video");if (!$player || !$player.isConnected) return;let canvasContext = this.canvasContext;if ($player instanceof HTMLCanvasElement) streamPlayerManager.getCanvasPlayer()?.updateFrame();canvasContext.drawImage($player, 0, 0);let $gameStream = $player.closest("#game-stream");if ($gameStream) $gameStream.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $gameStream.classList.add("bx-taking-screenshot");if (AppInterface) {let data = $canvas.toDataURL("image/png").split(";base64,")[1];AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();return;}$canvas.toBlob((blob) => {if (!blob) return;let now = +new Date, $download = this.$download;$download.download = `${currentStream.titleSlug}-${now}.png`, $download.href = URL.createObjectURL(blob), $download.click(), URL.revokeObjectURL($download.href), $download.href = "", $download.download = "", canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();}, "image/png");}} class RendererShortcut {static toggleVisibility() {let $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]');if (!$mediaContainer) {BxEventBus.Stream.emit("video.visibility.changed", { isVisible: !0 });return;}$mediaContainer.classList.toggle("bx-gone");let isVisible = !$mediaContainer.classList.contains("bx-gone");limitVideoPlayerFps(isVisible ? getStreamPref("video.maxFps") : 0), BxEventBus.Stream.emit("video.visibility.changed", { isVisible });}} class TrueAchievements {static instance;static getInstance = () => TrueAchievements.instance ?? (TrueAchievements.instance = new TrueAchievements);LOG_TAG = "TrueAchievements";$link;$button;$hiddenLink;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$link = createButton({label: t("true-achievements"),url: "#",icon: BxIcon.TRUE_ACHIEVEMENTS,style: 64 | 8 | 128 | 8192,onClick: this.onClick}), this.$button = createButton({label: t("true-achievements"),title: t("true-achievements"),icon: BxIcon.TRUE_ACHIEVEMENTS,style: 64,onClick: this.onClick}), this.$hiddenLink = CE("a", {target: "_blank"});}onClick = (e) => {e.preventDefault(), window.BX_EXPOSED.dialogRoutes?.closeAll();let dataset = this.$link.dataset;this.open(!0, dataset.xboxTitleId, dataset.id);};updateIds(xboxTitleId, id) {let $link = this.$link, $button = this.$button;if (clearDataSet($link), clearDataSet($button), xboxTitleId) $link.dataset.xboxTitleId = xboxTitleId, $button.dataset.xboxTitleId = xboxTitleId;if (id) $link.dataset.id = id, $button.dataset.id = id;}injectAchievementsProgress($elm) {if (SCRIPT_VARIANT !== "full") return;let $parent = $elm.parentElement, $div = CE("div", {class: "bx-guide-home-achievements-progress"}, $elm), xboxTitleId;try {let $container = $parent.closest("div[class*=AchievementsPreview-module__container]");if ($container) xboxTitleId = getReactProps($container).children.props.data.data.xboxTitleId;} catch (e) {}if (!xboxTitleId) xboxTitleId = this.getStreamXboxTitleId();if (typeof xboxTitleId !== "undefined") xboxTitleId = xboxTitleId.toString();if (this.updateIds(xboxTitleId), document.body.dataset.mediaType === "tv") $div.appendChild(this.$link);else $div.appendChild(this.$button);$parent.appendChild($div);}injectAchievementDetailPage($parent) {if (SCRIPT_VARIANT !== "full") return;let props = getReactProps($parent);if (!props) return;try {let achievementList = props.children.props.data.data, $header = $parent.querySelector("div[class*=AchievementDetailHeader]"), achievementName = getReactProps($header).children[0].props.achievementName, id, xboxTitleId;for (let achiev of achievementList)if (achiev.name === achievementName) {id = achiev.id, xboxTitleId = achiev.title.id;break;}if (id) this.updateIds(xboxTitleId, id), $parent.appendChild(this.$link);} catch (e) {}}getStreamXboxTitleId() {return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId;}open(override, xboxTitleId, id) {if (!xboxTitleId || xboxTitleId === "undefined") xboxTitleId = this.getStreamXboxTitleId();if (AppInterface?.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();}} diff --git a/src/enums/pref-keys.ts b/src/enums/pref-keys.ts index 2fae409..54872ea 100755 --- a/src/enums/pref-keys.ts +++ b/src/enums/pref-keys.ts @@ -34,6 +34,7 @@ export const enum GlobalPref { STREAM_CODEC_PROFILE = 'stream.video.codecProfile', STREAM_MAX_VIDEO_BITRATE = 'stream.video.maxBitrate', STREAM_COMBINE_SOURCES = 'stream.video.combineAudio', + STREAM_PREVENT_RESOLUTION_DROPS = 'stream.video.preventResolutionDrops', USER_AGENT_PROFILE = 'userAgent.profile', @@ -110,6 +111,7 @@ export type GlobalPrefTypeMap = { [GlobalPref.STREAM_MAX_VIDEO_BITRATE]: number; [GlobalPref.STREAM_PREFERRED_LOCALE]: StreamPreferredLocale; [GlobalPref.STREAM_RESOLUTION]: StreamResolution; + [GlobalPref.STREAM_PREVENT_RESOLUTION_DROPS]: boolean; [GlobalPref.TOUCH_CONTROLLER_AUTO_OFF]: boolean; [GlobalPref.TOUCH_CONTROLLER_DEFAULT_OPACITY]: number; [GlobalPref.TOUCH_CONTROLLER_MODE]: TouchControllerMode; @@ -245,6 +247,7 @@ export const ALL_PREFS: { GlobalPref.STREAM_MAX_VIDEO_BITRATE, GlobalPref.STREAM_PREFERRED_LOCALE, GlobalPref.STREAM_RESOLUTION, + GlobalPref.STREAM_PREVENT_RESOLUTION_DROPS, GlobalPref.TOUCH_CONTROLLER_AUTO_OFF, GlobalPref.TOUCH_CONTROLLER_DEFAULT_OPACITY, GlobalPref.TOUCH_CONTROLLER_MODE, diff --git a/src/modules/patcher/patcher.ts b/src/modules/patcher/patcher.ts index 206f90b..49facf4 100755 --- a/src/modules/patcher/patcher.ts +++ b/src/modules/patcher/patcher.ts @@ -1248,16 +1248,18 @@ ${subsVar} = subs; return false; } - const fakeDtMs = 1; + const maxDt = 10; const code = ` try { const obj = arguments[0]; - const baseMs = obj.frameSubmittedTimeMs; - const renderMs = obj.frameRenderedTimeMs - obj.frameDecodedTimeMs; - obj.frameDecodedTimeMs = baseMs + ${fakeDtMs}; - obj.frameRenderedTimeMs = obj.frameDecodedTimeMs + renderMs; - obj.expectedDisplayTime = obj.frameRenderedTimeMs; - arguments[0] = obj; + if (true || obj.frameDecodedTimeMs - obj.frameSubmittedTimeMs > ${maxDt}) { + const baseMs = obj.frameSubmittedTimeMs; + const renderMs = obj.frameRenderedTimeMs - obj.frameDecodedTimeMs; + obj.frameDecodedTimeMs = baseMs + ${maxDt}; + obj.frameRenderedTimeMs = obj.frameDecodedTimeMs + renderMs; + obj.expectedDisplayTime = obj.frameRenderedTimeMs; + arguments[0] = obj; + } } catch (e) { alert(e) } `; str = PatcherUtils.insertAt(str, index, code); @@ -1409,7 +1411,7 @@ let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ 'injectStreamMenuUseEffect', - 'patchStreamMetadata', + getGlobalPref(GlobalPref.STREAM_PREVENT_RESOLUTION_DROPS) && 'patchStreamMetadata', // 'exposeEventTarget', diff --git a/src/modules/ui/dialog/settings-dialog.ts b/src/modules/ui/dialog/settings-dialog.ts index 593d10c..344b1dd 100755 --- a/src/modules/ui/dialog/settings-dialog.ts +++ b/src/modules/ui/dialog/settings-dialog.ts @@ -213,6 +213,7 @@ export class SettingsDialog extends NavigationDialog { GlobalPref.STREAM_RESOLUTION, GlobalPref.STREAM_CODEC_PROFILE, GlobalPref.STREAM_MAX_VIDEO_BITRATE, + GlobalPref.STREAM_PREVENT_RESOLUTION_DROPS, GlobalPref.AUDIO_VOLUME_CONTROL_ENABLED, diff --git a/src/utils/settings-storages/global-settings-storage.ts b/src/utils/settings-storages/global-settings-storage.ts index f14c281..09ad55b 100755 --- a/src/utils/settings-storages/global-settings-storage.ts +++ b/src/utils/settings-storages/global-settings-storage.ts @@ -151,6 +151,11 @@ export class GlobalSettingsStorage extends BaseSettingsStorage { highest: StreamResolution.DIM_1080P_HQ, }, }, + [GlobalPref.STREAM_PREVENT_RESOLUTION_DROPS]: { + label: t('prevent-resolution-drops'), + default: false, + note: CE('a', { href: 'https://github.com/redphx/better-xcloud/issues/791', target: '_blank' }, '⚠️ ' + t('unexpected-behavior')), + }, [GlobalPref.STREAM_CODEC_PROFILE]: { label: t('visual-quality'), diff --git a/src/utils/translation.ts b/src/utils/translation.ts index 11af6c4..f070ac8 100755 --- a/src/utils/translation.ts +++ b/src/utils/translation.ts @@ -265,6 +265,7 @@ const Texts = { (e: any) => `按下 ${e.key} 來啟用此功能`, ], "press-to-bind": "Press a key or do a mouse click to bind...", + "prevent-resolution-drops": "Prevent resolution drops", "prompt-preset-name": "Preset's name:", "quality": "Quality", "recommended": "Recommended",