diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index 2d9cb9f..780d9ff 100755 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Better xCloud // @namespace https://github.com/redphx -// @version 6.3.2-beta +// @version 6.4.0-beta // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -25,7 +25,7 @@ var ALL_PREFS = {global: ["audio.mic.onPlaying","audio.volume.booster.enabled"," var SMART_TV_UNIQUE_ID = "FC4A1DA2-711C-4E9C-BC7F-047AF8A672EA", CHROMIUM_VERSION = "125.0.0.0"; if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) {let match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/);if (match) CHROMIUM_VERSION = match[1];} class UserAgent {static STORAGE_KEY = "BetterXcloud.UserAgent";static #config;static #isMobile = null;static #isSafari = null;static #isSafariMobile = null;static #USER_AGENTS = {"windows-edge": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,"macos-safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1","smarttv-generic": `${window.navigator.userAgent} Smart-TV`,"smarttv-tizen": `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`,"vr-oculus": window.navigator.userAgent + " OculusBrowser VR"};static init() {if (UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}"), !UserAgent.#config.profile) UserAgent.#config.profile = BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default";if (!UserAgent.#config.custom) UserAgent.#config.custom = "";UserAgent.spoof();}static updateStorage(profile, custom) {let config = UserAgent.#config;if (config.profile = profile, profile === "custom" && typeof custom !== "undefined") config.custom = custom;window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(config));}static getDefault() {return window.navigator.orgUserAgent || window.navigator.userAgent;}static get(profile) {let defaultUserAgent = window.navigator.userAgent;switch (profile) {case "default":return defaultUserAgent;case "custom":return UserAgent.#config.custom || defaultUserAgent;default:return UserAgent.#USER_AGENTS[profile] || defaultUserAgent;}}static isSafari() {if (this.#isSafari !== null) return this.#isSafari;let userAgent = UserAgent.getDefault().toLowerCase(), result = userAgent.includes("safari") && !userAgent.includes("chrom");return this.#isSafari = result, result;}static isSafariMobile() {if (this.#isSafariMobile !== null) return this.#isSafariMobile;let userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile");return this.#isSafariMobile = result, result;}static isMobile() {if (this.#isMobile !== null) return this.#isMobile;let userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent);return this.#isMobile = result, result;}static spoof() {let profile = UserAgent.#config.profile;if (profile === "default") return;let newUserAgent = UserAgent.get(profile);if ("userAgentData" in window.navigator) window.navigator.orgUserAgentData = window.navigator.userAgentData, Object.defineProperty(window.navigator, "userAgentData", {});window.navigator.orgUserAgent = window.navigator.userAgent, Object.defineProperty(window.navigator, "userAgent", {value: newUserAgent});}} -var SCRIPT_VERSION = "6.3.2-beta", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; +var SCRIPT_VERSION = "6.4.0-beta", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; UserAgent.init(); var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = {supportedRegion: !0,serverRegions: {},selectedRegion: {},gsToken: "",isSignedIn: !1,isPlaying: !1,browser: {capabilities: {touch: browserHasTouchSupport,batteryApi: "getBattery" in window.navigator,deviceVibration: !!window.navigator.vibrate,mkb: AppInterface || !UserAgent.getDefault().toLowerCase().match(/(android|iphone|ipad)/),emulatedNativeMkb: !!AppInterface}},userAgent: {isTv,capabilities: {touch: userAgentHasTouchSupport,mkb: AppInterface || !userAgent.match(/(android|iphone|ipad)/)}},currentStream: {},remotePlay: {},pointerServerPort: 9269}; function deepClone(obj) {if (!obj) return {};if ("structuredClone" in window) return structuredClone(obj);return JSON.parse(JSON.stringify(obj));} @@ -36,7 +36,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];AppInterface[method] && AppInterface[method]();} else AppInterface.onEventBus(this.group + "." + event);} catch (e) {console.log(e);}BX_FLAGS.Debug && BxLogger.warning("EventBus", "emit", `${this.group}.${event}`, payload);}} window.BxEventBus = BxEventBus; class GhPagesUtils {static fetchLatestCommit() {NATIVE_FETCH("https://api.github.com/repos/redphx/better-xcloud/branches/gh-pages", {method: "GET",headers: {Accept: "application/vnd.github.v3+json"}}).then((response) => response.json()).then((data) => {let latestCommitHash = data.commit.sha;window.localStorage.setItem("BetterXcloud.GhPages.CommitHash", latestCommitHash);}).catch((error) => {BxLogger.error("GhPagesUtils", "Error fetching the latest commit:", error);});}static getUrl(path) {if (path[0] === "/") alert('`path` must not starts with "/"');let prefix = "https://raw.githubusercontent.com/redphx/better-xcloud", latestCommitHash = window.localStorage.getItem("BetterXcloud.GhPages.CommitHash");if (latestCommitHash) return `${prefix}/${latestCommitHash}/${path}`;else return `${prefix}/refs/heads/gh-pages/${path}`;}static getNativeMkbCustomList(update = !1) {let key = "BetterXcloud.GhPages.ForceNativeMkb";update && NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => {if (json.$schemaVersion === 1) window.localStorage.setItem(key, JSON.stringify(json)), BxEventBus.Script.emit("list.forcedNativeMkb.updated", {data: json});else window.localStorage.removeItem(key);});let info = JSON.parse(window.localStorage.getItem(key) || "{}");if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), {};return info.data;}static getTouchControlCustomList() {let key = "BetterXcloud.GhPages.CustomTouchLayouts";return NATIVE_FETCH(GhPagesUtils.getUrl("touch-layouts/ids.json")).then((response) => response.json()).then((json) => {if (Array.isArray(json)) window.localStorage.setItem(key, JSON.stringify(json));}), JSON.parse(window.localStorage.getItem(key) || "[]");}static getLocalCoOpList() {let key = "BetterXcloud.GhPages.LocalCoOp";NATIVE_FETCH(GhPagesUtils.getUrl("local-co-op/ids.json")).then((response) => response.json()).then((json) => {if (json.$schemaVersion === 1) {window.localStorage.setItem(key, JSON.stringify(json));let ids = new Set(Object.keys(json.data));BxEventBus.Script.emit("list.localCoOp.updated", { ids });} else window.localStorage.removeItem(key), BxEventBus.Script.emit("list.localCoOp.updated", { ids: new Set });});let info = JSON.parse(window.localStorage.getItem(key) || "{}");if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), new Set;return new Set(Object.keys(info.data || {}));}} -var SUPPORTED_LANGUAGES = {"en-US": "English (US)","ca-CA": "Català","da-DK": "dansk","de-DE": "Deutsch","en-ID": "Bahasa Indonesia","es-ES": "español (España)","fr-FR": "français","it-IT": "italiano","ja-JP": "日本語","ko-KR": "한국어","pl-PL": "polski","pt-BR": "português (Brasil)","ru-RU": "русский","th-TH": "ภาษาไทย","tr-TR": "Türkçe","uk-UA": "українська","vi-VN": "Tiếng Việt","zh-CN": "中文(简体)","zh-TW": "中文(繁體)"}, Texts = {achievements: "Achievements",activate: "Activate",activated: "Activated",active: "Active",advanced: "Advanced","all-games": "All games","always-off": "Always off","always-on": "Always on","amd-fidelity-cas": "AMD FidelityFX CAS","app-settings": "App settings",apply: "Apply","aspect-ratio": "Aspect ratio","aspect-ratio-note": "Don't use with native touch games",audio: "Audio",auto: "Auto",availability: "Availability","back-to-home": "Back to home","back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?","background-opacity": "Background opacity",battery: "Battery","battery-saving": "Battery saving","better-xcloud": "Better xCloud","bitrate-audio-maximum": "Maximum audio bitrate","bitrate-video-maximum": "Maximum video bitrate",bottom: "Bottom","bottom-half": "Bottom half","bottom-left": "Bottom-left","bottom-right": "Bottom-right",brazil: "Brazil",brightness: "Brightness","browser-unsupported-feature": "Your browser doesn't support this feature","button-xbox": "Xbox button","bypass-region-restriction": "Bypass region restriction","can-stream-xbox-360-games": "Can stream Xbox 360 games",cancel: "Cancel","cant-stream-xbox-360-games": "Can't stream Xbox 360 games",center: "Center",chat: "Chat","clarity-boost": "Clarity boost","clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",clear: "Clear","clear-data": "Clear data","clear-data-confirm": "Do you want to clear all Better xCloud settings and data?","clear-data-success": "Data cleared! Refresh the page to apply the changes.",clock: "Clock",close: "Close","close-app": "Close app","combine-audio-video-streams": "Combine audio & video streams","combine-audio-video-streams-summary": "May fix the laggy audio problem","conditional-formatting": "Conditional formatting text color","confirm-delete-preset": "Do you want to delete this preset?","confirm-reload-stream": "Do you want to refresh the stream?",connected: "Connected","console-connect": "Connect","continent-asia": "Asia","continent-australia": "Australia","continent-europe": "Europe","continent-north-america": "North America","continent-south-america": "South America",contrast: "Contrast",controller: "Controller","controller-customization": "Controller customization","controller-customization-input-latency-note": "May slightly increase input latency","controller-friendly-ui": "Controller-friendly UI","controller-shortcuts": "Controller shortcuts","controller-shortcuts-connect-note": "Connect a controller to use this feature","controller-shortcuts-xbox-note": "Button to open the Guide menu","controller-vibration": "Controller vibration",copy: "Copy","create-shortcut": "Shortcut",custom: "Custom","deadzone-counterweight": "Deadzone counterweight",decrease: "Decrease",default: "Default","default-opacity": "Default opacity","default-preset-note": "You can't modify default presets. Create a new one to customize it.",delete: "Delete","detect-controller-button": "Detect controller button",device: "Device","device-unsupported-touch": "Your device doesn't have touch support","device-vibration": "Device vibration","device-vibration-not-using-gamepad": "On when not using gamepad",disable: "Disable","disable-features": "Disable features","disable-home-context-menu": "Disable context menu in Home page","disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog","disable-social-features": "Disable social features","disable-xcloud-analytics": "Disable xCloud analytics",disabled: "Disabled",disconnected: "Disconnected",download: "Download",downloaded: "Downloaded",edit: "Edit","enable-controller-shortcuts": "Enable controller shortcuts","enable-local-co-op-support": "Enable local co-op support","enable-local-co-op-support-note": "Only works with some games","enable-mic-on-startup": "Enable microphone on game launch","enable-mkb": "Emulate controller with Mouse & Keyboard","enable-quick-glance-mode": 'Enable "Quick Glance" mode',"enable-remote-play-feature": 'Enable the "Remote Play" feature',"enable-volume-control": "Enable volume control feature",enabled: "Enabled",experimental: "Experimental",export: "Export",fast: "Fast","force-native-mkb-games": "Force native Mouse & Keyboard for these games","fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile',"fortnite-force-console-version": "Fortnite: force console version","friends-followers": "Friends and followers","game-bar": "Game Bar","getting-consoles-list": "Getting the list of consoles...",guide: "Guide",help: "Help",hide: "Hide","hide-idle-cursor": "Hide mouse cursor on idle","hide-scrollbar": "Hide web page's scrollbar","hide-sections": "Hide sections","hide-system-menu-icon": "Hide System menu's icon","hide-touch-controller": "Hide touch controller","high-performance": "High performance","highest-quality": "Highest quality","highest-quality-note": "Your device may not be powerful enough to use these settings","horizontal-scroll-sensitivity": "Horizontal scroll sensitivity","horizontal-sensitivity": "Horizontal sensitivity","how-to-fix": "How to fix","how-to-improve-app-performance": "How to improve app's performance",ignore: "Ignore","image-quality": "Website's image quality",import: "Import","in-game-controller-customization": "In-game controller customization","in-game-controller-shortcuts": "In-game controller shortcuts","in-game-keyboard-shortcuts": "In-game keyboard shortcuts","in-game-shortcuts": "In-game shortcuts",increase: "Increase","install-android": "Better xCloud app for Android",invites: "Invites",japan: "Japan",jitter: "Jitter","keyboard-key": "Keyboard key","keyboard-shortcuts": "Keyboard shortcuts",korea: "Korea",language: "Language",large: "Large",layout: "Layout","left-stick": "Left stick","left-stick-deadzone": "Left stick deadzone","left-trigger-range": "Left trigger range","limit-fps": "Limit FPS","load-failed-message": "Failed to run Better xCloud","loading-screen": "Loading screen","local-co-op": "Local co-op","lowest-quality": "Lowest quality",manage: "Manage","map-mouse-to": "Map mouse to","may-not-work-properly": "May not work properly!",menu: "Menu",microphone: "Microphone","mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings","mkb-click-to-activate": "Click to activate","mkb-disclaimer": "This could be viewed as cheating when playing online","modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.","mouse-and-keyboard": "Mouse & Keyboard","mouse-click": "Mouse click","mouse-wheel": "Mouse wheel",muted: "Muted",name: "Name","native-mkb": "Native Mouse & Keyboard",new: "New","new-version-available": [e => `Version ${e.version} available`,e => `Versió ${e.version} disponible`,,e => `Version ${e.version} verfügbar`,e => `Versi ${e.version} tersedia`,e => `Versión ${e.version} disponible`,e => `Version ${e.version} disponible`,e => `Disponibile la versione ${e.version}`,e => `Ver ${e.version} が利用可能です`,e => `${e.version} 버전 사용가능`,e => `Dostępna jest nowa wersja ${e.version}`,e => `Versão ${e.version} disponível`,e => `Версия ${e.version} доступна`,e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`,e => `${e.version} sayılı yeni sürüm mevcut`,e => `Доступна версія ${e.version}`,e => `Đã có phiên bản ${e.version}`,e => `版本 ${e.version} 可供更新`,e => `已可更新為 ${e.version} 版`],"no-consoles-found": "No consoles found","no-controllers-connected": "No controllers connected",normal: "Normal",notifications: "Notifications",off: "Off",official: "Official",on: "On","only-supports-some-games": "Only supports some games",opacity: "Opacity",other: "Other",playing: "Playing",playtime: "Playtime",poland: "Poland","polling-rate": "Polling rate",position: "Position","powered-off": "Powered off","powered-on": "Powered on","prefer-ipv6-server": "Prefer IPv6 server","preferred-game-language": "Preferred game's language",preset: "Preset",press: "Press","press-any-button": "Press any button...","press-esc-to-cancel": "Press Esc to cancel","press-key-to-toggle-mkb": [e => `Press ${e.key} to toggle this feature`,e => `Premeu ${e.key} per alternar aquesta funció`,e => `Tryk på ${e.key} for at slå denne funktion til`,e => `${e.key}: Funktion an-/ausschalten`,e => `Tekan ${e.key} untuk mengaktifkan fitur ini`,e => `Pulsa ${e.key} para alternar esta función`,e => `Appuyez sur ${e.key} pour activer cette fonctionnalité`,e => `Premi ${e.key} per attivare questa funzionalità`,e => `${e.key} でこの機能を切替`,e => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`,e => `Naciśnij ${e.key} aby przełączyć tę funkcję`,e => `Pressione ${e.key} para alternar este recurso`,e => `Нажмите ${e.key} для переключения этой функции`,e => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`,e => `Etkinleştirmek için ${e.key} tuşuna basın`,e => `Натисніть ${e.key} щоб перемкнути цю функцію`,e => `Nhấn ${e.key} để bật/tắt tính năng này`,e => `按下 ${e.key} 来切换此功能`,e => `按下 ${e.key} 來啟用此功能`],"press-to-bind": "Press a key or do a mouse click to bind...","prompt-preset-name": "Preset's name:",recommended: "Recommended","recommended-settings-for-device": [e => `Recommended settings for ${e.device}`,e => `Configuració recomanada per a ${e.device}`,,e => `Empfohlene Einstellungen für ${e.device}`,e => `Rekomendasi pengaturan untuk ${e.device}`,e => `Ajustes recomendados para ${e.device}`,e => `Paramètres recommandés pour ${e.device}`,e => `Configurazioni consigliate per ${e.device}`,e => `${e.device} の推奨設定`,e => `다음 기기에서 권장되는 설정: ${e.device}`,e => `Zalecane ustawienia dla ${e.device}`,e => `Configurações recomendadas para ${e.device}`,e => `Рекомендуемые настройки для ${e.device}`,e => `การตั้งค่าที่แนะนำสำหรับ ${e.device}`,e => `${e.device} için önerilen ayarlar`,e => `Рекомендовані налаштування для ${e.device}`,e => `Cấu hình được đề xuất cho ${e.device}`,e => `${e.device} 的推荐设置`,e => `${e.device} 推薦的設定`],"reduce-animations": "Reduce UI animations",region: "Region","reload-page": "Reload page","remote-play": "Remote Play",rename: "Rename",renderer: "Renderer","renderer-configuration": "Renderer configuration","reset-highlighted-setting": "Reset highlighted setting","right-click-to-unbind": "Right-click on a key to unbind it","right-stick": "Right stick","right-stick-deadzone": "Right stick deadzone","right-trigger-range": "Right trigger range","rocket-always-hide": "Always hide","rocket-always-show": "Always show","rocket-animation": "Rocket animation","rocket-hide-queue": "Hide when queuing",saturation: "Saturation",save: "Save",screen: "Screen","screenshot-apply-filters": "Apply video filters to screenshots","section-all-games": "All games","section-most-popular": "Most popular","section-native-mkb": "Play with mouse & keyboard","section-news": "News","section-play-with-friends": "Play with friends","section-touch": "Play with touch","separate-touch-controller": "Separate Touch controller & Controller #1","separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",server: "Server","server-locations": "Server locations",settings: "Settings","settings-for": "Settings for","settings-reload": "Reload page to reflect changes","settings-reload-note": "Settings in this tab only go into effect on the next page load","settings-reloading": "Reloading...",sharpness: "Sharpness","shortcut-keys": "Shortcut keys",show: "Show","show-controller-connection-status": "Show controller connection status","show-game-art": "Show game art","show-hide": "Show/hide","show-stats-on-startup": "Show stats when starting the game","show-touch-controller": "Show touch controller","show-wait-time": "Show the estimated wait time","show-wait-time-in-game-card": "Show wait time in game card","simplify-stream-menu": "Simplify Stream's menu","skip-splash-video": "Skip Xbox splash video",slow: "Slow",small: "Small","smart-tv": "Smart TV",sound: "Sound",standard: "Standard",standby: "Standby","stat-bitrate": "Bitrate","stat-decode-time": "Decode time","stat-fps": "FPS","stat-frames-lost": "Frames lost","stat-packets-lost": "Packets lost","stat-ping": "Ping",stats: "Stats","stick-decay-minimum": "Stick decay minimum","stick-decay-strength": "Stick decay strength",stream: "Stream","stream-settings": "Stream settings","stream-stats": "Stream stats","stream-your-own-game": "Stream your own game",stretch: "Stretch","suggest-settings": "Suggest settings","suggest-settings-link": "Suggest recommended settings for this device","support-better-xcloud": "Support Better xCloud","swap-buttons": "Swap buttons","take-screenshot": "Take screenshot","target-resolution": "Target resolution","tc-all-white": "All white","tc-auto-off": "Off when controller found","tc-custom-layout-style": "Custom layout's button style","tc-muted-colors": "Muted colors","tc-standard-layout-style": "Standard layout's button style","text-size": "Text size",toggle: "Toggle",top: "Top","top-center": "Top-center","top-half": "Top half","top-left": "Top-left","top-right": "Top-right","touch-control-layout": "Touch control layout","touch-control-layout-by": [e => `Touch control layout by ${e.name}`,e => `Format del control tàctil per ${e.name}`,e => `Touch-kontrol layout af ${e.name}`,e => `Touch-Steuerungslayout von ${e.name}`,e => `Tata letak Sentuhan layar oleh ${e.name}`,e => `Disposición del control táctil por ${e.nombre}`,e => `Disposition du contrôleur tactile par ${e.name}`,e => `Configurazione dei comandi su schermo creata da ${e.name}`,e => `タッチ操作レイアウト作成者: ${e.name}`,e => `${e.name} 제작, 터치 컨트롤 레이아웃`,e => `Układ sterowania dotykowego stworzony przez ${e.name}`,e => `Disposição de controle por toque feito por ${e.name}`,e => `Сенсорная раскладка по ${e.name}`,e => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`,e => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,e => `Розташування сенсорного керування від ${e.name}`,e => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,e => `由 ${e.name} 提供的虚拟按键样式`,e => `觸控遊玩佈局由 ${e.name} 提供`],"touch-controller": "Touch controller","true-achievements": "TrueAchievements",ui: "UI","unexpected-behavior": "May cause unexpected behavior","united-states": "United States",unknown: "Unknown",unlimited: "Unlimited",unmuted: "Unmuted",unofficial: "Unofficial","unofficial-game-list": "Unofficial game list","unsharp-masking": "Unsharp masking",upload: "Upload",uploaded: "Uploaded","use-mouse-absolute-position": "Use mouse's absolute position","use-this-at-your-own-risk": "Use this at your own risk","user-agent-profile": "User-Agent profile","vertical-scroll-sensitivity": "Vertical scroll sensitivity","vertical-sensitivity": "Vertical sensitivity","vibration-intensity": "Vibration intensity","vibration-status": "Vibration",video: "Video","virtual-controller": "Virtual controller","virtual-controller-slot": "Virtual controller slot","visual-quality": "Visual quality","visual-quality-high": "High","visual-quality-low": "Low","visual-quality-normal": "Normal",volume: "Volume","wait-time-countdown": "Countdown","wait-time-estimated": "Estimated finish time","waiting-for-input": "Waiting for input...",wallpaper: "Wallpaper",webgl2: "WebGL2"}; +var SUPPORTED_LANGUAGES = {"en-US": "English (US)","ca-CA": "Català","da-DK": "dansk","de-DE": "Deutsch","en-ID": "Bahasa Indonesia","es-ES": "español (España)","fr-FR": "français","it-IT": "italiano","ja-JP": "日本語","ko-KR": "한국어","pl-PL": "polski","pt-BR": "português (Brasil)","ru-RU": "русский","th-TH": "ภาษาไทย","tr-TR": "Türkçe","uk-UA": "українська","vi-VN": "Tiếng Việt","zh-CN": "中文(简体)","zh-TW": "中文(繁體)"}, Texts = {webgpu: "WebGPU",achievements: "Achievements",activate: "Activate",activated: "Activated",active: "Active",advanced: "Advanced","all-games": "All games","always-off": "Always off","always-on": "Always on","amd-fidelity-cas": "AMD FidelityFX CAS","app-settings": "App settings",apply: "Apply","aspect-ratio": "Aspect ratio","aspect-ratio-note": "Don't use with native touch games",audio: "Audio",auto: "Auto",availability: "Availability","back-to-home": "Back to home","back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?","background-opacity": "Background opacity",battery: "Battery","battery-saving": "Battery saving","better-xcloud": "Better xCloud","bitrate-audio-maximum": "Maximum audio bitrate","bitrate-video-maximum": "Maximum video bitrate",bottom: "Bottom","bottom-half": "Bottom half","bottom-left": "Bottom-left","bottom-right": "Bottom-right",brazil: "Brazil",brightness: "Brightness","browser-unsupported-feature": "Your browser doesn't support this feature","button-xbox": "Xbox button","bypass-region-restriction": "Bypass region restriction","can-stream-xbox-360-games": "Can stream Xbox 360 games",cancel: "Cancel","cant-stream-xbox-360-games": "Can't stream Xbox 360 games",center: "Center",chat: "Chat","clarity-boost": "Clarity boost","clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",clear: "Clear","clear-data": "Clear data","clear-data-confirm": "Do you want to clear all Better xCloud settings and data?","clear-data-success": "Data cleared! Refresh the page to apply the changes.",clock: "Clock",close: "Close","close-app": "Close app","combine-audio-video-streams": "Combine audio & video streams","combine-audio-video-streams-summary": "May fix the laggy audio problem","conditional-formatting": "Conditional formatting text color","confirm-delete-preset": "Do you want to delete this preset?","confirm-reload-stream": "Do you want to refresh the stream?",connected: "Connected","console-connect": "Connect","continent-asia": "Asia","continent-australia": "Australia","continent-europe": "Europe","continent-north-america": "North America","continent-south-america": "South America",contrast: "Contrast",controller: "Controller","controller-customization": "Controller customization","controller-customization-input-latency-note": "May slightly increase input latency","controller-friendly-ui": "Controller-friendly UI","controller-shortcuts": "Controller shortcuts","controller-shortcuts-connect-note": "Connect a controller to use this feature","controller-shortcuts-xbox-note": "Button to open the Guide menu","controller-vibration": "Controller vibration",copy: "Copy","create-shortcut": "Shortcut",custom: "Custom","deadzone-counterweight": "Deadzone counterweight",decrease: "Decrease",default: "Default","default-opacity": "Default opacity","default-preset-note": "You can't modify default presets. Create a new one to customize it.",delete: "Delete","detect-controller-button": "Detect controller button",device: "Device","device-unsupported-touch": "Your device doesn't have touch support","device-vibration": "Device vibration","device-vibration-not-using-gamepad": "On when not using gamepad",disable: "Disable","disable-features": "Disable features","disable-home-context-menu": "Disable context menu in Home page","disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog","disable-social-features": "Disable social features","disable-xcloud-analytics": "Disable xCloud analytics",disabled: "Disabled",disconnected: "Disconnected",download: "Download",downloaded: "Downloaded",edit: "Edit","enable-controller-shortcuts": "Enable controller shortcuts","enable-local-co-op-support": "Enable local co-op support","enable-local-co-op-support-note": "Only works with some games","enable-mic-on-startup": "Enable microphone on game launch","enable-mkb": "Emulate controller with Mouse & Keyboard","enable-quick-glance-mode": 'Enable "Quick Glance" mode',"enable-remote-play-feature": 'Enable the "Remote Play" feature',"enable-volume-control": "Enable volume control feature",enabled: "Enabled",experimental: "Experimental",export: "Export",fast: "Fast","force-native-mkb-games": "Force native Mouse & Keyboard for these games","fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile',"fortnite-force-console-version": "Fortnite: force console version","friends-followers": "Friends and followers","game-bar": "Game Bar","getting-consoles-list": "Getting the list of consoles...",guide: "Guide",help: "Help",hide: "Hide","hide-idle-cursor": "Hide mouse cursor on idle","hide-scrollbar": "Hide web page's scrollbar","hide-sections": "Hide sections","hide-system-menu-icon": "Hide System menu's icon","hide-touch-controller": "Hide touch controller","high-performance": "High performance","highest-quality": "Highest quality","highest-quality-note": "Your device may not be powerful enough to use these settings","horizontal-scroll-sensitivity": "Horizontal scroll sensitivity","horizontal-sensitivity": "Horizontal sensitivity","how-to-fix": "How to fix","how-to-improve-app-performance": "How to improve app's performance",ignore: "Ignore","image-quality": "Website's image quality",import: "Import","in-game-controller-customization": "In-game controller customization","in-game-controller-shortcuts": "In-game controller shortcuts","in-game-keyboard-shortcuts": "In-game keyboard shortcuts","in-game-shortcuts": "In-game shortcuts",increase: "Increase","install-android": "Better xCloud app for Android",invites: "Invites",japan: "Japan",jitter: "Jitter","keyboard-key": "Keyboard key","keyboard-shortcuts": "Keyboard shortcuts",korea: "Korea",language: "Language",large: "Large",layout: "Layout","left-stick": "Left stick","left-stick-deadzone": "Left stick deadzone","left-trigger-range": "Left trigger range","limit-fps": "Limit FPS","load-failed-message": "Failed to run Better xCloud","loading-screen": "Loading screen","local-co-op": "Local co-op","lowest-quality": "Lowest quality",manage: "Manage","map-mouse-to": "Map mouse to","may-not-work-properly": "May not work properly!",menu: "Menu",microphone: "Microphone","mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings","mkb-click-to-activate": "Click to activate","mkb-disclaimer": "This could be viewed as cheating when playing online","modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.","mouse-and-keyboard": "Mouse & Keyboard","mouse-click": "Mouse click","mouse-wheel": "Mouse wheel",muted: "Muted",name: "Name","native-mkb": "Native Mouse & Keyboard",new: "New","new-version-available": [e => `Version ${e.version} available`,e => `Versió ${e.version} disponible`,,e => `Version ${e.version} verfügbar`,e => `Versi ${e.version} tersedia`,e => `Versión ${e.version} disponible`,e => `Version ${e.version} disponible`,e => `Disponibile la versione ${e.version}`,e => `Ver ${e.version} が利用可能です`,e => `${e.version} 버전 사용가능`,e => `Dostępna jest nowa wersja ${e.version}`,e => `Versão ${e.version} disponível`,e => `Версия ${e.version} доступна`,e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`,e => `${e.version} sayılı yeni sürüm mevcut`,e => `Доступна версія ${e.version}`,e => `Đã có phiên bản ${e.version}`,e => `版本 ${e.version} 可供更新`,e => `已可更新為 ${e.version} 版`],"no-consoles-found": "No consoles found","no-controllers-connected": "No controllers connected",normal: "Normal",notifications: "Notifications",off: "Off",official: "Official",on: "On","only-supports-some-games": "Only supports some games",opacity: "Opacity",other: "Other",playing: "Playing",playtime: "Playtime",poland: "Poland","polling-rate": "Polling rate",position: "Position","powered-off": "Powered off","powered-on": "Powered on","prefer-ipv6-server": "Prefer IPv6 server","preferred-game-language": "Preferred game's language",preset: "Preset",press: "Press","press-any-button": "Press any button...","press-esc-to-cancel": "Press Esc to cancel","press-key-to-toggle-mkb": [e => `Press ${e.key} to toggle this feature`,e => `Premeu ${e.key} per alternar aquesta funció`,e => `Tryk på ${e.key} for at slå denne funktion til`,e => `${e.key}: Funktion an-/ausschalten`,e => `Tekan ${e.key} untuk mengaktifkan fitur ini`,e => `Pulsa ${e.key} para alternar esta función`,e => `Appuyez sur ${e.key} pour activer cette fonctionnalité`,e => `Premi ${e.key} per attivare questa funzionalità`,e => `${e.key} でこの機能を切替`,e => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`,e => `Naciśnij ${e.key} aby przełączyć tę funkcję`,e => `Pressione ${e.key} para alternar este recurso`,e => `Нажмите ${e.key} для переключения этой функции`,e => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`,e => `Etkinleştirmek için ${e.key} tuşuna basın`,e => `Натисніть ${e.key} щоб перемкнути цю функцію`,e => `Nhấn ${e.key} để bật/tắt tính năng này`,e => `按下 ${e.key} 来切换此功能`,e => `按下 ${e.key} 來啟用此功能`],"press-to-bind": "Press a key or do a mouse click to bind...","prompt-preset-name": "Preset's name:",recommended: "Recommended","recommended-settings-for-device": [e => `Recommended settings for ${e.device}`,e => `Configuració recomanada per a ${e.device}`,,e => `Empfohlene Einstellungen für ${e.device}`,e => `Rekomendasi pengaturan untuk ${e.device}`,e => `Ajustes recomendados para ${e.device}`,e => `Paramètres recommandés pour ${e.device}`,e => `Configurazioni consigliate per ${e.device}`,e => `${e.device} の推奨設定`,e => `다음 기기에서 권장되는 설정: ${e.device}`,e => `Zalecane ustawienia dla ${e.device}`,e => `Configurações recomendadas para ${e.device}`,e => `Рекомендуемые настройки для ${e.device}`,e => `การตั้งค่าที่แนะนำสำหรับ ${e.device}`,e => `${e.device} için önerilen ayarlar`,e => `Рекомендовані налаштування для ${e.device}`,e => `Cấu hình được đề xuất cho ${e.device}`,e => `${e.device} 的推荐设置`,e => `${e.device} 推薦的設定`],"reduce-animations": "Reduce UI animations",region: "Region","reload-page": "Reload page","remote-play": "Remote Play",rename: "Rename",renderer: "Renderer","renderer-configuration": "Renderer configuration","reset-highlighted-setting": "Reset highlighted setting","right-click-to-unbind": "Right-click on a key to unbind it","right-stick": "Right stick","right-stick-deadzone": "Right stick deadzone","right-trigger-range": "Right trigger range","rocket-always-hide": "Always hide","rocket-always-show": "Always show","rocket-animation": "Rocket animation","rocket-hide-queue": "Hide when queuing",saturation: "Saturation",save: "Save",screen: "Screen","screenshot-apply-filters": "Apply video filters to screenshots","section-all-games": "All games","section-most-popular": "Most popular","section-native-mkb": "Play with mouse & keyboard","section-news": "News","section-play-with-friends": "Play with friends","section-touch": "Play with touch","separate-touch-controller": "Separate Touch controller & Controller #1","separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",server: "Server","server-locations": "Server locations",settings: "Settings","settings-for": "Settings for","settings-reload": "Reload page to reflect changes","settings-reload-note": "Settings in this tab only go into effect on the next page load","settings-reloading": "Reloading...",sharpness: "Sharpness","shortcut-keys": "Shortcut keys",show: "Show","show-controller-connection-status": "Show controller connection status","show-game-art": "Show game art","show-hide": "Show/hide","show-stats-on-startup": "Show stats when starting the game","show-touch-controller": "Show touch controller","show-wait-time": "Show the estimated wait time","show-wait-time-in-game-card": "Show wait time in game card","simplify-stream-menu": "Simplify Stream's menu","skip-splash-video": "Skip Xbox splash video",slow: "Slow",small: "Small","smart-tv": "Smart TV",sound: "Sound",standard: "Standard",standby: "Standby","stat-bitrate": "Bitrate","stat-decode-time": "Decode time","stat-fps": "FPS","stat-frames-lost": "Frames lost","stat-packets-lost": "Packets lost","stat-ping": "Ping",stats: "Stats","stick-decay-minimum": "Stick decay minimum","stick-decay-strength": "Stick decay strength",stream: "Stream","stream-settings": "Stream settings","stream-stats": "Stream stats","stream-your-own-game": "Stream your own game",stretch: "Stretch","suggest-settings": "Suggest settings","suggest-settings-link": "Suggest recommended settings for this device","support-better-xcloud": "Support Better xCloud","swap-buttons": "Swap buttons","take-screenshot": "Take screenshot","target-resolution": "Target resolution","tc-all-white": "All white","tc-auto-off": "Off when controller found","tc-custom-layout-style": "Custom layout's button style","tc-muted-colors": "Muted colors","tc-standard-layout-style": "Standard layout's button style","text-size": "Text size",toggle: "Toggle",top: "Top","top-center": "Top-center","top-half": "Top half","top-left": "Top-left","top-right": "Top-right","touch-control-layout": "Touch control layout","touch-control-layout-by": [e => `Touch control layout by ${e.name}`,e => `Format del control tàctil per ${e.name}`,e => `Touch-kontrol layout af ${e.name}`,e => `Touch-Steuerungslayout von ${e.name}`,e => `Tata letak Sentuhan layar oleh ${e.name}`,e => `Disposición del control táctil por ${e.nombre}`,e => `Disposition du contrôleur tactile par ${e.name}`,e => `Configurazione dei comandi su schermo creata da ${e.name}`,e => `タッチ操作レイアウト作成者: ${e.name}`,e => `${e.name} 제작, 터치 컨트롤 레이아웃`,e => `Układ sterowania dotykowego stworzony przez ${e.name}`,e => `Disposição de controle por toque feito por ${e.name}`,e => `Сенсорная раскладка по ${e.name}`,e => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`,e => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,e => `Розташування сенсорного керування від ${e.name}`,e => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,e => `由 ${e.name} 提供的虚拟按键样式`,e => `觸控遊玩佈局由 ${e.name} 提供`],"touch-controller": "Touch controller","true-achievements": "TrueAchievements",ui: "UI","unexpected-behavior": "May cause unexpected behavior","united-states": "United States",unknown: "Unknown",unlimited: "Unlimited",unmuted: "Unmuted",unofficial: "Unofficial","unofficial-game-list": "Unofficial game list","unsharp-masking": "Unsharp masking",upload: "Upload",uploaded: "Uploaded","use-mouse-absolute-position": "Use mouse's absolute position","use-this-at-your-own-risk": "Use this at your own risk","user-agent-profile": "User-Agent profile","vertical-scroll-sensitivity": "Vertical scroll sensitivity","vertical-sensitivity": "Vertical sensitivity","vibration-intensity": "Vibration intensity","vibration-status": "Vibration",video: "Video","virtual-controller": "Virtual controller","virtual-controller-slot": "Virtual controller slot","visual-quality": "Visual quality","visual-quality-high": "High","visual-quality-low": "Low","visual-quality-normal": "Normal",volume: "Volume","wait-time-countdown": "Countdown","wait-time-estimated": "Estimated finish time","waiting-for-input": "Waiting for input...",wallpaper: "Wallpaper",webgl2: "WebGL2"}; class Translations {static EN_US = "en-US";static KEY_LOCALE = "BetterXcloud.Locale";static KEY_TRANSLATIONS = "BetterXcloud.Locale.Translations";static selectedLocaleIndex = -1;static selectedLocale = "en-US";static supportedLocales = Object.keys(SUPPORTED_LANGUAGES);static foreignTranslations = {};static enUsIndex = Translations.supportedLocales.indexOf(Translations.EN_US);static async init() {Translations.refreshLocale(), await Translations.loadTranslations();}static refreshLocale(newLocale) {let locale;if (newLocale) localStorage.setItem(Translations.KEY_LOCALE, newLocale), locale = newLocale;else locale = localStorage.getItem(Translations.KEY_LOCALE);let supportedLocales = Translations.supportedLocales;if (!locale) {if (locale = window.navigator.language || Translations.EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.EN_US;localStorage.setItem(Translations.KEY_LOCALE, locale);}Translations.selectedLocale = locale, Translations.selectedLocaleIndex = supportedLocales.indexOf(locale);}static get(key, values) {let text = null;if (Translations.foreignTranslations && Translations.selectedLocale !== Translations.EN_US) text = Translations.foreignTranslations[key];if (!text) text = Texts[key] || alert(`Missing translation key: ${key}`);let translation;if (Array.isArray(text)) return translation = text[Translations.selectedLocaleIndex] || text[Translations.enUsIndex], translation(values);return translation = text, translation;}static async loadTranslations() {if (Translations.selectedLocale === Translations.EN_US) return;try {Translations.foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.KEY_TRANSLATIONS));} catch (e) {}if (!Translations.foreignTranslations) await this.downloadTranslations(Translations.selectedLocale);}static async updateTranslations(async = !1) {if (Translations.selectedLocale === Translations.EN_US) {localStorage.removeItem(Translations.KEY_TRANSLATIONS);return;}if (async) Translations.downloadTranslationsAsync(Translations.selectedLocale);else await Translations.downloadTranslations(Translations.selectedLocale);}static async downloadTranslations(locale) {try {let translations = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`))).json();if (localStorage.getItem(Translations.KEY_LOCALE) === locale) window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;return !0;} catch (e) {debugger;}return !1;}static downloadTranslationsAsync(locale) {NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`)).then((resp) => resp.json()).then((translations) => {window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;});}static switchLocale(locale) {localStorage.setItem(Translations.KEY_LOCALE, locale);}} var t = Translations.get; Translations.init(); @@ -77,7 +77,7 @@ class MkbMappingPresetsTable extends BasePresetsTable {static instance;static ge class GameSettingsStorage extends BaseSettingsStorage {constructor(id) {super(`${"BetterXcloud.Stream"}.${id}`, StreamSettingsStorage.DEFINITIONS);}isEmpty() {return Object.keys(this.settings).length === 0;}} class ControllerCustomizationsTable extends BasePresetsTable {static instance;static getInstance = () => ControllerCustomizationsTable.instance ?? (ControllerCustomizationsTable.instance = new ControllerCustomizationsTable(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS));TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS;DEFAULT_PRESETS = {[-1]: {id: -1,name: "ABXY ⇄ BAYX",data: {mapping: {0: 1,1: 0,2: 3,3: 2},settings: {leftStickDeadzone: [0, 100],rightStickDeadzone: [0, 100],leftTriggerRange: [0, 100],rightTriggerRange: [0, 100],vibrationIntensity: 100}}}};BLANK_PRESET_DATA = {mapping: {},settings: {leftTriggerRange: [0, 100],rightTriggerRange: [0, 100],leftStickDeadzone: [0, 100],rightStickDeadzone: [0, 100],vibrationIntensity: 100}};DEFAULT_PRESET_ID = 0;} class ControllerShortcutsTable extends BasePresetsTable {static instance;static getInstance = () => ControllerShortcutsTable.instance ?? (ControllerShortcutsTable.instance = new ControllerShortcutsTable);LOG_TAG = "ControllerShortcutsTable";TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_SHORTCUTS;DEFAULT_PRESETS = {[-1]: {id: -1,name: "Type A",data: {mapping: {3: AppInterface ? "device.volume.inc" : "stream.volume.inc",0: AppInterface ? "device.volume.dec" : "stream.volume.dec",2: "stream.stats.toggle",1: AppInterface ? "device.sound.toggle" : "stream.sound.toggle",5: "stream.screenshot.capture",9: "stream.menu.show"}}},[-2]: {id: -2,name: "Type B",data: {mapping: {12: AppInterface ? "device.volume.inc" : "stream.volume.inc",13: AppInterface ? "device.volume.dec" : "stream.volume.dec",15: "stream.stats.toggle",14: AppInterface ? "device.sound.toggle" : "stream.sound.toggle",4: "stream.screenshot.capture",8: "stream.menu.show"}}}};BLANK_PRESET_DATA = {mapping: {}};DEFAULT_PRESET_ID = -1;constructor() {super(LocalDb.TABLE_CONTROLLER_SHORTCUTS);BxLogger.info(this.LOG_TAG, "constructor()");}} -class StreamSettingsStorage extends BaseSettingsStorage {static DEFINITIONS = {"deviceVibration.mode": {requiredVariants: "full",label: t("device-vibration"),default: "off",options: {off: t("off"),on: t("on"),auto: t("device-vibration-not-using-gamepad")}},"deviceVibration.intensity": {requiredVariants: "full",label: t("vibration-intensity"),default: 50,min: 10,max: 100,params: {steps: 10,suffix: "%",exactTicks: 20}},"controller.pollingRate": {requiredVariants: "full",label: t("polling-rate"),default: 4,min: 4,max: 60,params: {steps: 4,exactTicks: 20,reverse: !0,customTextValue(value) {value = parseInt(value);let text = +(1000 / value).toFixed(2) + " Hz";if (value === 4) text = `${text} (${t("default")})`;return text;}}},"controller.settings": {default: {}},"nativeMkb.scroll.sensitivityX": {requiredVariants: "full",label: t("horizontal-scroll-sensitivity"),default: 0,min: 0,max: 1e4,params: {steps: 10,exactTicks: 2000,customTextValue: (value) => {if (!value) return t("default");return (value / 100).toFixed(1) + "x";}}},"nativeMkb.scroll.sensitivityY": {requiredVariants: "full",label: t("vertical-scroll-sensitivity"),default: 0,min: 0,max: 1e4,params: {steps: 10,exactTicks: 2000,customTextValue: (value) => {if (!value) return t("default");return (value / 100).toFixed(1) + "x";}}},"mkb.p1.preset.mappingId": {requiredVariants: "full",default: -1},"mkb.p1.slot": {requiredVariants: "full",default: 1,min: 1,max: 4,params: {hideSlider: !0}},"mkb.p2.preset.mappingId": {requiredVariants: "full",default: 0},"mkb.p2.slot": {requiredVariants: "full",default: 0,min: 0,max: 4,params: {hideSlider: !0,customTextValue(value) {return value = parseInt(value), value === 0 ? t("off") : value.toString();}}},"keyboardShortcuts.preset.inGameId": {requiredVariants: "full",default: -1},"video.player.type": {label: t("renderer"),default: "default",options: {default: t("default"),webgl2: t("webgl2")},suggest: {lowest: "default",highest: "webgl2"}},"video.processing": {label: t("clarity-boost"),default: "usm",options: {usm: t("unsharp-masking"),cas: t("amd-fidelity-cas")},suggest: {lowest: "usm",highest: "cas"}},"video.player.powerPreference": {label: t("renderer-configuration"),default: "default",options: {default: t("default"),"low-power": t("battery-saving"),"high-performance": t("high-performance")},suggest: {highest: "low-power"}},"video.maxFps": {label: t("limit-fps"),default: 60,min: 10,max: 60,params: {steps: 10,exactTicks: 10,customTextValue: (value) => {return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps";}}},"video.processing.sharpness": {label: t("sharpness"),default: 0,min: 0,max: 10,params: {exactTicks: 2,customTextValue: (value) => {return value = parseInt(value), value === 0 ? t("off") : value.toString();}},suggest: {lowest: 0,highest: 2}},"video.ratio": {label: t("aspect-ratio"),note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,default: "16:9",options: {"16:9": `16:9 (${t("default")})`,"18:9": "18:9","21:9": "21:9","16:10": "16:10","4:3": "4:3",fill: t("stretch")}},"video.position": {label: t("position"),note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,default: "center",options: {top: t("top"),"top-half": t("top-half"),center: `${t("center")} (${t("default")})`,"bottom-half": t("bottom-half"),bottom: t("bottom")}},"video.saturation": {label: t("saturation"),default: 100,min: 50,max: 150,params: {suffix: "%",ticks: 25}},"video.contrast": {label: t("contrast"),default: 100,min: 50,max: 150,params: {suffix: "%",ticks: 25}},"video.brightness": {label: t("brightness"),default: 100,min: 50,max: 150,params: {suffix: "%",ticks: 25}},"audio.volume": {label: t("volume"),default: 100,min: 0,max: 600,params: {steps: 10,suffix: "%",ticks: 100}},"stats.items": {label: t("stats"),default: ["ping", "fps", "btr", "dt", "pl", "fl"],multipleOptions: {time: t("clock"),play: t("playtime"),batt: t("battery"),ping: t("stat-ping"),jit: t("jitter"),fps: t("stat-fps"),btr: t("stat-bitrate"),dt: t("stat-decode-time"),pl: t("stat-packets-lost"),fl: t("stat-frames-lost"),dl: t("downloaded"),ul: t("uploaded")},params: {size: 0},ready: (setting) => {let multipleOptions = setting.multipleOptions;if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"];for (let key in multipleOptions)multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key];}},"stats.showWhenPlaying": {label: t("show-stats-on-startup"),default: !1},"stats.quickGlance.enabled": {label: "👀 " + t("enable-quick-glance-mode"),default: !0},"stats.position": {label: t("position"),default: "top-right",options: {"top-left": t("top-left"),"top-center": t("top-center"),"top-right": t("top-right")}},"stats.textSize": {label: t("text-size"),default: "0.9rem",options: {"0.9rem": t("small"),"1.0rem": t("normal"),"1.1rem": t("large")}},"stats.opacity.all": {label: t("opacity"),default: 80,min: 50,max: 100,params: {steps: 10,suffix: "%",ticks: 10}},"stats.opacity.background": {label: t("background-opacity"),default: 100,min: 0,max: 100,params: {steps: 10,suffix: "%",ticks: 10}},"stats.colors": {label: t("conditional-formatting"),default: !1},"localCoOp.enabled": {requiredVariants: "full",label: t("enable-local-co-op-support"),labelIcon: BxIcon.LOCAL_CO_OP,default: !1,note: () => CE("div", !1, CE("a", {href: "https://github.com/redphx/better-xcloud/discussions/275",target: "_blank"}, t("enable-local-co-op-support-note")), CE("br"), "⚠️ " + t("unexpected-behavior"))}};gameSettings = {};xboxTitleId = -1;constructor() {super("BetterXcloud.Stream", StreamSettingsStorage.DEFINITIONS);}setGameId(id) {this.xboxTitleId = id;}getGameSettings(id) {if (id > -1) {if (!this.gameSettings[id]) {let gameStorage = new GameSettingsStorage(id);this.gameSettings[id] = gameStorage;for (let key in gameStorage.settings)this.getSettingByGame(id, key);}return this.gameSettings[id];}return null;}getSetting(key, checkUnsupported) {return this.getSettingByGame(this.xboxTitleId, key, checkUnsupported);}getSettingByGame(id, key, checkUnsupported) {let gameSettings = this.getGameSettings(id);if (gameSettings?.hasSetting(key)) {let gameValue = gameSettings.getSetting(key, checkUnsupported), globalValue = super.getSetting(key, checkUnsupported);if (globalValue === gameValue) this.deleteSettingByGame(id, key), gameValue = globalValue;return gameValue;}return super.getSetting(key, checkUnsupported);}setSetting(key, value, origin) {return this.setSettingByGame(this.xboxTitleId, key, value, origin);}setSettingByGame(id, key, value, origin) {let gameSettings = this.getGameSettings(id);if (gameSettings) return BxLogger.info("setSettingByGame", id, key, value), gameSettings.setSetting(key, value, origin);return BxLogger.info("setSettingByGame", id, key, value), super.setSetting(key, value, origin);}deleteSettingByGame(id, key) {let gameSettings = this.getGameSettings(id);if (gameSettings) return gameSettings.deleteSetting(key);return !1;}hasGameSetting(id, key) {let gameSettings = this.getGameSettings(id);return !!(gameSettings && gameSettings.hasSetting(key));}getControllerSetting(gamepadId) {let controllerSetting = this.getSetting("controller.settings")[gamepadId];if (!controllerSetting) controllerSetting = {};if (!controllerSetting.hasOwnProperty("shortcutPresetId")) controllerSetting.shortcutPresetId = -1;if (!controllerSetting.hasOwnProperty("customizationPresetId")) controllerSetting.customizationPresetId = 0;return controllerSetting;}} +class StreamSettingsStorage extends BaseSettingsStorage {static DEFINITIONS = {"deviceVibration.mode": {requiredVariants: "full",label: t("device-vibration"),default: "off",options: {off: t("off"),on: t("on"),auto: t("device-vibration-not-using-gamepad")}},"deviceVibration.intensity": {requiredVariants: "full",label: t("vibration-intensity"),default: 50,min: 10,max: 100,params: {steps: 10,suffix: "%",exactTicks: 20}},"controller.pollingRate": {requiredVariants: "full",label: t("polling-rate"),default: 4,min: 4,max: 60,params: {steps: 4,exactTicks: 20,reverse: !0,customTextValue(value) {value = parseInt(value);let text = +(1000 / value).toFixed(2) + " Hz";if (value === 4) text = `${text} (${t("default")})`;return text;}}},"controller.settings": {default: {}},"nativeMkb.scroll.sensitivityX": {requiredVariants: "full",label: t("horizontal-scroll-sensitivity"),default: 0,min: 0,max: 1e4,params: {steps: 10,exactTicks: 2000,customTextValue: (value) => {if (!value) return t("default");return (value / 100).toFixed(1) + "x";}}},"nativeMkb.scroll.sensitivityY": {requiredVariants: "full",label: t("vertical-scroll-sensitivity"),default: 0,min: 0,max: 1e4,params: {steps: 10,exactTicks: 2000,customTextValue: (value) => {if (!value) return t("default");return (value / 100).toFixed(1) + "x";}}},"mkb.p1.preset.mappingId": {requiredVariants: "full",default: -1},"mkb.p1.slot": {requiredVariants: "full",default: 1,min: 1,max: 4,params: {hideSlider: !0}},"mkb.p2.preset.mappingId": {requiredVariants: "full",default: 0},"mkb.p2.slot": {requiredVariants: "full",default: 0,min: 0,max: 4,params: {hideSlider: !0,customTextValue(value) {return value = parseInt(value), value === 0 ? t("off") : value.toString();}}},"keyboardShortcuts.preset.inGameId": {requiredVariants: "full",default: -1},"video.player.type": {label: t("renderer"),default: "default",options: {default: t("default"),webgl2: t("webgl2"),webgpu: t("webgpu")},suggest: {lowest: "default",highest: "webgl2"},ready: (setting) => {if (!navigator.gpu) delete setting.options["webgpu"];}},"video.processing": {label: t("clarity-boost"),default: "usm",options: {usm: t("unsharp-masking"),cas: t("amd-fidelity-cas")},suggest: {lowest: "usm",highest: "cas"}},"video.player.powerPreference": {label: t("renderer-configuration"),default: "default",options: {default: t("default"),"low-power": t("battery-saving"),"high-performance": t("high-performance")},suggest: {highest: "low-power"}},"video.maxFps": {label: t("limit-fps"),default: 60,min: 10,max: 60,params: {steps: 10,exactTicks: 10,customTextValue: (value) => {return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps";}}},"video.processing.sharpness": {label: t("sharpness"),default: 0,min: 0,max: 10,params: {exactTicks: 2,customTextValue: (value) => {return value = parseInt(value), value === 0 ? t("off") : value.toString();}},suggest: {lowest: 0,highest: 2}},"video.ratio": {label: t("aspect-ratio"),note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,default: "16:9",options: {"16:9": `16:9 (${t("default")})`,"18:9": "18:9","21:9": "21:9","16:10": "16:10","4:3": "4:3",fill: t("stretch")}},"video.position": {label: t("position"),note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,default: "center",options: {top: t("top"),"top-half": t("top-half"),center: `${t("center")} (${t("default")})`,"bottom-half": t("bottom-half"),bottom: t("bottom")}},"video.saturation": {label: t("saturation"),default: 100,min: 50,max: 150,params: {suffix: "%",ticks: 25}},"video.contrast": {label: t("contrast"),default: 100,min: 50,max: 150,params: {suffix: "%",ticks: 25}},"video.brightness": {label: t("brightness"),default: 100,min: 50,max: 150,params: {suffix: "%",ticks: 25}},"audio.volume": {label: t("volume"),default: 100,min: 0,max: 600,params: {steps: 10,suffix: "%",ticks: 100}},"stats.items": {label: t("stats"),default: ["ping", "fps", "btr", "dt", "pl", "fl"],multipleOptions: {time: t("clock"),play: t("playtime"),batt: t("battery"),ping: t("stat-ping"),jit: t("jitter"),fps: t("stat-fps"),btr: t("stat-bitrate"),dt: t("stat-decode-time"),pl: t("stat-packets-lost"),fl: t("stat-frames-lost"),dl: t("downloaded"),ul: t("uploaded")},params: {size: 0},ready: (setting) => {let multipleOptions = setting.multipleOptions;if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"];for (let key in multipleOptions)multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key];}},"stats.showWhenPlaying": {label: t("show-stats-on-startup"),default: !1},"stats.quickGlance.enabled": {label: "👀 " + t("enable-quick-glance-mode"),default: !0},"stats.position": {label: t("position"),default: "top-right",options: {"top-left": t("top-left"),"top-center": t("top-center"),"top-right": t("top-right")}},"stats.textSize": {label: t("text-size"),default: "0.9rem",options: {"0.9rem": t("small"),"1.0rem": t("normal"),"1.1rem": t("large")}},"stats.opacity.all": {label: t("opacity"),default: 80,min: 50,max: 100,params: {steps: 10,suffix: "%",ticks: 10}},"stats.opacity.background": {label: t("background-opacity"),default: 100,min: 0,max: 100,params: {steps: 10,suffix: "%",ticks: 10}},"stats.colors": {label: t("conditional-formatting"),default: !1},"localCoOp.enabled": {requiredVariants: "full",label: t("enable-local-co-op-support"),labelIcon: BxIcon.LOCAL_CO_OP,default: !1,note: () => CE("div", !1, CE("a", {href: "https://github.com/redphx/better-xcloud/discussions/275",target: "_blank"}, t("enable-local-co-op-support-note")), CE("br"), "⚠️ " + t("unexpected-behavior"))}};gameSettings = {};xboxTitleId = -1;constructor() {super("BetterXcloud.Stream", StreamSettingsStorage.DEFINITIONS);}setGameId(id) {this.xboxTitleId = id;}getGameSettings(id) {if (id > -1) {if (!this.gameSettings[id]) {let gameStorage = new GameSettingsStorage(id);this.gameSettings[id] = gameStorage;for (let key in gameStorage.settings)this.getSettingByGame(id, key);}return this.gameSettings[id];}return null;}getSetting(key, checkUnsupported) {return this.getSettingByGame(this.xboxTitleId, key, checkUnsupported);}getSettingByGame(id, key, checkUnsupported) {let gameSettings = this.getGameSettings(id);if (gameSettings?.hasSetting(key)) {let gameValue = gameSettings.getSetting(key, checkUnsupported), globalValue = super.getSetting(key, checkUnsupported);if (globalValue === gameValue) this.deleteSettingByGame(id, key), gameValue = globalValue;return gameValue;}return super.getSetting(key, checkUnsupported);}setSetting(key, value, origin) {return this.setSettingByGame(this.xboxTitleId, key, value, origin);}setSettingByGame(id, key, value, origin) {let gameSettings = this.getGameSettings(id);if (gameSettings) return BxLogger.info("setSettingByGame", id, key, value), gameSettings.setSetting(key, value, origin);return BxLogger.info("setSettingByGame", id, key, value), super.setSetting(key, value, origin);}deleteSettingByGame(id, key) {let gameSettings = this.getGameSettings(id);if (gameSettings) return gameSettings.deleteSetting(key);return !1;}hasGameSetting(id, key) {let gameSettings = this.getGameSettings(id);return !!(gameSettings && gameSettings.hasSetting(key));}getControllerSetting(gamepadId) {let controllerSetting = this.getSetting("controller.settings")[gamepadId];if (!controllerSetting) controllerSetting = {};if (!controllerSetting.hasOwnProperty("shortcutPresetId")) controllerSetting.shortcutPresetId = -1;if (!controllerSetting.hasOwnProperty("customizationPresetId")) controllerSetting.customizationPresetId = 0;return controllerSetting;}} function migrateStreamSettings() {let storage = window.localStorage, globalSettings = JSON.parse(storage.getItem("BetterXcloud") || "{}"), streamSettings = JSON.parse(storage.getItem("BetterXcloud.Stream") || "{}"), modified2 = !1;for (let key in globalSettings)if (isStreamPref(key)) {if (!streamSettings.hasOwnProperty(key)) streamSettings[key] = globalSettings[key];delete globalSettings[key], modified2 = !0;}if (modified2) storage.setItem("BetterXcloud", JSON.stringify(globalSettings)), storage.setItem("BetterXcloud.Stream", JSON.stringify(streamSettings));} migrateStreamSettings(); var STORAGE = {Global: new GlobalSettingsStorage,Stream: new StreamSettingsStorage}, streamSettingsStorage = STORAGE.Stream, getStreamPrefDefinition = streamSettingsStorage.getDefinition.bind(streamSettingsStorage), getStreamPref = streamSettingsStorage.getSetting.bind(streamSettingsStorage), setStreamPref = streamSettingsStorage.setSetting.bind(streamSettingsStorage), getGamePref = streamSettingsStorage.getSettingByGame.bind(streamSettingsStorage), setGamePref = streamSettingsStorage.setSettingByGame.bind(streamSettingsStorage), setGameIdPref = streamSettingsStorage.setGameId.bind(streamSettingsStorage), hasGamePref = streamSettingsStorage.hasGameSetting.bind(streamSettingsStorage); @@ -127,11 +127,12 @@ class BxNumberStepper extends HTMLInputElement {intervalId = null;isHolding;cont class SettingElement {static renderOptions(key, setting, currentValue, onChange) {let $control = CE("select", {tabindex: 0}), $parent;if (setting.optionsGroup) $parent = CE("optgroup", {label: setting.optionsGroup}), $control.appendChild($parent);else $parent = $control;for (let value in setting.options) {let label = setting.options[value], $option = CE("option", { value }, label);$parent.appendChild($option);}return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => {let target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value;!e.ignoreOnChange && onChange(e, value);}), $control.setValue = (value) => {$control.value = value;}, $control;}static renderMultipleOptions(key, setting, currentValue, onChange, params = {}) {let $control = CE("select", {multiple: !0,tabindex: 0}), totalOptions = Object.keys(setting.multipleOptions).length, size = params.size ? Math.min(params.size, totalOptions) : totalOptions;$control.setAttribute("size", size.toString());for (let value in setting.multipleOptions) {let label = setting.multipleOptions[value], $option = CE("option", { value }, label);$option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) {e.preventDefault();let target = e.target;target.selected = !target.selected;let $parent = target.parentElement;$parent.focus(), BxEvent.dispatch($parent, "input");}), $control.appendChild($option);}return $control.addEventListener("mousedown", function(e) {let self = this, orgScrollTop = self.scrollTop;window.setTimeout(() => self.scrollTop = orgScrollTop, 0);}), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => {let target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value);!e.ignoreOnChange && onChange(e, values);}), Object.defineProperty($control, "value", {get() {return Array.from($control.options).filter((option) => option.selected).map((option) => option.value);},set(value) {let values = value.split(",");Array.from($control.options).forEach((option) => {option.selected = values.includes(option.value);});}}), $control;}static renderCheckbox(key, setting, currentValue, onChange) {let $control = CE("input", { type: "checkbox", tabindex: 0 });return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => {!e.ignoreOnChange && onChange(e, e.target.checked);}), $control.setValue = (value) => {$control.checked = !!value;}, $control;}static renderNumberStepper(key, setting, value, onChange, options = {}) {return BxNumberStepper.create(key, value, setting.min, setting.max, options, onChange);}static METHOD_MAP = {options: SettingElement.renderOptions,"multiple-options": SettingElement.renderMultipleOptions,"number-stepper": SettingElement.renderNumberStepper,checkbox: SettingElement.renderCheckbox};static render(type, key, setting, currentValue, onChange, options) {let method = SettingElement.METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1));if (type !== "number-stepper") $control.id = `bx_setting_${escapeCssSelector(key)}`;if (type === "options" || type === "multiple-options") $control.name = $control.id;return $control;}static fromPref(key, onChange, overrideParams = {}) {let { definition, storage } = getPrefInfo(key);if (!definition) return null;let currentValue = storage.getSetting(key), type;if ("options" in definition) type = "options";else if ("multipleOptions" in definition) type = "multiple-options";else if (typeof definition.default === "number") type = "number-stepper";else type = "checkbox";let params = {};if ("params" in definition) params = Object.assign(overrideParams, definition.params || {});if (params.disabled) currentValue = definition.default;return SettingElement.render(type, key, definition, currentValue, (e, value) => {if (isGlobalPref(key)) setGlobalPref(key, value, "ui");else {let id = SettingsManager.getInstance().getTargetGameId();setGamePref(id, key, value, "ui");}onChange && onChange(e, value);}, params);}} class BxSelectElement extends HTMLSelectElement {isControllerFriendly;optionsList;indicatorsList;$indicators;visibleIndex;isMultiple;$select;$btnNext;$btnPrev;$label;$checkBox;static create($select, forceFriendly = !1) {let isControllerFriendly = forceFriendly || getGlobalPref("ui.controllerFriendly");if ($select.multiple && !isControllerFriendly) return $select.classList.add("bx-select"), $select;$select.removeAttribute("tabindex");let $wrapper = CE("div", {class: "bx-select",_dataset: {controllerFriendly: isControllerFriendly}});if ($select.classList.contains("bx-full-width")) $wrapper.classList.add("bx-full-width");let $content, self = $wrapper;self.isControllerFriendly = isControllerFriendly, self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = [];let $btnPrev, $btnNext;if (isControllerFriendly) {$btnPrev = createButton({label: "<",style: 64}), $btnNext = createButton({label: ">",style: 64}), setNearby($wrapper, {orientation: "horizontal",focus: $btnNext}), self.$btnNext = $btnNext, self.$btnPrev = $btnPrev;let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self);$btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext);} else $select.addEventListener("change", (e) => {self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);});if (self.isMultiple) $content = CE("button", {class: "bx-select-value bx-focusable",tabindex: 0}, CE("div", !1, self.$checkBox = CE("input", { type: "checkbox" }), self.$label = CE("span", !1, "")), self.$indicators), $content.addEventListener("click", (e) => {self.$checkBox.click();}), self.$checkBox.addEventListener("input", (e) => {let $option = BxSelectElement.getOptionAtIndex.call(self, self.visibleIndex);$option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input");});else $content = CE("div", !1, self.$label = CE("label", { for: $select.id + "_checkbox" }, ""), self.$indicators);return $select.addEventListener("input", BxSelectElement.render.bind(self)), new MutationObserver((mutationList, observer2) => {mutationList.forEach((mutation) => {if (mutation.type === "childList" || mutation.type === "attributes") self.visibleIndex = $select.selectedIndex, self.optionsList = Array.from($select.querySelectorAll("option")), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);});}).observe($select, {subtree: !0,childList: !0,attributes: !0}), self.append($select, $btnPrev || "", $content, $btnNext || ""), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", {get() {return $select.value;},set(value) {self.optionsList = Array.from($select.querySelectorAll("option")), $select.value = value, self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);}}), Object.defineProperty(self, "disabled", {get() {return $select.disabled;},set(value) {$select.disabled = value;}}), self.addEventListener = function() {$select.addEventListener.apply($select, arguments);}, self.removeEventListener = function() {$select.removeEventListener.apply($select, arguments);}, self.dispatchEvent = function() {return $select.dispatchEvent.apply($select, arguments);}, self.appendChild = function(node) {return $select.appendChild(node), node;}, self;}static resetIndicators() {let {optionsList,indicatorsList,$indicators} = this, targetSize = optionsList.length;if (indicatorsList.length > targetSize) while (indicatorsList.length > targetSize)indicatorsList.pop()?.remove();else if (indicatorsList.length < targetSize) while (indicatorsList.length < targetSize) {let $indicator = CE("span", {});indicatorsList.push($indicator), $indicators.appendChild($indicator);}for (let $indicator of indicatorsList)clearDataSet($indicator);$indicators.classList.toggle("bx-invisible", targetSize <= 1);}static getOptionAtIndex(index) {return this.optionsList[index];}static render(e) {let {$label,$btnNext,$btnPrev,$checkBox,optionsList,indicatorsList} = this;if (e && e.manualTrigger) this.visibleIndex = this.$select.selectedIndex;this.visibleIndex = BxSelectElement.normalizeIndex.call(this, this.visibleIndex);let $option = BxSelectElement.getOptionAtIndex.call(this, this.visibleIndex), content = "";if ($option) {let $parent = $option.parentElement, hasLabel = $parent instanceof HTMLOptGroupElement || this.$select.querySelector("optgroup");if (content = $option.dataset.label || $option.textContent || "", content && hasLabel) {let groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : " ";$label.innerHTML = "";let fragment = document.createDocumentFragment();fragment.appendChild(CE("span", !1, groupLabel)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment);} else $label.textContent = content;} else $label.textContent = content;if ($label.classList.toggle("bx-line-through", $option && $option.disabled), this.isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content);let disableButtons = optionsList.length <= 1;$btnPrev?.classList.toggle("bx-gone", disableButtons), $btnNext?.classList.toggle("bx-gone", disableButtons);for (let i = 0;i < optionsList.length; i++) {let $option2 = optionsList[i], $indicator = indicatorsList[i];if (!$option2 || !$indicator) continue;if (clearDataSet($indicator), $option2.selected) $indicator.dataset.selected = "true";if ($option2.index === this.visibleIndex) $indicator.dataset.highlighted = "true";}}static normalizeIndex(index) {return Math.min(Math.max(index, 0), this.optionsList.length - 1);}static onPrevNext(e) {if (!e.target) return;let {$btnNext,$select,isMultiple,visibleIndex: currentIndex} = this, newIndex = e.target.closest("button") === $btnNext ? currentIndex + 1 : currentIndex - 1;if (newIndex > this.optionsList.length - 1) newIndex = 0;else if (newIndex < 0) newIndex = this.optionsList.length - 1;if (newIndex = BxSelectElement.normalizeIndex.call(this, newIndex), this.visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex;if (isMultiple) BxSelectElement.render.call(this);else BxEvent.dispatch($select, "input");}} class XboxApi {static CACHED_TITLES = {};static async getProductTitle(xboxTitleId) {if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId];let title;try {let url = `https://displaycatalog.mp.microsoft.com/v7.0/products/lookup?market=US&languages=en&value=${xboxTitleId}&alternateId=XboxTitleId&fieldsTemplate=browse`;title = (await (await NATIVE_FETCH(url)).json()).Products[0].LocalizedProperties[0].ProductTitle;} catch (e) {title = "Unknown Game #" + xboxTitleId;}return XboxApi.CACHED_TITLES[xboxTitleId] = title, title;}} -class SettingsManager {static instance;static getInstance = () => SettingsManager.instance ?? (SettingsManager.instance = new SettingsManager);$streamSettingsSelection;$tips;playingGameId = -1;targetGameId = -1;SETTINGS = {"localCoOp.enabled": {onChange: () => {BxExposed.toggleLocalCoOp(getStreamPref("localCoOp.enabled"));}},"deviceVibration.mode": {onChange: StreamSettings.refreshControllerSettings},"deviceVibration.intensity": {onChange: StreamSettings.refreshControllerSettings},"controller.pollingRate": {onChange: StreamSettings.refreshControllerSettings},"controller.settings": {onChange: StreamSettings.refreshControllerSettings},"nativeMkb.scroll.sensitivityX": {onChange: () => {let value = getStreamPref("nativeMkb.scroll.sensitivityX");NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100);}},"nativeMkb.scroll.sensitivityY": {onChange: () => {let value = getStreamPref("nativeMkb.scroll.sensitivityY");NativeMkbHandler.getInstance()?.setVerticalScrollMultiplier(value / 100);}},"video.player.type": {onChange: updateVideoPlayer,onChangeUi: onChangeVideoPlayerType},"video.player.powerPreference": {onChange: () => {let streamPlayer = STATES.currentStream.streamPlayer;if (!streamPlayer) return;streamPlayer.reloadPlayer(), updateVideoPlayer();}},"video.processing": {onChange: updateVideoPlayer},"video.processing.sharpness": {onChange: updateVideoPlayer},"video.maxFps": {onChange: () => {let value = getStreamPref("video.maxFps");limitVideoPlayerFps(value);}},"video.ratio": {onChange: updateVideoPlayer},"video.brightness": {onChange: updateVideoPlayer},"video.contrast": {onChange: updateVideoPlayer},"video.saturation": {onChange: updateVideoPlayer},"video.position": {onChange: updateVideoPlayer},"audio.volume": {onChange: () => {let value = getStreamPref("audio.volume");SoundShortcut.setGainNodeVolume(value);}},"stats.items": {onChange: StreamStats.refreshStyles},"stats.quickGlance.enabled": {onChange: () => {let value = getStreamPref("stats.quickGlance.enabled"), streamStats = StreamStats.getInstance();value ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop();}},"stats.position": {onChange: StreamStats.refreshStyles},"stats.textSize": {onChange: StreamStats.refreshStyles},"stats.opacity.all": {onChange: StreamStats.refreshStyles},"stats.opacity.background": {onChange: StreamStats.refreshStyles},"stats.colors": {onChange: StreamStats.refreshStyles},"mkb.p1.preset.mappingId": {onChange: StreamSettings.refreshMkbSettings},"mkb.p1.slot": {onChange: () => {EmulatedMkbHandler.getInstance()?.resetXcloudGamepads();}},"keyboardShortcuts.preset.inGameId": {onChange: StreamSettings.refreshKeyboardShortcuts}};constructor() {BxEventBus.Stream.on("setting.changed", (data) => {if (isStreamPref(data.settingKey)) this.updateStreamElement(data.settingKey);}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => {this.switchGameSettings(id);}), this.renderStreamSettingsSelection();}updateStreamElement(key, onChanges, onChangeUis) {let info = this.SETTINGS[key];if (info.onChangeUi) if (onChangeUis) onChangeUis.add(info.onChangeUi);else info.onChangeUi();if (info.onChange && STATES.isPlaying) if (onChanges) onChanges.add(info.onChange);else info.onChange();let $elm = info.$element;if (!$elm) return;let value = getGamePref(this.targetGameId, key, !0);if ("setValue" in $elm) $elm.setValue(value);else $elm.value = value.toString();this.updateDataset($elm, key);}switchGameSettings(id) {if (setGameIdPref(id), this.targetGameId === id) return;let onChanges = new Set, onChangeUis = new Set, oldGameId = this.targetGameId;this.targetGameId = id;let key;for (key in this.SETTINGS) {if (!isStreamPref(key)) continue;let oldValue = getGamePref(oldGameId, key, !0), newValue = getGamePref(this.targetGameId, key, !0);if (oldValue === newValue) continue;this.updateStreamElement(key, onChanges, onChangeUis);}onChangeUis.forEach((fn) => fn && fn()), onChanges.forEach((fn) => fn && fn()), this.$tips.classList.toggle("bx-gone", id < 0);}setElement(pref, $elm) {if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {};this.updateDataset($elm, pref), this.SETTINGS[pref].$element = $elm;}getElement(pref, params) {if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {};let $elm = this.SETTINGS[pref].$element;if (!$elm) $elm = SettingElement.fromPref(pref, null, params), this.SETTINGS[pref].$element = $elm;return this.updateDataset($elm, pref), $elm;}hasElement(pref) {return !!this.SETTINGS[pref]?.$element;}updateDataset($elm, pref) {if (this.targetGameId === this.playingGameId && hasGamePref(this.playingGameId, pref)) $elm.dataset.override = "true";else delete $elm.dataset.override;}renderStreamSettingsSelection() {this.$tips = CE("p", { class: "bx-gone" }, `⇐ Q ⟶: ${t("reset-highlighted-setting")}`);let $select = BxSelectElement.create(CE("select", !1, CE("optgroup", { label: t("settings-for") }, CE("option", { value: -1 }, t("all-games")))), !0);$select.addEventListener("input", (e) => {let id = parseInt($select.value);BxEventBus.Stream.emit("gameSettings.switched", { id });}), this.$streamSettingsSelection = CE("div", {class: "bx-stream-settings-selection bx-gone",_nearby: { orientation: "vertical" }}, CE("div", !1, $select), this.$tips), BxEventBus.Stream.on("xboxTitleId.changed", async ({ id }) => {this.playingGameId = id;let gameSettings = STORAGE.Stream.getGameSettings(id), selectedId = gameSettings && !gameSettings.isEmpty() ? id : -1;setGameIdPref(selectedId);let $optGroup = $select.querySelector("optgroup");while ($optGroup.childElementCount > 1)$optGroup.lastElementChild?.remove();if (id >= 0) {let title = id === 0 ? "Xbox" : await XboxApi.getProductTitle(id);$optGroup.appendChild(CE("option", {value: id}, title));}$select.value = selectedId.toString(), BxEventBus.Stream.emit("gameSettings.switched", { id: selectedId });});}getStreamSettingsSelection() {return this.$streamSettingsSelection;}getTargetGameId() {return this.targetGameId;}} -function onChangeVideoPlayerType() {let playerType = getStreamPref("video.player.type"), settingsManager = SettingsManager.getInstance();if (!settingsManager.hasElement("video.processing")) return;let isDisabled = !1, $videoProcessing = settingsManager.getElement("video.processing"), $videoSharpness = settingsManager.getElement("video.processing.sharpness"), $videoPowerPreference = settingsManager.getElement("video.player.powerPreference"), $videoMaxFps = settingsManager.getElement("video.maxFps"), $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`);if (playerType === "webgl2") $optCas && ($optCas.disabled = !1);else if ($videoProcessing.value = "usm", setStreamPref("video.processing", "usm", "direct"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0;$videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2");} -function limitVideoPlayerFps(targetFps) {STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);} -function updateVideoPlayer() {let streamPlayer = STATES.currentStream.streamPlayer;if (!streamPlayer) return;limitVideoPlayerFps(getStreamPref("video.maxFps"));let options = {processing: getStreamPref("video.processing"),sharpness: getStreamPref("video.processing.sharpness"),saturation: getStreamPref("video.saturation"),contrast: getStreamPref("video.contrast"),brightness: getStreamPref("video.brightness")};streamPlayer.setPlayerType(getStreamPref("video.player.type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer();} -window.addEventListener("resize", updateVideoPlayer); +class SettingsManager {static instance;static getInstance = () => SettingsManager.instance ?? (SettingsManager.instance = new SettingsManager);$streamSettingsSelection;$tips;playingGameId = -1;targetGameId = -1;SETTINGS = {"localCoOp.enabled": {onChange: () => {BxExposed.toggleLocalCoOp(getStreamPref("localCoOp.enabled"));}},"deviceVibration.mode": {onChange: StreamSettings.refreshControllerSettings},"deviceVibration.intensity": {onChange: StreamSettings.refreshControllerSettings},"controller.pollingRate": {onChange: StreamSettings.refreshControllerSettings},"controller.settings": {onChange: StreamSettings.refreshControllerSettings},"nativeMkb.scroll.sensitivityX": {onChange: () => {let value = getStreamPref("nativeMkb.scroll.sensitivityX");NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100);}},"nativeMkb.scroll.sensitivityY": {onChange: () => {let value = getStreamPref("nativeMkb.scroll.sensitivityY");NativeMkbHandler.getInstance()?.setVerticalScrollMultiplier(value / 100);}},"video.player.type": {onChange: updateVideoPlayer,onChangeUi: onChangeVideoPlayerType},"video.player.powerPreference": {onChange: () => {if (!STATES.currentStream.streamPlayerManager) return;updateVideoPlayer();}},"video.processing": {onChange: updateVideoPlayer},"video.processing.sharpness": {onChange: updateVideoPlayer},"video.maxFps": {onChange: () => {let value = getStreamPref("video.maxFps");limitVideoPlayerFps(value);}},"video.ratio": {onChange: updateVideoPlayer},"video.brightness": {onChange: updateVideoPlayer},"video.contrast": {onChange: updateVideoPlayer},"video.saturation": {onChange: updateVideoPlayer},"video.position": {onChange: updateVideoPlayer},"audio.volume": {onChange: () => {let value = getStreamPref("audio.volume");SoundShortcut.setGainNodeVolume(value);}},"stats.items": {onChange: StreamStats.refreshStyles},"stats.quickGlance.enabled": {onChange: () => {let value = getStreamPref("stats.quickGlance.enabled"), streamStats = StreamStats.getInstance();value ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop();}},"stats.position": {onChange: StreamStats.refreshStyles},"stats.textSize": {onChange: StreamStats.refreshStyles},"stats.opacity.all": {onChange: StreamStats.refreshStyles},"stats.opacity.background": {onChange: StreamStats.refreshStyles},"stats.colors": {onChange: StreamStats.refreshStyles},"mkb.p1.preset.mappingId": {onChange: StreamSettings.refreshMkbSettings},"mkb.p1.slot": {onChange: () => {EmulatedMkbHandler.getInstance()?.resetXcloudGamepads();}},"keyboardShortcuts.preset.inGameId": {onChange: StreamSettings.refreshKeyboardShortcuts}};constructor() {BxEventBus.Stream.on("setting.changed", (data) => {if (isStreamPref(data.settingKey)) this.updateStreamElement(data.settingKey);}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => {this.switchGameSettings(id);}), this.renderStreamSettingsSelection();}updateStreamElement(key, onChanges, onChangeUis) {let info = this.SETTINGS[key];if (info.onChangeUi) if (onChangeUis) onChangeUis.add(info.onChangeUi);else info.onChangeUi();if (info.onChange && STATES.isPlaying) if (onChanges) onChanges.add(info.onChange);else info.onChange();let $elm = info.$element;if (!$elm) return;let value = getGamePref(this.targetGameId, key, !0);if ("setValue" in $elm) $elm.setValue(value);else $elm.value = value.toString();this.updateDataset($elm, key);}switchGameSettings(id) {if (setGameIdPref(id), this.targetGameId === id) return;let onChanges = new Set, onChangeUis = new Set, oldGameId = this.targetGameId;this.targetGameId = id;let key;for (key in this.SETTINGS) {if (!isStreamPref(key)) continue;let oldValue = getGamePref(oldGameId, key, !0), newValue = getGamePref(this.targetGameId, key, !0);if (oldValue === newValue) continue;this.updateStreamElement(key, onChanges, onChangeUis);}onChangeUis.forEach((fn) => fn && fn()), onChanges.forEach((fn) => fn && fn()), this.$tips.classList.toggle("bx-gone", id < 0);}setElement(pref, $elm) {if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {};this.updateDataset($elm, pref), this.SETTINGS[pref].$element = $elm;}getElement(pref, params) {if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {};let $elm = this.SETTINGS[pref].$element;if (!$elm) $elm = SettingElement.fromPref(pref, null, params), this.SETTINGS[pref].$element = $elm;return this.updateDataset($elm, pref), $elm;}hasElement(pref) {return !!this.SETTINGS[pref]?.$element;}updateDataset($elm, pref) {if (this.targetGameId === this.playingGameId && hasGamePref(this.playingGameId, pref)) $elm.dataset.override = "true";else delete $elm.dataset.override;}renderStreamSettingsSelection() {this.$tips = CE("p", { class: "bx-gone" }, `⇐ Q ⟶: ${t("reset-highlighted-setting")}`);let $select = BxSelectElement.create(CE("select", !1, CE("optgroup", { label: t("settings-for") }, CE("option", { value: -1 }, t("all-games")))), !0);$select.addEventListener("input", (e) => {let id = parseInt($select.value);BxEventBus.Stream.emit("gameSettings.switched", { id });}), this.$streamSettingsSelection = CE("div", {class: "bx-stream-settings-selection bx-gone",_nearby: { orientation: "vertical" }}, CE("div", !1, $select), this.$tips), BxEventBus.Stream.on("xboxTitleId.changed", async ({ id }) => {this.playingGameId = id;let gameSettings = STORAGE.Stream.getGameSettings(id), selectedId = gameSettings && !gameSettings.isEmpty() ? id : -1;setGameIdPref(selectedId);let $optGroup = $select.querySelector("optgroup");while ($optGroup.childElementCount > 1)$optGroup.lastElementChild?.remove();if (id >= 0) {let title = id === 0 ? "Xbox" : await XboxApi.getProductTitle(id);$optGroup.appendChild(CE("option", {value: id}, title));}$select.value = selectedId.toString(), BxEventBus.Stream.emit("gameSettings.switched", { id: selectedId });});}getStreamSettingsSelection() {return this.$streamSettingsSelection;}getTargetGameId() {return this.targetGameId;}} +function onChangeVideoPlayerType() {let playerType = getStreamPref("video.player.type"), settingsManager = SettingsManager.getInstance();if (!settingsManager.hasElement("video.processing")) return;let isDisabled = !1, $videoProcessing = settingsManager.getElement("video.processing"), $videoSharpness = settingsManager.getElement("video.processing.sharpness"), $videoPowerPreference = settingsManager.getElement("video.player.powerPreference"), $videoMaxFps = settingsManager.getElement("video.maxFps"), $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`);if (playerType === "default") {if ($videoProcessing.value = "usm", setStreamPref("video.processing", "usm", "direct"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0;} else $optCas && ($optCas.disabled = !1);$videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType === "default"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType === "default");} +function limitVideoPlayerFps(targetFps) {STATES.currentStream.streamPlayerManager?.getCanvasPlayer()?.setTargetFps(targetFps);} +function updateVideoPlayer() {let streamPlayerManager = STATES.currentStream.streamPlayerManager;if (!streamPlayerManager) return;limitVideoPlayerFps(getStreamPref("video.maxFps"));let options = {processing: getStreamPref("video.processing"),sharpness: getStreamPref("video.processing.sharpness"),saturation: getStreamPref("video.saturation"),contrast: getStreamPref("video.contrast"),brightness: getStreamPref("video.brightness")};streamPlayerManager.switchPlayerType(getStreamPref("video.player.type")), streamPlayerManager.updateOptions(options), streamPlayerManager.refreshPlayer();} +function resizeVideoPlayer() {STATES.currentStream.streamPlayerManager?.resizePlayer();} +window.addEventListener("resize", resizeVideoPlayer); class NavigationDialog {dialogManager;onMountedCallbacks = [];constructor() {this.dialogManager = NavigationDialogManager.getInstance();}isCancellable() {return !0;}isOverlayVisible() {return !0;}show(configs = {}, clearStack = !1) {if (NavigationDialogManager.getInstance().show(this, configs, clearStack), !this.getFocusedElement()) this.focusIfNeeded();}hide() {NavigationDialogManager.getInstance().hide();}getFocusedElement() {let $activeElement = document.activeElement;if (!$activeElement) return null;if (this.$container.contains($activeElement)) return $activeElement;return null;}onBeforeMount(configs = {}) {}onMounted(configs = {}) {for (let callback of this.onMountedCallbacks)callback.call(this);}onBeforeUnmount() {}onUnmounted() {}handleKeyPress(key) {return !1;}handleGamepad(button) {return !1;}} class NavigationDialogManager {static instance;static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager);LOG_TAG = "NavigationDialogManager";static GAMEPAD_POLLING_INTERVAL = 50;static GAMEPAD_KEYS = [0,1,2,3,12,15,13,14,4,5,6,7,10,11,8,9];static GAMEPAD_DIRECTION_MAP = {12: 1,13: 3,14: 4,15: 2,100: 1,101: 3,102: 4,103: 2};static SIBLING_PROPERTY_MAP = {horizontal: {4: "previousElementSibling",2: "nextElementSibling"},vertical: {1: "previousElementSibling",3: "nextElementSibling"}};gamepadPollingIntervalId = null;gamepadLastStates = [];gamepadHoldingIntervalId = null;$overlay;$container;dialog = null;dialogsStack = [];constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => {e.preventDefault(), e.stopPropagation(), this.dialog?.isCancellable() && this.hide();}), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), new MutationObserver((mutationList) => {if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return;let $dialog = mutationList[0].addedNodes[0];if (!$dialog || !($dialog instanceof HTMLElement)) return;calculateSelectBoxes($dialog);}).observe(this.$container, { childList: !0 });}updateActiveInput(input) {document.documentElement.dataset.activeInput = input;}handleEvent(event) {switch (event.type) {case "keydown":this.updateActiveInput("keyboard");let $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key, handled = this.dialog?.handleKeyPress(keyCode);if (handled) {event.preventDefault(), event.stopPropagation();return;}if (keyCode === "ArrowUp" || keyCode === "ArrowDown") handled = !0, this.focusDirection(keyCode === "ArrowUp" ? 1 : 3);else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") {if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2);} else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") {if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));} else if (keyCode === "Escape") handled = !0, this.hide();if (handled) event.preventDefault(), event.stopPropagation();break;}}isShowing() {return this.$container && !this.$container.classList.contains("bx-gone");}pollGamepad = () => {let gamepads = window.navigator.getGamepads();for (let gamepad of gamepads) {if (!gamepad || !gamepad.connected) continue;if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue;let { axes, buttons } = gamepad, releasedButton = null, heldButton = null, lastState = this.gamepadLastStates[gamepad.index], lastTimestamp, lastKey, lastKeyPressed;if (lastState) [lastTimestamp, lastKey, lastKeyPressed] = lastState;if (lastTimestamp && lastTimestamp === gamepad.timestamp) continue;for (let key of NavigationDialogManager.GAMEPAD_KEYS)if (lastKey === key && !buttons[key].pressed) {releasedButton = key;break;} else if (buttons[key].pressed) {heldButton = key;break;}if (heldButton === null && releasedButton === null && axes && axes.length >= 2) {if (lastKey) {let releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastKey === 102 || lastKey === 103), releasedVertical = Math.abs(axes[1]) < 0.1 && (lastKey === 100 || lastKey === 101);if (releasedHorizontal || releasedVertical) releasedButton = lastKey;else heldButton = lastKey;} else if (axes[0] < -0.5) heldButton = 102;else if (axes[0] > 0.5) heldButton = 103;else if (axes[1] < -0.5) heldButton = 100;else if (axes[1] > 0.5) heldButton = 101;}if (heldButton !== null) {if (this.gamepadLastStates[gamepad.index] = [gamepad.timestamp, heldButton, !1], this.clearGamepadHoldingInterval(), NavigationDialogManager.GAMEPAD_DIRECTION_MAP[heldButton]) this.gamepadHoldingIntervalId = window.setInterval(() => {let lastState2 = this.gamepadLastStates[gamepad.index];if (lastState2) {if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) {this.handleGamepad(gamepad, heldButton);return;}}this.clearGamepadHoldingInterval();}, 100);continue;}if (releasedButton === null) {this.clearGamepadHoldingInterval();continue;}if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return;if (this.updateActiveInput("gamepad"), this.handleGamepad(gamepad, releasedButton)) return;if (releasedButton === 0) {document.activeElement?.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));return;} else if (releasedButton === 1) {this.hide();return;}}};handleGamepad(gamepad, key) {let handled = this.dialog?.handleGamepad(key);if (handled) return !0;let direction = NavigationDialogManager.GAMEPAD_DIRECTION_MAP[key];if (!direction) return !1;if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === "range") {let $range = document.activeElement;if (direction === 4 || direction === 2) {let $numberStepper = $range.closest(".bx-number-stepper");if ($numberStepper) BxNumberStepper.change.call($numberStepper, direction === 4 ? "dec" : "inc");else $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === 4 ? -1 : 1)).toString(), $range.dispatchEvent(new InputEvent("input"));handled = !0;}}if (!handled) this.focusDirection(direction);return this.gamepadLastStates[gamepad.index] && (this.gamepadLastStates[gamepad.index][2] = !0), !0;}clearGamepadHoldingInterval() {this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = null;}show(dialog, configs = {}, clearStack = !1) {this.clearGamepadHoldingInterval(), BxEventBus.Script.emit("dialog.shown", {}), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.unmountCurrentDialog(), this.dialogsStack.push(dialog), this.dialog = dialog, dialog.onBeforeMount(configs), this.$container.appendChild(dialog.getContent()), dialog.onMounted(configs), this.$overlay.classList.remove("bx-gone"), this.$overlay.classList.toggle("bx-invisible", !dialog.isOverlayVisible()), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling();}hide() {if (this.clearGamepadHoldingInterval(), !this.isShowing()) return;if (document.body.classList.remove("bx-no-scroll"), BxEventBus.Script.emit("dialog.dismissed", {}), this.$overlay.classList.add("bx-gone"), this.$overlay.classList.remove("bx-invisible"), this.$container.classList.add("bx-gone"), this.$container.removeEventListener("keydown", this), this.stopGamepadPolling(), this.dialog) {let dialogIndex = this.dialogsStack.indexOf(this.dialog);if (dialogIndex > -1) this.dialogsStack = this.dialogsStack.slice(0, dialogIndex);}if (this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1, this.dialogsStack.length) this.dialogsStack[this.dialogsStack.length - 1].show();}focus($elm) {if (!$elm) return !1;if ($elm.nearby && $elm.nearby.focus) if ($elm.nearby.focus instanceof HTMLElement) return this.focus($elm.nearby.focus);else return $elm.nearby.focus();return $elm.focus(), $elm === document.activeElement;}getOrientation($elm) {let nearby = $elm.nearby || {};if (nearby.selfOrientation) return nearby.selfOrientation;let orientation, $current = $elm.parentElement;while ($current !== this.$container) {let tmp = $current.nearby?.orientation;if ($current.nearby && tmp) {orientation = tmp;break;}$current = $current.parentElement;}return orientation = orientation || "vertical", setNearby($elm, {selfOrientation: orientation}), orientation;}findNextTarget($focusing, direction, checkParent = !1, checked = []) {if (!$focusing || $focusing === this.$container) return null;if (checked.includes($focusing)) return null;checked.push($focusing);let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target);if (nearby[1] && direction === 1) return nearby[1];else if (nearby[3] && direction === 3) return nearby[3];else if (nearby[4] && direction === 4) return nearby[4];else if (nearby[2] && direction === 2) return nearby[2];let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction];if (siblingProperty) {let $sibling = $target;while ($sibling[siblingProperty]) {$sibling = $sibling[siblingProperty];let $focusable = this.findFocusableElement($sibling, direction);if ($focusable) return $focusable;}}if (nearby.loop) {if (nearby.loop(direction)) return null;}if (checkParent) return this.findNextTarget($parent, direction, checkParent, checked);return null;}findFocusableElement($elm, direction) {if (!$elm) return null;if (!!$elm.disabled) return null;if (!isElementVisible($elm)) return null;if ($elm.tabIndex > -1) return $elm;let focus = $elm.nearby?.focus;if (focus) {if (focus instanceof HTMLElement) return this.findFocusableElement(focus, direction);else if (typeof focus === "function") {if (focus()) return document.activeElement;}}let children = Array.from($elm.children), orientation = $elm.nearby?.orientation || "vertical";if (orientation === "horizontal" || orientation === "vertical" && direction === 1) children.reverse();for (let $child of children) {if (!$child || !($child instanceof HTMLElement)) return null;let $target = this.findFocusableElement($child, direction);if ($target) return $target;}return null;}startGamepadPolling() {this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);}stopGamepadPolling() {this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null;}focusDirection(direction) {let dialog = this.dialog;if (!dialog) return;let $focusing = dialog.getFocusedElement();if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null;let $target = this.findNextTarget($focusing, direction, !0);this.focus($target);}unmountCurrentDialog() {let dialog = this.dialog;dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null;}} var LOG_TAG = "TouchController"; @@ -210,7 +211,8 @@ class MkbMappingManagerDialog extends BaseProfileManagerDialog {static instance; 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","xhome.enabled"]}, {group: "server",label: t("server"),items: [{pref: "server.region",multiLines: !0},{pref: "stream.locale",multiLines: !0},"server.ipv6.prefer"]}, {group: "stream",label: t("stream"),items: ["stream.video.resolution","stream.video.codecProfile","stream.video.maxBitrate","audio.volume.booster.enabled","screenshot.applyFilters","audio.mic.onPlaying","game.fortnite.forceConsole","stream.video.combineAudio"]}, {requiredVariants: "full",group: "mkb",label: t("mouse-and-keyboard"),items: ["nativeMkb.mode",{pref: "nativeMkb.forcedGames",multiLines: !0,note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/574", target: "_blank" }, t("unofficial-game-list"))},"mkb.enabled","mkb.cursor.hideIdle"],...!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? {unsupported: !0,unsupportedNote: CE("a", {href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657",target: "_blank"}, "⚠️ " + t("browser-unsupported-feature"))} : {}}, {requiredVariants: "full",group: "touch-control",label: t("touch-controller"),items: [{pref: "touchController.mode",note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/241", target: "_blank" }, t("unofficial-game-list"))},"touchController.autoOff","touchController.opacity.default","touchController.style.standard","touchController.style.custom"],...!STATES.userAgent.capabilities.touch ? {unsupported: !0,unsupportedNote: "⚠️ " + t("device-unsupported-touch")} : {}}, {group: "ui",label: t("ui"),items: ["ui.layout","ui.imageQuality","ui.gameCard.waitTime.show","ui.controllerStatus.show","ui.streamMenu.simplify","ui.splashVideo.skip",!AppInterface && "ui.hideScrollbar","ui.systemMenu.hideHandle","ui.feedbackDialog.disabled","ui.reduceAnimations",{pref: "ui.hideSections",multiLines: !0},{pref: "block.features",multiLines: !0}]}, {requiredVariants: "full",group: "game-bar",label: t("game-bar"),items: ["gameBar.position"]}, {group: "loading-screen",label: t("loading-screen"),items: ["loadingScreen.gameArt.show","loadingScreen.waitTime.show","loadingScreen.rocket"]}, {group: "other",label: t("other"),items: ["block.tracking"]}, {group: "advanced",label: t("advanced"),items: [{pref: "userAgent.profile",multiLines: !0,onCreated: (setting, $control) => {let defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", {type: "text",placeholder: defaultUserAgent,autocomplete: "off",class: "bx-settings-custom-user-agent",tabindex: 0});$inpCustomUserAgent.addEventListener("input", (e) => {let profile = $control.value, custom = e.target.value.trim();UserAgent.updateStorage(profile, custom), this.onGlobalSettingChanged(e);}), $control.insertAdjacentElement("afterend", $inpCustomUserAgent), setNearby($inpCustomUserAgent.parentElement, {orientation: "vertical"});}}]}, {group: "footer",items: [($parent) => {try {let appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10);$parent.appendChild(CE("div", {class: "bx-settings-app-version"}, `xCloud website version ${appVersion} (${appDate})`));} catch (e) {}},($parent) => {$parent.appendChild(CE("a", {class: "bx-donation-link",href: "https://ko-fi.com/redphx",target: "_blank",tabindex: 0}, `❤️ ${t("support-better-xcloud")}`));},($parent) => {$parent.appendChild(createButton({label: t("clear-data"),style: 8 | 128 | 64,onClick: (e) => {if (confirm(t("clear-data-confirm"))) clearAllData();}}));},($parent) => {$parent.appendChild(CE("div", { class: "bx-debug-info" }, createButton({label: "Debug info",style: 8 | 128 | 64,onClick: (e) => {let $button = e.target.closest("button");if (!$button) return;let $pre = $button.nextElementSibling;if (!$pre) {let debugInfo = deepClone(BX_FLAGS.DeviceInfo);debugInfo.settings = JSON.parse(window.localStorage.getItem("BetterXcloud") || "{}"), $pre = CE("pre", {class: "bx-focusable bx-gone",tabindex: 0,_on: {click: async (e2) => {await copyToClipboard(e2.target.innerText);}}}, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```"), $button.insertAdjacentElement("afterend", $pre);}$pre.classList.toggle("bx-gone"), $pre.scrollIntoView();}})));}]}];TAB_DISPLAY_ITEMS = [{requiredVariants: "full",group: "audio",label: t("audio"),helpUrl: "https://better-xcloud.github.io/ingame-features/#audio",items: [{pref: "audio.volume",params: {disabled: !getGlobalPref("audio.volume.booster.enabled")},onCreated: (setting, $elm) => {let $range = $elm.querySelector("input[type=range");BxEventBus.Stream.on("setting.changed", (payload) => {let { settingKey } = payload;if (settingKey === "audio.volume") $range.value = getStreamPref(settingKey).toString(), BxEvent.dispatch($range, "input", { ignoreOnChange: !0 });});}}]}, {group: "video",label: t("video"),helpUrl: "https://better-xcloud.github.io/ingame-features/#video",items: ["video.player.type","video.maxFps","video.player.powerPreference","video.processing","video.ratio","video.position","video.processing.sharpness","video.saturation","video.contrast","video.brightness"]}];TAB_CONTROLLER_ITEMS = [{group: "controller",label: t("controller"),helpUrl: "https://better-xcloud.github.io/ingame-features/#controller",items: ["localCoOp.enabled","controller.pollingRate",($parent) => {$parent.appendChild(ControllerExtraSettings.renderSettings.apply(this));}]},STATES.userAgent.capabilities.touch && {group: "touch-control",label: t("touch-controller"),items: [{label: t("layout"),content: CE("select", {disabled: !0}, CE("option", !1, t("default"))),onCreated: (setting, $elm) => {$elm.addEventListener("input", (e) => {TouchController.applyCustomLayout($elm.value, 1000);}), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => {let customLayouts = TouchController.getCustomLayouts();while ($elm.firstChild)$elm.removeChild($elm.firstChild);if ($elm.disabled = !customLayouts, !customLayouts) {$elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("input"));return;}let $fragment = document.createDocumentFragment();for (let key in customLayouts.layouts) {let layout = customLayouts.layouts[key], name;if (layout.author) name = `${layout.name} (${layout.author})`;else name = layout.name;let $option = CE("option", { value: key }, name);$fragment.appendChild($option);}$elm.appendChild($fragment), $elm.value = customLayouts.default_layout;});}}]},STATES.browser.capabilities.deviceVibration && {group: "device",label: t("device"),items: [{pref: "deviceVibration.mode",multiLines: !0,unsupported: !STATES.browser.capabilities.deviceVibration}, {pref: "deviceVibration.intensity",unsupported: !STATES.browser.capabilities.deviceVibration}]}];TAB_MKB_ITEMS = [{requiredVariants: "full",group: "mkb",label: t("mouse-and-keyboard"),helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/",items: [($parent) => {$parent.appendChild(MkbExtraSettings.renderSettings.apply(this));}]},NativeMkbHandler.isAllowed() && {requiredVariants: "full",group: "native-mkb",label: t("native-mkb"),items: ["nativeMkb.scroll.sensitivityY","nativeMkb.scroll.sensitivityX"]}];TAB_STATS_ITEMS = [{group: "stats",label: t("stream-stats"),helpUrl: "https://better-xcloud.github.io/stream-stats/",items: ["stats.showWhenPlaying","stats.quickGlance.enabled","stats.items","stats.position","stats.textSize","stats.opacity.all","stats.opacity.background","stats.colors"]}];SETTINGS_UI = {global: {group: "global",icon: BxIcon.HOME,items: this.TAB_GLOBAL_ITEMS},stream: {group: "stream",icon: BxIcon.DISPLAY,items: this.TAB_DISPLAY_ITEMS},controller: {group: "controller",icon: BxIcon.CONTROLLER,items: this.TAB_CONTROLLER_ITEMS,requiredVariants: "full"},mkb: {group: "mkb",icon: BxIcon.NATIVE_MKB,items: this.TAB_MKB_ITEMS,requiredVariants: "full"},stats: {group: "stats",icon: BxIcon.STREAM_STATS,items: this.TAB_STATS_ITEMS}};constructor() {super();BxLogger.info(this.LOG_TAG, "constructor()"), this.boundOnContextMenu = this.onContextMenu.bind(this), this.settingsManager = SettingsManager.getInstance(), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(), this.onMountedCallbacks.push(() => {if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);let $selectUserAgent = document.querySelector(`#bx_setting_${escapeCssSelector("userAgent.profile")}`);if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1;}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => {this.$tabContents.dataset.gameId = id.toString();});}getDialog() {return this;}getContent() {return this.$container;}onMounted() {super.onMounted();}isOverlayVisible() {return !STATES.isPlaying;}reloadPage() {this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload();}isSupportedVariant(requiredVariants) {if (typeof requiredVariants === "undefined") return !0;return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT);}onTabClicked = (e) => {let $svg = e.target.closest("svg"), $child, children = Array.from(this.$tabContents.children);for ($child of children)if ($child.dataset.tabGroup === $svg.dataset.group) $child.classList.remove("bx-gone"), calculateSelectBoxes($child);else if ($child.dataset.tabGroup) $child.classList.add("bx-gone");this.$streamSettingsSelection.classList.toggle("bx-gone", $svg.dataset.group === "global");for (let $child2 of Array.from(this.$tabs.children))$child2.classList.remove("bx-active");$svg.classList.add("bx-active");};renderTab(settingTab) {let $svg = createSvgIcon(settingTab.icon);return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", this.onTabClicked), $svg;}onGlobalSettingChanged = (e) => {PatcherCache.getInstance().clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger");};onContextMenu(e) {e.preventDefault();let $elm = e.target;$elm instanceof HTMLElement && this.resetHighlightedSetting($elm);}renderServerSetting(setting) {let selectedValue = getGlobalPref("server.region"), continents = {"america-north": {label: t("continent-north-america")},"america-south": {label: t("continent-south-america")},asia: {label: t("continent-asia")},australia: {label: t("continent-australia")},europe: {label: t("continent-europe")},other: {label: t("other")}}, $control = CE("select", {id: `bx_setting_${escapeCssSelector(setting.pref)}`,tabindex: 0});$control.name = $control.id, $control.addEventListener("input", (e) => {setGlobalPref(setting.pref, e.target.value, "ui"), this.onGlobalSettingChanged(e);}), setting.options = {};for (let regionName in STATES.serverRegions) {let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`;if (region.isDefault) {if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default";}setting.options[value] = label;let $option = CE("option", { value }, label), continent = continents[region.contintent];if (!continent.children) continent.children = [];continent.children.push($option);}let fragment = document.createDocumentFragment(), key;for (key in continents) {let continent = continents[key];if (!continent.children) continue;fragment.appendChild(CE("optgroup", {label: continent.label}, ...continent.children));}return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;}renderSettingRow(settingTab, $tabContent, settingTabContent, setting) {if (typeof setting === "string") setting = {pref: setting};let pref = setting.pref, $control;if (setting.content) if (typeof setting.content === "function") $control = setting.content.apply(this);else $control = setting.content;else if (!setting.unsupported) {if (pref === "server.region") $control = this.renderServerSetting(setting);else if (pref === "bx.locale") $control = SettingElement.fromPref(pref, async (e) => {let newLocale = e.target.value;if (getGlobalPref("ui.controllerFriendly")) {let timeoutId = e.target.timeoutId;timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => {Translations.refreshLocale(newLocale), Translations.updateTranslations();}, 500);} else Translations.refreshLocale(newLocale), Translations.updateTranslations();this.onGlobalSettingChanged(e);});else if (pref === "userAgent.profile") $control = SettingElement.fromPref("userAgent.profile", (e) => {let value = e.target.value, isCustom = value === "custom", userAgent2 = UserAgent.get(value);UserAgent.updateStorage(value);let $inp = $control.nextElementSibling;$inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e);});else if ($control = this.settingsManager.getElement(pref, setting.params), settingTab.group === "global") $control.addEventListener("input", this.onGlobalSettingChanged);if ($control instanceof HTMLSelectElement) $control = BxSelectElement.create($control);}let prefDefinition = null;if (pref) prefDefinition = getPrefInfo(pref).definition;if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return;let label = prefDefinition?.label || setting.label || "", note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental;if (typeof note === "function") note = note();if (typeof unsupportedNote === "function") unsupportedNote = unsupportedNote();if (settingTabContent.label && setting.pref) {if (prefDefinition?.suggest) typeof prefDefinition.suggest.lowest !== "undefined" && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest), typeof prefDefinition.suggest.highest !== "undefined" && (this.suggestedSettings.highest[setting.pref] = prefDefinition.suggest.highest);}if (experimental) if (label = "🧪 " + label, !note) note = t("experimental");else note = `${t("experimental")}: ${note}`;let $note;if (unsupportedNote) $note = CE("div", { class: "bx-settings-dialog-note" }, unsupportedNote);else if (note) $note = CE("div", { class: "bx-settings-dialog-note" }, note);let $row = createSettingRow(label, !prefDefinition?.unsupported && $control, {$note,multiLines: setting.multiLines,icon: prefDefinition?.labelIcon,onContextMenu: this.boundOnContextMenu,pref});if (pref) $row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`;$row.dataset.type = settingTabContent.group, $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control);}renderSettingsSection(settingTab, sections) {let $tabContent = CE("div", {class: "bx-gone",_dataset: {tabGroup: settingTab.group}});for (let section of sections) {if (!section) continue;if (section instanceof HTMLElement) {$tabContent.appendChild(section);continue;}if (!this.isSupportedVariant(section.requiredVariants)) continue;if (!this.renderFullSettings && settingTab.group === "global" && section.group !== "general" && section.group !== "footer") continue;let label = section.label;if (label === t("better-xcloud")) {if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)";label = createButton({label,url: "https://github.com/redphx/better-xcloud/releases",style: 4096 | 16 | 64});}if (label) {let $title = CE("h2", {_nearby: {orientation: "horizontal"}}, CE("span", !1, label), section.helpUrl && createButton({icon: BxIcon.QUESTION,style: 8 | 64,url: section.helpUrl,title: t("help")}));$tabContent.appendChild($title);}if (section.unsupportedNote) {let $note = CE("b", { class: "bx-note-unsupported" }, section.unsupportedNote);$tabContent.appendChild($note);}if (section.unsupported) continue;if (section.content) {$tabContent.appendChild(section.content);continue;}section.items = section.items || [];for (let setting of section.items) {if (setting === !1) continue;if (typeof setting === "function") {setting.apply(this, [$tabContent]);continue;}this.renderSettingRow(settingTab, $tabContent, section, setting);}}return $tabContent;}setupDialog() {let $tabs, $tabContents, $container = CE("div", {class: "bx-settings-dialog",_nearby: {orientation: "horizontal"}}, CE("div", {class: "bx-settings-tabs-container",_nearby: {orientation: "vertical",focus: () => {return this.dialogManager.focus($tabs);},loop: (direction) => {if (direction === 1 || direction === 3) return this.focusVisibleTab(direction === 1 ? "last" : "first"), !0;return !1;}}}, $tabs = CE("div", {class: "bx-settings-tabs bx-hide-scroll-bar",_nearby: {focus: () => this.focusActiveTab()}}), CE("div", !1, this.$btnReload = createButton({icon: BxIcon.REFRESH,style: 64 | 32,onClick: (e) => {this.reloadPage();}}), createButton({icon: BxIcon.CLOSE,style: 64 | 32,onClick: (e) => {this.dialogManager.hide();}}))), CE("div", {class: "bx-settings-tab-contents",_nearby: {orientation: "vertical",loop: (direction) => {if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0;return !1;}}}, this.$streamSettingsSelection = SettingsManager.getInstance().getStreamSettingsSelection(), $tabContents = CE("div", {class: "bx-settings-tab-content",_nearby: {orientation: "vertical",focus: () => this.jumpToSettingGroup("next")}})));this.$container = $container, this.$tabs = $tabs, this.$tabContents = $tabContents, $container.addEventListener("click", (e) => {if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide();});let settingTabGroup;for (settingTabGroup in this.SETTINGS_UI) {let settingTab = this.SETTINGS_UI[settingTabGroup];if (!settingTab) continue;if (!this.isSupportedVariant(settingTab.requiredVariants)) continue;if (settingTab.group !== "global" && !this.renderFullSettings) continue;let $svg = this.renderTab(settingTab);$tabs.appendChild($svg);let $tabContent = this.renderSettingsSection.call(this, settingTab, settingTab.items);$tabContents.appendChild($tabContent);}$tabs.firstElementChild.dispatchEvent(new Event("click"));}focusTab(tabId) {let $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`);$tab && $tab.dispatchEvent(new Event("click"));}focusIfNeeded() {this.jumpToSettingGroup("next");}focusActiveTab() {let $currentTab = this.$tabs.querySelector(".bx-active");return $currentTab && $currentTab.focus(), !0;}focusVisibleSetting(type = "first") {let controls = Array.from(this.$tabContents.querySelectorAll("div[data-tab-group]:not(.bx-gone) > *"));if (!controls.length) return !1;if (type === "last") controls.reverse();for (let $control of controls) {if (!($control instanceof HTMLElement)) continue;let $focusable = this.dialogManager.findFocusableElement($control);if ($focusable) {if (this.dialogManager.focus($focusable)) return !0;}}return !1;}focusVisibleTab(type = "first") {let tabs = Array.from(this.$tabs.querySelectorAll("svg:not(.bx-gone)"));if (!tabs.length) return !1;if (type === "last") tabs.reverse();for (let $tab of tabs)if (this.dialogManager.focus($tab)) return !0;return !1;}jumpToSettingGroup(direction) {let $tabContent = this.$tabContents.querySelector("div[data-tab-group]:not(.bx-gone)");if (!$tabContent) return !1;let $header, $focusing = document.activeElement;if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2");else {let $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling", $tmp = $parent, times = 0;while (!0) {if (!$tmp) break;if ($tmp.tagName === "H2") {if ($header = $tmp, !$tmp.nextElementSibling?.classList.contains("bx-note-unsupported")) {if (++times, direction === "next" || times >= 2) break;}}$tmp = $tmp[siblingProperty];}}let $target;if ($header) $target = this.dialogManager.findNextTarget($header, 3, !1);if ($target) return this.dialogManager.focus($target);return !1;}resetHighlightedSetting($elm) {let targetGameId = SettingsManager.getInstance().getTargetGameId();if (targetGameId < 0) return;if (!$elm) $elm = document.activeElement instanceof HTMLElement ? document.activeElement : void 0;let $row = $elm?.closest("div[data-tab-group] > .bx-settings-row");if (!$row) return;let pref = $row.prefKey;if (!pref) alert("Pref not found: " + $row.id);if (!isStreamPref(pref)) return;let deleted = STORAGE.Stream.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, streamPlayer = currentStream.streamPlayer, $canvas = this.$canvas;if (!streamPlayer || !$canvas) return;let $player;if (getGlobalPref("screenshot.applyFilters")) $player = streamPlayer.getPlayerElement();else $player = streamPlayer.getPlayerElement("default");if (!$player || !$player.isConnected) return;let $gameStream = $player.closest("#game-stream");if ($gameStream) $gameStream.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $gameStream.classList.add("bx-taking-screenshot");let canvasContext = this.canvasContext;if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().forceDrawFrame();if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) {let data = $canvas.toDataURL("image/png").split(";base64,")[1];AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();return;}$canvas.toBlob((blob) => {if (!blob) return;let now = +new Date, $download = this.$download;$download.download = `${currentStream.titleSlug}-${now}.png`, $download.href = URL.createObjectURL(blob), $download.click(), URL.revokeObjectURL($download.href), $download.href = "", $download.download = "", canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();}, "image/png");}} +class BaseStreamPlayer {logTag;playerType;elementType;$video;options = {processing: "usm",sharpness: 0,brightness: 1,contrast: 1,saturation: 1};isStopped = !1;constructor(playerType, elementType, $video, logTag) {this.playerType = playerType, this.elementType = elementType, this.$video = $video, this.logTag = logTag;}init() {BxLogger.info(this.logTag, "Initialize");}updateOptions(newOptions, refresh = !1) {this.options = Object.assign(this.options, newOptions), refresh && this.refreshPlayer();}} +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 $gameStream = $player.closest("#game-stream");if ($gameStream) $gameStream.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $gameStream.classList.add("bx-taking-screenshot");let canvasContext = this.canvasContext;if ($player instanceof HTMLCanvasElement) streamPlayerManager.getCanvasPlayer()?.forceDrawFrame();if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) {let data = $canvas.toDataURL("image/png").split(";base64,")[1];AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();return;}$canvas.toBlob((blob) => {if (!blob) return;let now = +new Date, $download = this.$download;$download.download = `${currentStream.titleSlug}-${now}.png`, $download.href = URL.createObjectURL(blob), $download.click(), URL.revokeObjectURL($download.href), $download.href = "", $download.download = "", canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();}, "image/png");}} class RendererShortcut {static toggleVisibility() {let $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]');if (!$mediaContainer) {BxEventBus.Stream.emit("video.visibility.changed", { isVisible: !0 });return;}$mediaContainer.classList.toggle("bx-gone");let isVisible = !$mediaContainer.classList.contains("bx-gone");limitVideoPlayerFps(isVisible ? getStreamPref("video.maxFps") : 0), BxEventBus.Stream.emit("video.visibility.changed", { isVisible });}} class TrueAchievements {static instance;static getInstance = () => TrueAchievements.instance ?? (TrueAchievements.instance = new TrueAchievements);LOG_TAG = "TrueAchievements";$link;$button;$hiddenLink;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$link = createButton({label: t("true-achievements"),url: "#",icon: BxIcon.TRUE_ACHIEVEMENTS,style: 64 | 8 | 128 | 8192,onClick: this.onClick}), this.$button = createButton({label: t("true-achievements"),title: t("true-achievements"),icon: BxIcon.TRUE_ACHIEVEMENTS,style: 64,onClick: this.onClick}), this.$hiddenLink = CE("a", {target: "_blank"});}onClick = (e) => {e.preventDefault(), window.BX_EXPOSED.dialogRoutes?.closeAll();let dataset = this.$link.dataset;this.open(!0, dataset.xboxTitleId, dataset.id);};updateIds(xboxTitleId, id) {let $link = this.$link, $button = this.$button;if (clearDataSet($link), clearDataSet($button), xboxTitleId) $link.dataset.xboxTitleId = xboxTitleId, $button.dataset.xboxTitleId = xboxTitleId;if (id) $link.dataset.id = id, $button.dataset.id = id;}injectAchievementsProgress($elm) {if (SCRIPT_VARIANT !== "full") return;let $parent = $elm.parentElement, $div = CE("div", {class: "bx-guide-home-achievements-progress"}, $elm), xboxTitleId;try {let $container = $parent.closest("div[class*=AchievementsPreview-module__container]");if ($container) xboxTitleId = getReactProps($container).children.props.data.data.xboxTitleId;} catch (e) {}if (!xboxTitleId) xboxTitleId = this.getStreamXboxTitleId();if (typeof xboxTitleId !== "undefined") xboxTitleId = xboxTitleId.toString();if (this.updateIds(xboxTitleId), document.body.dataset.mediaType === "tv") $div.appendChild(this.$link);else $div.appendChild(this.$button);$parent.appendChild($div);}injectAchievementDetailPage($parent) {if (SCRIPT_VARIANT !== "full") return;let props = getReactProps($parent);if (!props) return;try {let achievementList = props.children.props.data.data, $header = $parent.querySelector("div[class*=AchievementDetailHeader]"), achievementName = getReactProps($header).children[0].props.achievementName, id, xboxTitleId;for (let achiev of achievementList)if (achiev.name === achievementName) {id = achiev.id, xboxTitleId = achiev.title.id;break;}if (id) this.updateIds(xboxTitleId, id), $parent.appendChild(this.$link);} catch (e) {}}getStreamXboxTitleId() {return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId;}open(override, xboxTitleId, id) {if (!xboxTitleId || xboxTitleId === "undefined") xboxTitleId = this.getStreamXboxTitleId();if (AppInterface && AppInterface.openTrueAchievementsLink) {AppInterface.openTrueAchievementsLink(override, xboxTitleId?.toString(), id?.toString());return;}let url = "https://www.trueachievements.com";if (xboxTitleId) {if (url += `/deeplink/${xboxTitleId}`, id) url += `/${id}`;}this.$hiddenLink.href = url, this.$hiddenLink.click();}} class VirtualControllerShortcut {static pressXboxButton() {let streamSession = window.BX_EXPOSED.streamSession;if (!streamSession) return;let released = generateVirtualControllerMapping(0), pressed = generateVirtualControllerMapping(0, {Nexus: 1,VirtualPhysicality: 1024});streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [pressed]), setTimeout(() => {streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [released]);}, 100);}} @@ -306,9 +308,73 @@ color = saturation != 1.0 ? mix(vec3(dot(color, LUMINOSITY_FACTOR)), color, satu color = contrast * (color - 0.5) + 0.5; color = brightness * color; fragColor = vec4(color, 1.0);}`; -class WebGL2Player {LOG_TAG = "WebGL2Player";$video;$canvas;gl = null;resources = [];program = null;stopped = !1;options = {filterId: 1,sharpenFactor: 0,brightness: 0,contrast: 0,saturation: 0};targetFps = 60;frameInterval = 0;lastFrameTime = 0;animFrameId = null;constructor($video) {BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video;let $canvas = document.createElement("canvas");$canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas);}setFilter(filterId, update = !0) {this.options.filterId = filterId, update && this.updateCanvas();}setSharpness(sharpness, update = !0) {this.options.sharpenFactor = sharpness, update && this.updateCanvas();}setBrightness(brightness, update = !0) {this.options.brightness = 1 + (brightness - 100) / 100, update && this.updateCanvas();}setContrast(contrast, update = !0) {this.options.contrast = 1 + (contrast - 100) / 100, update && this.updateCanvas();}setSaturation(saturation, update = !0) {this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas();}setTargetFps(target) {this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0;}getCanvas() {return this.$canvas;}updateCanvas() {let gl = this.gl, program = this.program;gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation);}forceDrawFrame() {let gl = this.gl;gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);}setupRendering() {let frameCallback;if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {let $video = this.$video;frameCallback = $video.requestVideoFrameCallback.bind($video);} else frameCallback = requestAnimationFrame;let animate = () => {if (this.stopped) return;this.animFrameId = frameCallback(animate);let draw = !0;if (this.targetFps === 0) draw = !1;else if (this.targetFps < 60) {let currentTime = performance.now();if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1;else this.lastFrameTime = currentTime;}if (draw) {let gl = this.gl;gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);}};this.animFrameId = frameCallback(animate);}setupShaders() {BxLogger.info(this.LOG_TAG, "Setting up", getStreamPref("video.player.powerPreference"));let gl = this.$canvas.getContext("webgl2", {isBx: !0,antialias: !0,alpha: !1,powerPreference: getStreamPref("video.player.powerPreference")});this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);let vShader = gl.createShader(gl.VERTEX_SHADER);gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader);let fShader = gl.createShader(gl.FRAGMENT_SHADER);gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader);let program = gl.createProgram();if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`);this.updateCanvas();let buffer = gl.createBuffer();this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);let texture = gl.createTexture();this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0);}resume() {this.stop(), this.stopped = !1, BxLogger.info(this.LOG_TAG, "Resume"), this.$canvas.classList.remove("bx-gone"), this.setupRendering();}stop() {if (BxLogger.info(this.LOG_TAG, "Stop"), this.$canvas.classList.add("bx-gone"), this.stopped = !0, this.animFrameId) {if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId);else cancelAnimationFrame(this.animFrameId);this.animFrameId = null;}}destroy() {BxLogger.info(this.LOG_TAG, "Destroy"), this.stop();let gl = this.gl;if (gl) {gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null);for (let resource of this.resources)if (resource instanceof WebGLProgram) gl.deleteProgram(resource);else if (resource instanceof WebGLShader) gl.deleteShader(resource);else if (resource instanceof WebGLTexture) gl.deleteTexture(resource);else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource);this.gl = null;}if (this.$canvas.isConnected) this.$canvas.parentElement?.removeChild(this.$canvas);this.$canvas.width = 1, this.$canvas.height = 1;}} -class StreamPlayer {$video;playerType = "default";options = {};webGL2Player = null;$videoCss = null;$usmMatrix = null;constructor($video, type, options) {this.setupVideoElements(), this.$video = $video, this.options = options || {}, this.setPlayerType(type);}setupVideoElements() {if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) return;let $fragment = document.createDocumentFragment();this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss);let $svg = CE("svg", {id: "bx-video-filters",xmlns: "http://www.w3.org/2000/svg",class: "bx-gone"}, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", {id: "bx-filter-usm",xmlns: "http://www.w3.org/2000/svg"}, this.$usmMatrix = CE("feConvolveMatrix", {id: "bx-filter-usm-matrix",order: "3",xmlns: "http://www.w3.org/2000/svg"}))));$fragment.appendChild($svg), document.documentElement.appendChild($fragment);}getVideoPlayerFilterStyle() {let filters = [], sharpness = this.options.sharpness || 0;if (this.options.processing === "usm" && sharpness != 0) {let matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`;this.$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)");}let saturation = this.options.saturation || 100;if (saturation != 100) filters.push(`saturate(${saturation}%)`);let contrast = this.options.contrast || 100;if (contrast != 100) filters.push(`contrast(${contrast}%)`);let brightness = this.options.brightness || 100;if (brightness != 100) filters.push(`brightness(${brightness}%)`);return filters.join(" ");}resizePlayer() {let PREF_RATIO = getStreamPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas;if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas();let targetWidth, targetHeight, targetObjectFit;if (PREF_RATIO.includes(":")) {let tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]), width = 0, height = 0, parentRect = $video.parentElement.getBoundingClientRect();if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio;else width = parentRect.width, height = width / videoRatio;width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString();let $parent = $video.parentElement, position = getStreamPref("video.position");if ($parent.style.removeProperty("padding-top"), $parent.dataset.position = position, position === "top-half" || position === "bottom-half") {let padding = Math.floor((window.innerHeight - height) / 4);if (padding > 0) {if (position === "bottom-half") padding *= 3;$parent.style.paddingTop = padding + "px";}}targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill";} else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString();if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit, $video.dispatchEvent(new Event("resize"));if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions();}setPlayerType(type, refreshPlayer = !1) {if (this.playerType !== type) {let videoClass = BX_FLAGS.DeviceInfo.deviceType === "android-tv" ? "bx-pixel" : "bx-gone";if (type === "webgl2") {if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video);else this.webGL2Player.resume();this.$videoCss.textContent = "", this.$video.classList.add(videoClass);} else this.webGL2Player?.stop(), this.$video.classList.remove(videoClass);}this.playerType = type, refreshPlayer && this.refreshPlayer();}setOptions(options, refreshPlayer = !1) {this.options = options, refreshPlayer && this.refreshPlayer();}updateOptions(options, refreshPlayer = !1) {this.options = Object.assign(this.options, options), refreshPlayer && this.refreshPlayer();}getPlayerElement(playerType) {if (typeof playerType === "undefined") playerType = this.playerType;if (playerType === "webgl2") return this.webGL2Player?.getCanvas();return this.$video;}getWebGL2Player() {return this.webGL2Player;}refreshPlayer() {if (this.playerType === "webgl2") {let options = this.options, webGL2Player = this.webGL2Player;if (options.processing === "usm") webGL2Player.setFilter(1);else webGL2Player.setFilter(2);ScreenshotManager.getInstance().updateCanvasFilters("none"), webGL2Player.setSharpness(options.sharpness || 0), webGL2Player.setSaturation(options.saturation || 100), webGL2Player.setContrast(options.contrast || 100), webGL2Player.setBrightness(options.brightness || 100);} else {let filters = this.getVideoPlayerFilterStyle(), videoCss = "";if (filters) videoCss += `filter: ${filters} !important;`;if (getGlobalPref("screenshot.applyFilters")) ScreenshotManager.getInstance().updateCanvasFilters(filters);let css = "";if (videoCss) css = `#game-stream video { ${videoCss} }`;this.$videoCss.textContent = css;}this.resizePlayer();}reloadPlayer() {this.cleanUpWebGL2Player(), this.playerType = "default", this.setPlayerType("webgl2", !1);}cleanUpWebGL2Player() {this.webGL2Player?.destroy(), this.webGL2Player = null;}destroy() {this.cleanUpWebGL2Player();}} -function patchVideoApi() {let PREF_SKIP_SPLASH_VIDEO = getGlobalPref("ui.splashVideo.skip"), showFunc = function() {if (this.style.visibility = "visible", !this.videoWidth) return;let playerOptions = {processing: getStreamPref("video.processing"),sharpness: getStreamPref("video.processing.sharpness"),saturation: getStreamPref("video.saturation"),contrast: getStreamPref("video.contrast"),brightness: getStreamPref("video.brightness")};STATES.currentStream.streamPlayer = new StreamPlayer(this, getStreamPref("video.player.type"), playerOptions), BxEventBus.Stream.emit("state.playing", {$video: this});}, nativePlay = HTMLMediaElement.prototype.play;HTMLMediaElement.prototype.nativePlay = nativePlay, HTMLMediaElement.prototype.play = function() {if (this.className && this.className.startsWith("XboxSplashVideo")) {if (PREF_SKIP_SPLASH_VIDEO) return this.volume = 0, this.style.display = "none", this.dispatchEvent(new Event("ended")), new Promise(() => {});return nativePlay.apply(this);}let $parent = this.parentElement;if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 });return nativePlay.apply(this);};} +class BaseCanvasPlayer extends BaseStreamPlayer {$canvas;targetFps = 60;frameInterval = 0;lastFrameTime = 0;animFrameId = null;frameCallback;constructor(playerType, $video, logTag) {super(playerType, "canvas", $video, logTag);let $canvas = document.createElement("canvas");$canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, $video.insertAdjacentElement("afterend", this.$canvas);let frameCallback;if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {let $video2 = this.$video;frameCallback = $video2.requestVideoFrameCallback.bind($video2);} else frameCallback = requestAnimationFrame;this.frameCallback = frameCallback;}async init() {super.init(), await this.setupShaders(), this.setupRendering();}setTargetFps(target) {this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0;}getCanvas() {return this.$canvas;}destroy() {if (BxLogger.info(this.logTag, "Destroy"), this.isStopped = !0, this.animFrameId) {if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId);else cancelAnimationFrame(this.animFrameId);this.animFrameId = null;}if (this.$canvas.isConnected) this.$canvas.remove();this.$canvas.width = 1, this.$canvas.height = 1;}toFilterId(processing) {return processing === "cas" ? 2 : 1;}} +class WebGL2Player extends BaseCanvasPlayer {gl = null;resources = [];program = null;boundDrawFrame;constructor($video) {super("webgl2", $video, "WebGL2Player");this.boundDrawFrame = this.drawFrame.bind(this);}updateCanvas() {console.log("updateCanvas", this.options);let gl = this.gl, program = this.program, filterId = this.toFilterId(this.options.processing);gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpness), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness / 100), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast / 100), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation / 100);}forceDrawFrame() {let gl = this.gl;gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);}drawFrame() {if (this.isStopped) return;this.animFrameId = this.frameCallback(this.boundDrawFrame);let draw = !0;if (this.targetFps === 0) draw = !1;else if (this.targetFps < 60) {let currentTime = performance.now();if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1;else this.lastFrameTime = currentTime;}if (draw) {let gl = this.gl;gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);}}setupRendering() {this.animFrameId = this.frameCallback(this.boundDrawFrame);}async setupShaders() {let gl = this.$canvas.getContext("webgl2", {isBx: !0,antialias: !0,alpha: !1,powerPreference: getStreamPref("video.player.powerPreference")});this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);let vShader = gl.createShader(gl.VERTEX_SHADER);gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader);let fShader = gl.createShader(gl.FRAGMENT_SHADER);gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader);let program = gl.createProgram();if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`);this.updateCanvas();let buffer = gl.createBuffer();this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);let texture = gl.createTexture();this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0);}destroy() {super.destroy();let gl = this.gl;if (!gl) return;gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null);for (let resource of this.resources)if (resource instanceof WebGLProgram) gl.deleteProgram(resource);else if (resource instanceof WebGLShader) gl.deleteShader(resource);else if (resource instanceof WebGLTexture) gl.deleteTexture(resource);else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource);this.gl = null;}refreshPlayer() {this.updateCanvas();}} +class VideoPlayer extends BaseStreamPlayer {$videoCss;$usmMatrix;constructor($video, logTag) {super("default", "video", $video, logTag);}init() {super.init();let xmlns = "http://www.w3.org/2000/svg", $svg = CE("svg", {id: "bx-video-filters",class: "bx-gone",xmlns}, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", {id: "bx-filter-usm",xmlns}, this.$usmMatrix = CE("feConvolveMatrix", {id: "bx-filter-usm-matrix",order: "3",xmlns}))));this.$videoCss = CE("style", { id: "bx-video-css" });let $fragment = document.createDocumentFragment();$fragment.append(this.$videoCss, $svg), document.documentElement.appendChild($fragment);}setupRendering() {}forceDrawFrame() {}updateCanvas() {}refreshPlayer() {let filters = this.getVideoPlayerFilterStyle(), videoCss = "";if (filters) videoCss += `filter: ${filters} !important;`;if (getGlobalPref("screenshot.applyFilters")) ScreenshotManager.getInstance().updateCanvasFilters(filters);let css = "";if (videoCss) css = `#game-stream video { ${videoCss} }`;this.$videoCss.textContent = css;}clearFilters() {this.$videoCss.textContent = "";}getVideoPlayerFilterStyle() {let filters = [], sharpness = this.options.sharpness || 0;if (this.options.processing === "usm" && sharpness != 0) {let matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`;this.$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)");}let saturation = this.options.saturation || 100;if (saturation != 100) filters.push(`saturate(${saturation}%)`);let contrast = this.options.contrast || 100;if (contrast != 100) filters.push(`contrast(${contrast}%)`);let brightness = this.options.brightness || 100;if (brightness != 100) filters.push(`brightness(${brightness}%)`);return filters.join(" ");}} +var clarity_boost_default3 = `struct Params { +filterId: f32, +sharpness: f32, +brightness: f32, +contrast: f32, +saturation: f32,}; +struct VertexOutput { +@builtin(position) position: vec4, +@location(0) uv: vec2,}; +@group(0) @binding(0) var ourSampler: sampler; +@group(0) @binding(1) var ourTexture: texture_2d; +@group(0) @binding(2) var ourParams: Params; +const FILTER_UNSHARP_MASKING: f32 = 1.0; +const CAS_CONTRAST_PEAK: f32 = 0.8 * -3.0 + 8.0; +@vertex +fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { +let pos: array, 6> = array( +vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), +vec2(-1.0, 1.0), vec2(1.0, -1.0), vec2(1.0, 1.0) +); +let uv: array, 6> = array( +vec2(0.0, 1.0), vec2(1.0, 1.0), vec2(0.0, 0.0), +vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(1.0, 0.0) +); +return VertexOutput(vec4(pos[vertexIndex], 0.0, 1.0), uv[vertexIndex]);} +fn luminance(color: vec3) -> f32 { +return dot(color, vec3(0.299, 0.587, 0.114));} +fn clarityBoost(coord: vec2, texSize: vec2, e: vec3) -> vec3 { +let texelSize = 1.0 / texSize; +let a = textureSample(ourTexture, ourSampler, coord + texelSize * vec2(-1.0, 1.0)).rgb; +let b = textureSample(ourTexture, ourSampler, coord + texelSize * vec2( 0.0, 1.0)).rgb; +let c = textureSample(ourTexture, ourSampler, coord + texelSize * vec2( 1.0, 1.0)).rgb; +let d = textureSample(ourTexture, ourSampler, coord + texelSize * vec2(-1.0, 0.0)).rgb; +let f = textureSample(ourTexture, ourSampler, coord + texelSize * vec2( 1.0, 0.0)).rgb; +let g = textureSample(ourTexture, ourSampler, coord + texelSize * vec2(-1.0, -1.0)).rgb; +let h = textureSample(ourTexture, ourSampler, coord + texelSize * vec2( 0.0, -1.0)).rgb; +let i = textureSample(ourTexture, ourSampler, coord + texelSize * vec2( 1.0, -1.0)).rgb; +if ourParams.filterId == FILTER_UNSHARP_MASKING { +let gaussianBlur = (a + c + g + i) * 1.0 + (b + d + f + h) * 2.0 + e * 4.0; +let blurred = gaussianBlur / 16.0; +return e + (e - blurred) * (ourParams.sharpness / 3.0);} +let minRgb = min(min(min(d, e), min(f, b)), h) + min(min(a, c), min(g, i)); +let maxRgb = max(max(max(d, e), max(f, b)), h) + max(max(a, c), max(g, i)); +let reciprocalMaxRgb = 1.0 / maxRgb; +var amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, vec3(0.0), vec3(1.0)); +amplifyRgb = 1.0 / sqrt(amplifyRgb); +let weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK)); +let reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0); +let window = b + d + f + h; +let outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, vec3(0.0), vec3(1.0)); +return mix(e, outColor, ourParams.sharpness / 2.0);} +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { +let texSize = vec2(textureDimensions(ourTexture)); +let center = textureSample(ourTexture, ourSampler, input.uv); +var adjustedRgb = clarityBoost(input.uv, texSize, center.rgb); +let gray = luminance(adjustedRgb); +adjustedRgb = mix(vec3(gray), adjustedRgb, ourParams.saturation); +adjustedRgb = (adjustedRgb - 0.5) * ourParams.contrast + 0.5; +adjustedRgb *= ourParams.brightness; +return vec4(adjustedRgb, 1.0);}`; +class WebGPUPlayer extends BaseCanvasPlayer {static device;context;texture;pipeline;sampler;bindGroup;boundDrawFrame;static async prepare() {if (!navigator.gpu) return;try {let adapter = await navigator.gpu.requestAdapter({powerPreference: "low-power"});WebGPUPlayer.device = await adapter?.requestDevice();} catch (ex) {alert(ex);}}constructor($video) {super("webgpu", $video, "WebGPUPlayer");this.boundDrawFrame = this.drawFrame.bind(this);}setupShaders() {if (this.context = this.$canvas.getContext("webgpu"), !this.context) {alert("Can't initiate context");return;}let format = navigator.gpu.getPreferredCanvasFormat();this.context.configure({device: WebGPUPlayer.device,format,alphaMode: "opaque"}), this.texture = WebGPUPlayer.device.createTexture({size: [this.$video.videoWidth, this.$video.videoHeight, 1],format: "rgba8unorm",usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT});let shaderModule = WebGPUPlayer.device.createShaderModule({ code: clarity_boost_default3 });this.pipeline = WebGPUPlayer.device.createRenderPipeline({layout: "auto",vertex: {module: shaderModule,entryPoint: "vs_main"},fragment: {module: shaderModule,entryPoint: "fs_main",targets: [{ format }]},primitive: { topology: "triangle-list" }}), this.sampler = WebGPUPlayer.device.createSampler({ magFilter: "linear", minFilter: "linear" }), this.updateCanvas();}prepareUniformBuffer(value, classType) {let uniform = new classType(value), uniformBuffer = WebGPUPlayer.device.createBuffer({size: uniform.byteLength,usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST});return WebGPUPlayer.device.queue.writeBuffer(uniformBuffer, 0, uniform), uniformBuffer;}updateCanvas() {let paramsBuffer = this.prepareUniformBuffer([this.toFilterId(this.options.processing),this.options.sharpness,this.options.brightness / 100,this.options.contrast / 100,this.options.saturation / 100], Float32Array);this.bindGroup = WebGPUPlayer.device.createBindGroup({layout: this.pipeline.getBindGroupLayout(0),entries: [{ binding: 0, resource: this.sampler },{ binding: 1, resource: this.texture.createView() },{ binding: 2, resource: { buffer: paramsBuffer } }]});}drawFrame() {if (this.isStopped) return;this.animFrameId = this.frameCallback(this.boundDrawFrame);let draw = !0;if (this.targetFps === 0) draw = !1;else if (this.targetFps < 60) {let currentTime = performance.now();if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1;else this.lastFrameTime = currentTime;}if (draw) {let frame = new VideoFrame(this.$video);WebGPUPlayer.device.queue.copyExternalImageToTexture({ source: frame }, { texture: this.texture }, [this.$video.videoWidth, this.$video.videoHeight]), frame.close();let commandEncoder = WebGPUPlayer.device.createCommandEncoder(), passEncoder = commandEncoder.beginRenderPass({colorAttachments: [{view: this.context.getCurrentTexture().createView(),loadOp: "clear",storeOp: "store",clearValue: [0, 0, 0, 1]}]});passEncoder.setPipeline(this.pipeline), passEncoder.setBindGroup(0, this.bindGroup), passEncoder.draw(6), passEncoder.end(), WebGPUPlayer.device.queue.submit([commandEncoder.finish()]);}}setupRendering() {this.animFrameId = this.frameCallback(this.boundDrawFrame);}forceDrawFrame() {}refreshPlayer() {this.updateCanvas();}destroy() {if (super.destroy(), this.isStopped = !0, this.texture) this.texture.destroy(), this.texture = null;if (this.pipeline = null, this.bindGroup = null, this.sampler = null, this.context) this.context.unconfigure(), this.context = null;console.log("WebGPU context successfully freed.");}} +class StreamPlayerManager {static instance;static getInstance = () => StreamPlayerManager.instance ?? (StreamPlayerManager.instance = new StreamPlayerManager);$video;videoPlayer;canvasPlayer;playerType = "default";constructor() {}setVideoElement($video) {this.$video = $video, this.videoPlayer = new VideoPlayer($video, "VideoPlayer"), this.videoPlayer.init();}resizePlayer() {let PREF_RATIO = getStreamPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, targetWidth, targetHeight, targetObjectFit;if (PREF_RATIO.includes(":")) {let tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]), width = 0, height = 0, parentRect = $video.parentElement.getBoundingClientRect();if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio;else width = parentRect.width, height = width / videoRatio;width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString();let $parent = $video.parentElement, position = getStreamPref("video.position");if ($parent.style.removeProperty("padding-top"), $parent.dataset.position = position, position === "top-half" || position === "bottom-half") {let padding = Math.floor((window.innerHeight - height) / 4);if (padding > 0) {if (position === "bottom-half") padding *= 3;$parent.style.paddingTop = padding + "px";}}targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill";} else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString();if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, this.canvasPlayer) {let $canvas = this.canvasPlayer.getCanvas();$canvas.style.width = targetWidth, $canvas.style.height = targetHeight, $canvas.style.objectFit = targetObjectFit, $video.dispatchEvent(new Event("resize"));}if (isNativeTouchGame && this.playerType !== "default") window.BX_EXPOSED.streamSession.updateDimensions();}switchPlayerType(type, refreshPlayer = !1) {if (this.playerType !== type) {let videoClass = BX_FLAGS.DeviceInfo.deviceType === "android-tv" ? "bx-pixel" : "bx-gone";if (this.cleanUpCanvasPlayer(), type === "default") this.$video.classList.remove(videoClass);else {if (type === "webgpu") this.canvasPlayer = new WebGPUPlayer(this.$video);else this.canvasPlayer = new WebGL2Player(this.$video);this.canvasPlayer.init(), this.videoPlayer.clearFilters(), this.$video.classList.add(videoClass);}this.playerType = type;}refreshPlayer && this.refreshPlayer();}updateOptions(options, refreshPlayer = !1) {(this.canvasPlayer || this.videoPlayer).updateOptions(options, refreshPlayer);}getPlayerElement(elementType) {if (typeof elementType === "undefined") elementType = this.playerType === "default" ? "video" : "canvas";if (elementType !== "video") return this.canvasPlayer?.getCanvas();return this.$video;}getCanvasPlayer() {return this.canvasPlayer;}refreshPlayer() {if (this.playerType === "default") this.videoPlayer.refreshPlayer();else ScreenshotManager.getInstance().updateCanvasFilters("none"), this.canvasPlayer?.refreshPlayer();this.resizePlayer();}getVideoPlayerFilterStyle() {throw new Error("Method not implemented.");}cleanUpCanvasPlayer() {this.canvasPlayer?.destroy(), this.canvasPlayer = null;}destroy() {this.cleanUpCanvasPlayer();}} +function patchVideoApi() {let PREF_SKIP_SPLASH_VIDEO = getGlobalPref("ui.splashVideo.skip"), showFunc = function() {if (this.style.visibility = "visible", !this.videoWidth) return;let playerOptions = {processing: getStreamPref("video.processing"),sharpness: getStreamPref("video.processing.sharpness"),saturation: getStreamPref("video.saturation"),contrast: getStreamPref("video.contrast"),brightness: getStreamPref("video.brightness")}, streamPlayerManager = StreamPlayerManager.getInstance();streamPlayerManager.setVideoElement(this), streamPlayerManager.updateOptions(playerOptions, !1), streamPlayerManager.switchPlayerType(getStreamPref("video.player.type")), STATES.currentStream.streamPlayerManager = streamPlayerManager, BxEventBus.Stream.emit("state.playing", {$video: this});}, nativePlay = HTMLMediaElement.prototype.play;HTMLMediaElement.prototype.nativePlay = nativePlay, HTMLMediaElement.prototype.play = function() {if (this.className && this.className.startsWith("XboxSplashVideo")) {if (PREF_SKIP_SPLASH_VIDEO) return this.volume = 0, this.style.display = "none", this.dispatchEvent(new Event("ended")), new Promise(() => {});return nativePlay.apply(this);}let $parent = this.parentElement;if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 });return nativePlay.apply(this);};} function patchRtcCodecs() {if (getGlobalPref("stream.video.codecProfile") === "default") return;if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1;} function patchRtcPeerConnection() {let nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;RTCPeerConnection.prototype.createDataChannel = function() {let dataChannel = nativeCreateDataChannel.apply(this, arguments);return BxEventBus.Stream.emit("dataChannelCreated", { dataChannel }), dataChannel;};let maxVideoBitrateDef = getGlobalPrefDefinition("stream.video.maxBitrate"), maxVideoBitrate = getGlobalPref("stream.video.maxBitrate"), codec = getGlobalPref("stream.video.codecProfile");if (codec !== "default" || maxVideoBitrate < maxVideoBitrateDef.max) {let nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription;RTCPeerConnection.prototype.setLocalDescription = function(description) {if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec);try {if (maxVideoBitrate < maxVideoBitrateDef.max && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000));} catch (e) {BxLogger.error("setLocalDescription", e);}return nativeSetLocalDescription.apply(this, arguments);};}let OrgRTCPeerConnection = window.RTCPeerConnection;window.RTCPeerConnection = function() {let conn = new OrgRTCPeerConnection;return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => {BxLogger.info("connectionstatechange", conn.connectionState);}), conn;};} function patchAudioContext() {let OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain;window.AudioContext = function(options) {if (options && options.latencyHint) options.latencyHint = 0;let ctx = new OrgAudioContext(options);return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() {let gainNode = nativeCreateGain.apply(this);return gainNode.gain.value = getStreamPref("audio.volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode;}, STATES.currentStream.audioContext = ctx, ctx;};} @@ -352,9 +418,9 @@ BxEventBus.Stream.on("state.playing", (payload) => {window.BX_STREAM_SETTINGS = BxEventBus.Stream.on("state.error", () => {BxEventBus.Stream.emit("state.stopped", {});}); window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, (e) => {if (e.component === "product-detail") ProductDetailsPage.injectButtons();}); BxEventBus.Stream.on("dataChannelCreated", (payload) => {let { dataChannel } = payload;if (dataChannel?.label !== "message") return;dataChannel.addEventListener("message", async (msg) => {if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return;if (!msg.data.includes("/titleinfo")) return;let currentStream = STATES.currentStream, json = JSON.parse(JSON.parse(msg.data).content), currentId = currentStream.xboxTitleId ?? null, newId = parseInt(json.titleid, 16);if (STATES.remotePlay.isPlaying) if (currentStream.titleSlug = "remote-play", json.focused) {let productTitle = await XboxApi.getProductTitle(newId);if (productTitle) currentStream.titleSlug = productTitleToSlug(productTitle);else newId = -1;} else newId = 0;if (currentId !== newId) currentStream.xboxTitleId = newId, BxEventBus.Stream.emit("xboxTitleId.changed", {id: newId});});}); -function unload() {if (!STATES.isPlaying) return;KeyboardShortcutHandler.getInstance().stop(), EmulatedMkbHandler.getInstance()?.destroy(), NativeMkbHandler.getInstance()?.destroy(), DeviceVibrationManager.getInstance()?.reset(), STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().destroy(), StreamBadges.getInstance().destroy(), MouseCursorHider.getInstance()?.stop(), TouchController.reset(), GameBar.getInstance()?.disable(), BxEventBus.Stream.emit("xboxTitleId.changed", { id: -1 });} +function unload() {if (!STATES.isPlaying) return;KeyboardShortcutHandler.getInstance().stop(), EmulatedMkbHandler.getInstance()?.destroy(), NativeMkbHandler.getInstance()?.destroy(), DeviceVibrationManager.getInstance()?.reset(), STATES.currentStream.streamPlayerManager?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().destroy(), StreamBadges.getInstance().destroy(), MouseCursorHider.getInstance()?.stop(), TouchController.reset(), GameBar.getInstance()?.disable(), BxEventBus.Stream.emit("xboxTitleId.changed", { id: -1 });} BxEventBus.Stream.on("state.stopped", unload); window.addEventListener("pagehide", (e) => {BxEventBus.Stream.emit("state.stopped", {});}); window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, (e) => {ScreenshotManager.getInstance().takeScreenshot();}); -function main() {if (GhPagesUtils.fetchLatestCommit(), getGlobalPref("nativeMkb.mode") !== "off") {let customList = getGlobalPref("nativeMkb.forcedGames");BX_FLAGS.ForceNativeMkbTitles.push(...customList);}if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getGlobalPref("audio.volume.booster.enabled") && patchAudioContext(), getGlobalPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager();if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getGlobalPref("xhome.enabled")) RemotePlayManager.detect();if (getGlobalPref("touchController.mode") === "all") TouchController.setup();if (AppInterface && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on")) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString());if (getGlobalPref("ui.gameCard.waitTime.show") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getGlobalPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));} +function main() {if (GhPagesUtils.fetchLatestCommit(), getGlobalPref("nativeMkb.mode") !== "off") {let customList = getGlobalPref("nativeMkb.forcedGames");BX_FLAGS.ForceNativeMkbTitles.push(...customList);}if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getGlobalPref("audio.volume.booster.enabled") && patchAudioContext(), getGlobalPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager();if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), WebGPUPlayer.prepare(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getGlobalPref("xhome.enabled")) RemotePlayManager.detect();if (getGlobalPref("touchController.mode") === "all") TouchController.setup();if (AppInterface && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on")) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString());if (getGlobalPref("ui.gameCard.waitTime.show") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getGlobalPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));} main();