diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index 5d02b62..bd13eda 100644 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -171,6 +171,7 @@ var XcloudEvent; } BxEvent.dispatch = dispatch; })(BxEvent || (BxEvent = {})); +window.BxEvent = BxEvent; // src/utils/bx-flags.ts @@ -179,7 +180,7 @@ var XcloudEvent; var DEFAULT_FLAGS = { CheckForUpdate: true, PreloadRemotePlay: true, - PreloadUi: false, + PreloadUi: true, EnableXcloudLogging: false, SafariWorkaround: true, UseDevTouchLayout: false @@ -265,98 +266,186 @@ var createButton = (options) => { var CTN = document.createTextNode.bind(document); window.BX_CE = createElement; -// src/assets/svg/controller.svg -var controller_default = "\n \n\n"; - -// src/assets/svg/copy.svg -var copy_default = "\n \n\n"; - -// src/assets/svg/cursor-text.svg -var cursor_text_default = "\n \n\n"; - -// src/assets/svg/display.svg -var display_default = "\n \n\n"; - -// src/assets/svg/mouse-settings.svg -var mouse_settings_default = "\n \n\n"; - -// src/assets/svg/mouse.svg -var mouse_default = "\n \n\n"; - -// src/assets/svg/new.svg -var new_default = "\n \n\n"; - -// src/assets/svg/question.svg -var question_default = "\n \n\n"; - -// src/assets/svg/refresh.svg -var refresh_default = "\n \n\n"; - -// src/assets/svg/remote-play.svg -var remote_play_default = "\n \n\n"; - -// src/assets/svg/stream-settings.svg -var stream_settings_default = "\n \n\n"; - -// src/assets/svg/stream-stats.svg -var stream_stats_default = "\n \n\n"; - -// src/assets/svg/trash.svg -var trash_default = "\n \n\n"; - -// src/assets/svg/touch-control-enable.svg -var touch_control_enable_default = "\n \n \n \n \n\n"; - -// src/assets/svg/touch-control-disable.svg -var touch_control_disable_default = "\n \n \n \n \n \n \n \n\n"; - -// src/assets/svg/caret-left.svg -var caret_left_default = "\n \n\n"; - -// src/assets/svg/caret-right.svg -var caret_right_default = "\n \n\n"; - -// src/assets/svg/camera.svg -var camera_default = "\n \n \n \n \n\n"; - -// src/assets/svg/microphone.svg -var microphone_default = "\n \n\n"; - -// src/assets/svg/microphone-slash.svg -var microphone_slash_default = "\n \n \n\n"; - -// src/utils/bx-icon.ts -var BxIcon = { - STREAM_SETTINGS: stream_settings_default, - STREAM_STATS: stream_stats_default, - CONTROLLER: controller_default, - DISPLAY: display_default, - MOUSE: mouse_default, - MOUSE_SETTINGS: mouse_settings_default, - NEW: new_default, - COPY: copy_default, - TRASH: trash_default, - CURSOR_TEXT: cursor_text_default, - QUESTION: question_default, - REFRESH: refresh_default, - REMOTE_PLAY: remote_play_default, - CARET_LEFT: caret_left_default, - CARET_RIGHT: caret_right_default, - SCREENSHOT: camera_default, - TOUCH_CONTROL_ENABLE: touch_control_enable_default, - TOUCH_CONTROL_DISABLE: touch_control_disable_default, - MICROPHONE: microphone_default, - MICROPHONE_MUTED: microphone_slash_default -}; - -// src/modules/game-bar/action-base.ts -class BaseGameBarAction { - constructor() { +// src/utils/screenshot.ts +class Screenshot { + static #$canvas; + static #canvasContext; + static setup() { + if (Screenshot.#$canvas) { + return; + } + Screenshot.#$canvas = CE("canvas", { class: "bx-gone" }); + Screenshot.#canvasContext = Screenshot.#$canvas.getContext("2d", { + alpha: false, + willReadFrequently: false + }); } - reset() { + static updateCanvasSize(width, height) { + const $canvas = Screenshot.#$canvas; + if ($canvas) { + $canvas.width = width; + $canvas.height = height; + } + } + static updateCanvasFilters(filters) { + Screenshot.#canvasContext.filter = filters; + } + static onAnimationEnd(e) { + e.target.classList.remove("bx-taking-screenshot"); + } + static takeScreenshot(callback) { + const currentStream = STATES.currentStream; + const $video = currentStream.$video; + const $canvas = Screenshot.#$canvas; + if (!$video || !$canvas) { + return; + } + $video.parentElement?.addEventListener("animationend", this.onAnimationEnd); + $video.parentElement?.classList.add("bx-taking-screenshot"); + const canvasContext = Screenshot.#canvasContext; + canvasContext.drawImage($video, 0, 0, $canvas.width, $canvas.height); + if (AppInterface) { + const data = $canvas.toDataURL("image/png").split(";base64,")[1]; + AppInterface.saveScreenshot(currentStream.titleId, data); + canvasContext.clearRect(0, 0, $canvas.width, $canvas.height); + callback && callback(); + return; + } + $canvas && $canvas.toBlob((blob) => { + const now = +new Date; + const $anchor = CE("a", { + download: `${currentStream.titleId}-${now}.png`, + href: URL.createObjectURL(blob) + }); + $anchor.click(); + URL.revokeObjectURL($anchor.href); + canvasContext.clearRect(0, 0, $canvas.width, $canvas.height); + callback && callback(); + }, "image/png"); } } +// src/utils/prompt-font.ts +var PrompFont; +(function(PrompFont2) { + PrompFont2["A"] = "⇓"; + PrompFont2["B"] = "⇒"; + PrompFont2["X"] = "⇐"; + PrompFont2["Y"] = "⇑"; + PrompFont2["LB"] = "↘"; + PrompFont2["RB"] = "↙"; + PrompFont2["LT"] = "↖"; + PrompFont2["RT"] = "↗"; + PrompFont2["SELECT"] = "⇺"; + PrompFont2["START"] = "⇻"; + PrompFont2["HOME"] = ""; + PrompFont2["UP"] = "≻"; + PrompFont2["DOWN"] = "≽"; + PrompFont2["LEFT"] = "≺"; + PrompFont2["RIGHT"] = "≼"; + PrompFont2["L3"] = "↺"; + PrompFont2["LS_UP"] = "↾"; + PrompFont2["LS_DOWN"] = "⇂"; + PrompFont2["LS_LEFT"] = "↼"; + PrompFont2["LS_RIGHT"] = "⇀"; + PrompFont2["R3"] = "↻"; + PrompFont2["RS_UP"] = "↿"; + PrompFont2["RS_DOWN"] = "⇃"; + PrompFont2["RS_LEFT"] = "↽"; + PrompFont2["RS_RIGHT"] = "⇁"; +})(PrompFont || (PrompFont = {})); + +// src/modules/mkb/definitions.ts +var GamepadKey; +(function(GamepadKey2) { + GamepadKey2[GamepadKey2["A"] = 0] = "A"; + GamepadKey2[GamepadKey2["B"] = 1] = "B"; + GamepadKey2[GamepadKey2["X"] = 2] = "X"; + GamepadKey2[GamepadKey2["Y"] = 3] = "Y"; + GamepadKey2[GamepadKey2["LB"] = 4] = "LB"; + GamepadKey2[GamepadKey2["RB"] = 5] = "RB"; + GamepadKey2[GamepadKey2["LT"] = 6] = "LT"; + GamepadKey2[GamepadKey2["RT"] = 7] = "RT"; + GamepadKey2[GamepadKey2["SELECT"] = 8] = "SELECT"; + GamepadKey2[GamepadKey2["START"] = 9] = "START"; + GamepadKey2[GamepadKey2["L3"] = 10] = "L3"; + GamepadKey2[GamepadKey2["R3"] = 11] = "R3"; + GamepadKey2[GamepadKey2["UP"] = 12] = "UP"; + GamepadKey2[GamepadKey2["DOWN"] = 13] = "DOWN"; + GamepadKey2[GamepadKey2["LEFT"] = 14] = "LEFT"; + GamepadKey2[GamepadKey2["RIGHT"] = 15] = "RIGHT"; + GamepadKey2[GamepadKey2["HOME"] = 16] = "HOME"; + GamepadKey2[GamepadKey2["SHARE"] = 17] = "SHARE"; + GamepadKey2[GamepadKey2["LS_UP"] = 100] = "LS_UP"; + GamepadKey2[GamepadKey2["LS_DOWN"] = 101] = "LS_DOWN"; + GamepadKey2[GamepadKey2["LS_LEFT"] = 102] = "LS_LEFT"; + GamepadKey2[GamepadKey2["LS_RIGHT"] = 103] = "LS_RIGHT"; + GamepadKey2[GamepadKey2["RS_UP"] = 200] = "RS_UP"; + GamepadKey2[GamepadKey2["RS_DOWN"] = 201] = "RS_DOWN"; + GamepadKey2[GamepadKey2["RS_LEFT"] = 202] = "RS_LEFT"; + GamepadKey2[GamepadKey2["RS_RIGHT"] = 203] = "RS_RIGHT"; +})(GamepadKey || (GamepadKey = {})); +var GamepadKeyName = { + [GamepadKey.A]: ["A", PrompFont.A], + [GamepadKey.B]: ["B", PrompFont.B], + [GamepadKey.X]: ["X", PrompFont.X], + [GamepadKey.Y]: ["Y", PrompFont.Y], + [GamepadKey.LB]: ["LB", PrompFont.LB], + [GamepadKey.RB]: ["RB", PrompFont.RB], + [GamepadKey.LT]: ["LT", PrompFont.LT], + [GamepadKey.RT]: ["RT", PrompFont.RT], + [GamepadKey.SELECT]: ["Select", PrompFont.SELECT], + [GamepadKey.START]: ["Start", PrompFont.START], + [GamepadKey.HOME]: ["Home", PrompFont.HOME], + [GamepadKey.UP]: ["D-Pad Up", PrompFont.UP], + [GamepadKey.DOWN]: ["D-Pad Down", PrompFont.DOWN], + [GamepadKey.LEFT]: ["D-Pad Left", PrompFont.LEFT], + [GamepadKey.RIGHT]: ["D-Pad Right", PrompFont.RIGHT], + [GamepadKey.L3]: ["L3", PrompFont.L3], + [GamepadKey.LS_UP]: ["Left Stick Up", PrompFont.LS_UP], + [GamepadKey.LS_DOWN]: ["Left Stick Down", PrompFont.LS_DOWN], + [GamepadKey.LS_LEFT]: ["Left Stick Left", PrompFont.LS_LEFT], + [GamepadKey.LS_RIGHT]: ["Left Stick Right", PrompFont.LS_RIGHT], + [GamepadKey.R3]: ["R3", PrompFont.R3], + [GamepadKey.RS_UP]: ["Right Stick Up", PrompFont.RS_UP], + [GamepadKey.RS_DOWN]: ["Right Stick Down", PrompFont.RS_DOWN], + [GamepadKey.RS_LEFT]: ["Right Stick Left", PrompFont.RS_LEFT], + [GamepadKey.RS_RIGHT]: ["Right Stick Right", PrompFont.RS_RIGHT] +}; +var GamepadStick; +(function(GamepadStick2) { + GamepadStick2[GamepadStick2["LEFT"] = 0] = "LEFT"; + GamepadStick2[GamepadStick2["RIGHT"] = 1] = "RIGHT"; +})(GamepadStick || (GamepadStick = {})); +var MouseButtonCode; +(function(MouseButtonCode2) { + MouseButtonCode2["LEFT_CLICK"] = "Mouse0"; + MouseButtonCode2["RIGHT_CLICK"] = "Mouse2"; + MouseButtonCode2["MIDDLE_CLICK"] = "Mouse1"; +})(MouseButtonCode || (MouseButtonCode = {})); +var MouseMapTo; +(function(MouseMapTo2) { + MouseMapTo2[MouseMapTo2["OFF"] = 0] = "OFF"; + MouseMapTo2[MouseMapTo2["LS"] = 1] = "LS"; + MouseMapTo2[MouseMapTo2["RS"] = 2] = "RS"; +})(MouseMapTo || (MouseMapTo = {})); +var WheelCode; +(function(WheelCode2) { + WheelCode2["SCROLL_UP"] = "ScrollUp"; + WheelCode2["SCROLL_DOWN"] = "ScrollDown"; + WheelCode2["SCROLL_LEFT"] = "ScrollLeft"; + WheelCode2["SCROLL_RIGHT"] = "ScrollRight"; +})(WheelCode || (WheelCode = {})); +var MkbPresetKey; +(function(MkbPresetKey2) { + MkbPresetKey2["MOUSE_MAP_TO"] = "map_to"; + MkbPresetKey2["MOUSE_SENSITIVITY_X"] = "sensitivity_x"; + MkbPresetKey2["MOUSE_SENSITIVITY_Y"] = "sensitivity_y"; + MkbPresetKey2["MOUSE_DEADZONE_COUNTERWEIGHT"] = "deadzone_counterweight"; + MkbPresetKey2["MOUSE_STICK_DECAY_STRENGTH"] = "stick_decay_strength"; + MkbPresetKey2["MOUSE_STICK_DECAY_MIN"] = "stick_decay_min"; +})(MkbPresetKey || (MkbPresetKey = {})); + // src/utils/translation.ts var SUPPORTED_LANGUAGES = { "en-US": "English (United States)", @@ -370,6 +459,7 @@ var SUPPORTED_LANGUAGES = { "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", @@ -418,8 +508,10 @@ var Texts = { copy: "Copy", custom: "Custom", "deadzone-counterweight": "Deadzone counterweight", + decrease: "Decrease", default: "Default", delete: "Delete", + device: "Device", "device-unsupported-touch": "Your device doesn't have touch support", "device-vibration": "Device vibration", "device-vibration-not-using-gamepad": "On when not using gamepad", @@ -449,12 +541,14 @@ var Texts = { "game-bar": "Game Bar", "getting-consoles-list": "Getting the list of consoles...", help: "Help", + hide: "Hide", "hide-idle-cursor": "Hide mouse cursor on idle", "hide-scrollbar": "Hide web page's scrollbar", "hide-system-menu-icon": "Hide System menu's icon", "hide-touch-controller": "Hide touch controller", "horizontal-sensitivity": "Horizontal sensitivity", import: "Import", + increase: "Increase", "install-android": "Install Better xCloud app for Android", "keyboard-shortcuts": "Keyboard shortcuts", language: "Language", @@ -465,13 +559,13 @@ var Texts = { "local-co-op": "Local co-op", "map-mouse-to": "Map mouse to", "may-not-work-properly": "May not work properly!", - "menu-stream-settings": "Stream settings", - "menu-stream-stats": "Stream stats", + menu: "Menu", microphone: "Microphone", "mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings", "mkb-click-to-activate": "Click to activate", "mkb-disclaimer": "Using this feature when playing online could be viewed as cheating", "mouse-and-keyboard": "Mouse & Keyboard", + "mute-unmute-sound": "Mute/unmute sound", muted: "Muted", name: "Name", new: "New", @@ -502,6 +596,7 @@ var Texts = { (e) => `Naciśnij ${e.key}, aby przełączyć funkcję myszy i klawiatury`, (e) => `Pressione ${e.key} para ativar/desativar a função de Mouse e Teclado`, (e) => `Нажмите ${e.key} для переключения функции мыши и клавиатуры`, + , (e) => `Klavye ve fare özelliğini açmak için ${e.key} tuşuna basın`, (e) => `Натисніть ${e.key}, щоб увімкнути або вимкнути функцію миші та клавіатури`, (e) => `Nhấn ${e.key} để bật/tắt tính năng Chuột và Bàn phím`, @@ -523,6 +618,7 @@ var Texts = { "safari-failed-message": "Failed to run Better xCloud. Retrying, please wait...", saturation: "Saturation", save: "Save", + screen: "Screen", "screenshot-apply-filters": "Applies video filters to screenshots", "separate-touch-controller": "Separate Touch controller & Controller #1", "separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2", @@ -530,7 +626,9 @@ var Texts = { "settings-reload": "Reload page to reflect changes", "settings-reloading": "Reloading...", "shortcut-keys": "Shortcut keys", + show: "Show", "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", @@ -551,6 +649,8 @@ var Texts = { "stick-decay-minimum": "Stick decay minimum", "stick-decay-strength": "Stick decay strength", stream: "Stream", + "stream-settings": "Stream settings", + "stream-stats": "Stream stats", stretch: "Stretch", "stretch-note": "Don't use with native touch games", "support-better-xcloud": "Support Better xCloud", @@ -566,6 +666,9 @@ var Texts = { "tc-muted-colors": "Muted colors", "tc-standard-layout-style": "Standard layout's button style", "text-size": "Text size", + toggle: "Toggle", + "toggle-microphone": "Toggle microphone", + "toggle-stream-stats": "Toggle stream stats", "top-center": "Top-center", "top-left": "Top-left", "top-right": "Top-right", @@ -582,6 +685,7 @@ var Texts = { (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} 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}`, @@ -638,7 +742,7 @@ class Translations { } static get(key, values) { let text = null; - if (Translations.#selectedLocale !== Translations.#EN_US) { + if (Translations.#foreignTranslations && Translations.#selectedLocale !== Translations.#EN_US) { text = Translations.#foreignTranslations[key]; } if (!text) { @@ -696,148 +800,6 @@ class Translations { var t = Translations.get; Translations.init(); -// src/utils/screenshot.ts -class Screenshot { - static setup() { - const currentStream = STATES.currentStream; - if (!currentStream.$screenshotCanvas) { - currentStream.$screenshotCanvas = CE("canvas", { class: "bx-gone" }); - currentStream.screenshotCanvasContext = currentStream.$screenshotCanvas.getContext("2d", { - alpha: false, - willReadFrequently: false - }); - } - } - static updateCanvasSize(width, height) { - const $canvas = STATES.currentStream.$screenshotCanvas; - if ($canvas) { - $canvas.width = width; - $canvas.height = height; - } - } - static updateCanvasFilters(filters) { - STATES.currentStream.screenshotCanvasContext && (STATES.currentStream.screenshotCanvasContext.filter = filters); - } - static onAnimationEnd(e) { - e.target.classList.remove("bx-taking-screenshot"); - } - static takeScreenshot(callback) { - const currentStream = STATES.currentStream; - const $video = currentStream.$video; - const $canvas = currentStream.$screenshotCanvas; - if (!$video || !$canvas) { - return; - } - $video.parentElement?.addEventListener("animationend", this.onAnimationEnd); - $video.parentElement?.classList.add("bx-taking-screenshot"); - const canvasContext = currentStream.screenshotCanvasContext; - canvasContext.drawImage($video, 0, 0, $canvas.width, $canvas.height); - if (AppInterface) { - const data = $canvas.toDataURL("image/png").split(";base64,")[1]; - AppInterface.saveScreenshot(currentStream.titleId, data); - canvasContext.clearRect(0, 0, $canvas.width, $canvas.height); - callback && callback(); - return; - } - $canvas && $canvas.toBlob((blob) => { - const now = +new Date; - const $anchor = CE("a", { - download: `${currentStream.titleId}-${now}.png`, - href: URL.createObjectURL(blob) - }); - $anchor.click(); - URL.revokeObjectURL($anchor.href); - canvasContext.clearRect(0, 0, $canvas.width, $canvas.height); - callback && callback(); - }, "image/png"); - } -} - -// src/modules/game-bar/action-screenshot.ts -class ScreenshotAction extends BaseGameBarAction { - $content; - constructor() { - super(); - const onClick = (e) => { - BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); - Screenshot.takeScreenshot(); - }; - this.$content = createButton({ - style: ButtonStyle.GHOST, - icon: BxIcon.SCREENSHOT, - title: t("take-screenshot"), - onClick - }); - } - render() { - return this.$content; - } -} - -// src/utils/toast.ts -class Toast { - static #$wrapper; - static #$msg; - static #$status; - static #stack = []; - static #isShowing = false; - static #timeout; - static #DURATION = 3000; - static show(msg, status, options = {}) { - options = options || {}; - const args = Array.from(arguments); - if (options.instant) { - Toast.#stack = [args]; - Toast.#showNext(); - } else { - Toast.#stack.push(args); - !Toast.#isShowing && Toast.#showNext(); - } - } - static #showNext() { - if (!Toast.#stack.length) { - Toast.#isShowing = false; - return; - } - Toast.#isShowing = true; - Toast.#timeout && clearTimeout(Toast.#timeout); - Toast.#timeout = window.setTimeout(Toast.#hide, Toast.#DURATION); - const [msg, status, options] = Toast.#stack.shift(); - if (options.html) { - Toast.#$msg.innerHTML = msg; - } else { - Toast.#$msg.textContent = msg; - } - if (status) { - Toast.#$status.classList.remove("bx-gone"); - Toast.#$status.textContent = status; - } else { - Toast.#$status.classList.add("bx-gone"); - } - const classList = Toast.#$wrapper.classList; - classList.remove("bx-offscreen", "bx-hide"); - classList.add("bx-show"); - } - static #hide() { - Toast.#timeout = null; - const classList = Toast.#$wrapper.classList; - classList.remove("bx-show"); - classList.add("bx-hide"); - } - static setup() { - Toast.#$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, Toast.#$msg = CE("span", { class: "bx-toast-msg" }), Toast.#$status = CE("span", { class: "bx-toast-status" })); - Toast.#$wrapper.addEventListener("transitionend", (e) => { - const classList = Toast.#$wrapper.classList; - if (classList.contains("bx-hide")) { - classList.remove("bx-offscreen", "bx-hide"); - classList.add("bx-offscreen"); - Toast.#showNext(); - } - }); - document.documentElement.appendChild(Toast.#$wrapper); - } -} - // src/utils/settings.ts var SettingElementType; (function(SettingElementType2) { @@ -1076,6 +1038,138 @@ class SettingElement { } } +// src/modules/mkb/mkb-preset.ts +class MkbPreset { + static MOUSE_SETTINGS = { + [MkbPresetKey.MOUSE_MAP_TO]: { + label: t("map-mouse-to"), + type: SettingElementType.OPTIONS, + default: MouseMapTo[MouseMapTo.RS], + options: { + [MouseMapTo[MouseMapTo.RS]]: t("right-stick"), + [MouseMapTo[MouseMapTo.LS]]: t("left-stick"), + [MouseMapTo[MouseMapTo.OFF]]: t("off") + } + }, + [MkbPresetKey.MOUSE_SENSITIVITY_Y]: { + label: t("horizontal-sensitivity"), + type: SettingElementType.NUMBER_STEPPER, + default: 50, + min: 1, + max: 200, + params: { + suffix: "%", + exactTicks: 20 + } + }, + [MkbPresetKey.MOUSE_SENSITIVITY_X]: { + label: t("vertical-sensitivity"), + type: SettingElementType.NUMBER_STEPPER, + default: 50, + min: 1, + max: 200, + params: { + suffix: "%", + exactTicks: 20 + } + }, + [MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: { + label: t("deadzone-counterweight"), + type: SettingElementType.NUMBER_STEPPER, + default: 20, + min: 1, + max: 100, + params: { + suffix: "%", + exactTicks: 10 + } + }, + [MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: { + label: t("stick-decay-strength"), + type: SettingElementType.NUMBER_STEPPER, + default: 100, + min: 10, + max: 100, + params: { + suffix: "%", + exactTicks: 10 + } + }, + [MkbPresetKey.MOUSE_STICK_DECAY_MIN]: { + label: t("stick-decay-minimum"), + type: SettingElementType.NUMBER_STEPPER, + default: 10, + min: 1, + max: 10, + params: { + suffix: "%" + } + } + }; + static DEFAULT_PRESET = { + mapping: { + [GamepadKey.UP]: ["ArrowUp"], + [GamepadKey.DOWN]: ["ArrowDown"], + [GamepadKey.LEFT]: ["ArrowLeft"], + [GamepadKey.RIGHT]: ["ArrowRight"], + [GamepadKey.LS_UP]: ["KeyW"], + [GamepadKey.LS_DOWN]: ["KeyS"], + [GamepadKey.LS_LEFT]: ["KeyA"], + [GamepadKey.LS_RIGHT]: ["KeyD"], + [GamepadKey.RS_UP]: ["KeyI"], + [GamepadKey.RS_DOWN]: ["KeyK"], + [GamepadKey.RS_LEFT]: ["KeyJ"], + [GamepadKey.RS_RIGHT]: ["KeyL"], + [GamepadKey.A]: ["Space", "KeyE"], + [GamepadKey.X]: ["KeyR"], + [GamepadKey.B]: ["ControlLeft", "Backspace"], + [GamepadKey.Y]: ["KeyV"], + [GamepadKey.START]: ["Enter"], + [GamepadKey.SELECT]: ["Tab"], + [GamepadKey.LB]: ["KeyC", "KeyG"], + [GamepadKey.RB]: ["KeyQ"], + [GamepadKey.HOME]: ["Backquote"], + [GamepadKey.RT]: [MouseButtonCode.LEFT_CLICK], + [GamepadKey.LT]: [MouseButtonCode.RIGHT_CLICK], + [GamepadKey.L3]: ["ShiftLeft"], + [GamepadKey.R3]: ["KeyF"] + }, + mouse: { + [MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo[MouseMapTo.RS], + [MkbPresetKey.MOUSE_SENSITIVITY_X]: 50, + [MkbPresetKey.MOUSE_SENSITIVITY_Y]: 50, + [MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20, + [MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: 100, + [MkbPresetKey.MOUSE_STICK_DECAY_MIN]: 10 + } + }; + static convert(preset) { + const obj = { + mapping: {}, + mouse: Object.assign({}, preset.mouse) + }; + for (const buttonIndex in preset.mapping) { + for (const keyName of preset.mapping[parseInt(buttonIndex)]) { + obj.mapping[keyName] = parseInt(buttonIndex); + } + } + const mouse = obj.mouse; + mouse[MkbPresetKey.MOUSE_SENSITIVITY_X] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY; + mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY; + mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT] *= MkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT; + mouse[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH] *= 0.01; + mouse[MkbPresetKey.MOUSE_STICK_DECAY_MIN] *= 0.01; + const mouseMapTo = MouseMapTo[mouse[MkbPresetKey.MOUSE_MAP_TO]]; + if (typeof mouseMapTo !== "undefined") { + mouse[MkbPresetKey.MOUSE_MAP_TO] = mouseMapTo; + } else { + mouse[MkbPresetKey.MOUSE_MAP_TO] = MkbPreset.MOUSE_SETTINGS[MkbPresetKey.MOUSE_MAP_TO].default; + } + console.log(obj); + return obj; + } +} + // src/modules/stream/stream-badges.ts var StreamBadge; (function(StreamBadge2) { @@ -2217,838 +2311,67 @@ var getPref = prefs.get.bind(prefs); var setPref = prefs.set.bind(prefs); var toPrefElement = prefs.toElement.bind(prefs); -// src/modules/touch-controller.ts -var LOG_TAG = "TouchController"; - -class TouchController { - static #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent("message", { - data: JSON.stringify({ - content: '{"layoutId":""}', - target: "/streaming/touchcontrols/showlayoutv2", - type: "Message" - }), - origin: "better-xcloud" - }); - static #$style; - static #enable = false; - static #dataChannel; - static #customLayouts = {}; - static #baseCustomLayouts = {}; - static #currentLayoutId; - static #customList; - static enable() { - TouchController.#enable = true; +// src/utils/toast.ts +class Toast { + static #$wrapper; + static #$msg; + static #$status; + static #stack = []; + static #isShowing = false; + static #timeout; + static #DURATION = 3000; + static show(msg, status, options = {}) { + options = options || {}; + const args = Array.from(arguments); + if (options.instant) { + Toast.#stack = [args]; + Toast.#showNext(); + } else { + Toast.#stack.push(args); + !Toast.#isShowing && Toast.#showNext(); + } } - static disable() { - TouchController.#enable = false; - } - static isEnabled() { - return TouchController.#enable; - } - static #showDefault() { - TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER); - } - static #show() { - document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.remove("bx-offscreen"); + static #showNext() { + if (!Toast.#stack.length) { + Toast.#isShowing = false; + return; + } + Toast.#isShowing = true; + Toast.#timeout && clearTimeout(Toast.#timeout); + Toast.#timeout = window.setTimeout(Toast.#hide, Toast.#DURATION); + const [msg, status, options] = Toast.#stack.shift(); + if (options.html) { + Toast.#$msg.innerHTML = msg; + } else { + Toast.#$msg.textContent = msg; + } + if (status) { + Toast.#$status.classList.remove("bx-gone"); + Toast.#$status.textContent = status; + } else { + Toast.#$status.classList.add("bx-gone"); + } + const classList = Toast.#$wrapper.classList; + classList.remove("bx-offscreen", "bx-hide"); + classList.add("bx-show"); } static #hide() { - document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.add("bx-offscreen"); - } - static toggleVisibility(status) { - if (!TouchController.#dataChannel) { - return; - } - status ? TouchController.#hide() : TouchController.#show(); - } - static reset() { - TouchController.#enable = false; - TouchController.#dataChannel = null; - TouchController.#$style && (TouchController.#$style.textContent = ""); - } - static #dispatchMessage(msg) { - TouchController.#dataChannel && window.setTimeout(() => { - TouchController.#dataChannel.dispatchEvent(msg); - }, 10); - } - static #dispatchLayouts(data) { - BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, { - data - }); - } - static async getCustomLayouts(xboxTitleId, retries = 1) { - if (xboxTitleId in TouchController.#customLayouts) { - TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]); - return; - } - retries = retries || 1; - if (retries > 2) { - TouchController.#customLayouts[xboxTitleId] = null; - window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000); - return; - } - const baseUrl = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts${BX_FLAGS.UseDevTouchLayout ? "/dev" : ""}`; - const url = `${baseUrl}/${xboxTitleId}.json`; - try { - const resp = await NATIVE_FETCH(url); - const json = await resp.json(); - const layouts = {}; - json.layouts.forEach(async (layoutName) => { - let baseLayouts = {}; - if (layoutName in TouchController.#baseCustomLayouts) { - baseLayouts = TouchController.#baseCustomLayouts[layoutName]; - } else { - try { - const layoutUrl = `${baseUrl}/layouts/${layoutName}.json`; - const resp2 = await NATIVE_FETCH(layoutUrl); - const json2 = await resp2.json(); - baseLayouts = json2.layouts; - TouchController.#baseCustomLayouts[layoutName] = baseLayouts; - } catch (e) { - } - } - Object.assign(layouts, baseLayouts); - }); - json.layouts = layouts; - TouchController.#customLayouts[xboxTitleId] = json; - window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000); - } catch (e) { - TouchController.getCustomLayouts(xboxTitleId, retries + 1); - } - } - static loadCustomLayout(xboxTitleId, layoutId, delay = 0) { - if (!window.BX_EXPOSED.touchLayoutManager) { - const listener = (e) => { - window.removeEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener); - if (TouchController.#enable) { - TouchController.loadCustomLayout(xboxTitleId, layoutId, 0); - } - }; - window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener); - return; - } - const layoutChanged = TouchController.#currentLayoutId !== layoutId; - TouchController.#currentLayoutId = layoutId; - const layoutData = TouchController.#customLayouts[xboxTitleId]; - if (!xboxTitleId || !layoutId || !layoutData) { - TouchController.#enable && TouchController.#showDefault(); - return; - } - const layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout]; - if (!layout) { - return; - } - let msg; - let html9 = false; - if (layout.author) { - const author = `${escapeHtml(layout.author)}`; - msg = t("touch-control-layout-by", { name: author }); - html9 = true; - } else { - msg = t("touch-control-layout"); - } - layoutChanged && Toast.show(msg, layout.name, { html: html9 }); - window.setTimeout(() => { - window.BX_EXPOSED.shouldShowSensorControls = JSON.stringify(layout).includes("gyroscope"); - window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({ - type: "showLayout", - scope: xboxTitleId, - subscope: "base", - layout: { - id: "System.Standard", - displayName: "System", - layoutFile: layout - } - }); - }, delay); - } - static updateCustomList() { - const key = "better_xcloud_custom_touch_layouts"; - TouchController.#customList = JSON.parse(window.localStorage.getItem(key) || "[]"); - NATIVE_FETCH("https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/ids.json").then((response) => response.json()).then((json) => { - TouchController.#customList = json; - window.localStorage.setItem(key, JSON.stringify(json)); - }); - } - static getCustomList() { - return TouchController.#customList; + Toast.#timeout = null; + const classList = Toast.#$wrapper.classList; + classList.remove("bx-show"); + classList.add("bx-hide"); } static setup() { - window.testTouchLayout = (layout) => { - const { touchLayoutManager } = window.BX_EXPOSED; - touchLayoutManager && touchLayoutManager.changeLayoutForScope({ - type: "showLayout", - scope: "" + STATES.currentStream?.xboxTitleId, - subscope: "base", - layout: { - id: "System.Standard", - displayName: "Custom", - layoutFile: layout - } - }); - }; - const $style = document.createElement("style"); - document.documentElement.appendChild($style); - TouchController.#$style = $style; - const PREF_STYLE_STANDARD = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD); - const PREF_STYLE_CUSTOM = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM); - window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { - const dataChannel = e.dataChannel; - if (!dataChannel || dataChannel.label !== "message") { - return; - } - let filter = ""; - if (TouchController.#enable) { - if (PREF_STYLE_STANDARD === "white") { - filter = "grayscale(1) brightness(2)"; - } else if (PREF_STYLE_STANDARD === "muted") { - filter = "sepia(0.5)"; - } - } else if (PREF_STYLE_CUSTOM === "muted") { - filter = "sepia(0.5)"; - } - if (filter) { - $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`; - } else { - $style.textContent = ""; - } - TouchController.#dataChannel = dataChannel; - dataChannel.addEventListener("open", () => { - window.setTimeout(TouchController.#show, 1000); - }); - let focused = false; - dataChannel.addEventListener("message", (msg) => { - if (msg.origin === "better-xcloud" || typeof msg.data !== "string") { - return; - } - if (msg.data.includes("touchcontrols/showtitledefault")) { - if (TouchController.#enable) { - if (focused) { - TouchController.getCustomLayouts(STATES.currentStream?.xboxTitleId); - } else { - TouchController.#showDefault(); - } - } - return; - } - try { - if (msg.data.includes("/titleinfo")) { - const json = JSON.parse(JSON.parse(msg.data).content); - focused = json.focused; - if (!json.focused) { - TouchController.#show(); - } - STATES.currentStream.xboxTitleId = parseInt(json.titleid, 16).toString(); - } - } catch (e2) { - BxLogger.error(LOG_TAG, "Load custom layout", e2); - } - }); - }); - } -} - -// src/modules/game-bar/action-touch-control.ts -class TouchControlAction extends BaseGameBarAction { - $content; - constructor() { - super(); - const onClick = (e) => { - BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); - const $parent = e.target.closest("div[data-enabled]"); - let enabled = $parent.getAttribute("data-enabled", "true") === "true"; - $parent.setAttribute("data-enabled", (!enabled).toString()); - TouchController.toggleVisibility(enabled); - }; - const $btnEnable = createButton({ - style: ButtonStyle.GHOST, - icon: BxIcon.TOUCH_CONTROL_ENABLE, - title: t("show-touch-controller"), - onClick, - classes: ["bx-activated"] - }); - const $btnDisable = createButton({ - style: ButtonStyle.GHOST, - icon: BxIcon.TOUCH_CONTROL_DISABLE, - title: t("hide-touch-controller"), - onClick - }); - this.$content = CE("div", {}, $btnEnable, $btnDisable); - this.reset(); - } - render() { - return this.$content; - } - reset() { - this.$content.setAttribute("data-enabled", "true"); - } -} - -// src/modules/game-bar/action-microphone.ts -var MicrophoneState; -(function(MicrophoneState2) { - MicrophoneState2["REQUESTED"] = "Requested"; - MicrophoneState2["ENABLED"] = "Enabled"; - MicrophoneState2["MUTED"] = "Muted"; - MicrophoneState2["NOT_ALLOWED"] = "NotAllowed"; - MicrophoneState2["NOT_FOUND"] = "NotFound"; -})(MicrophoneState || (MicrophoneState = {})); - -class MicrophoneAction extends BaseGameBarAction { - $content; - visible = false; - constructor() { - super(); - const onClick = (e) => { - BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); - const state = this.$content.getAttribute("data-enabled"); - const enableMic = state === "true" ? false : true; - try { - window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic); - this.$content.setAttribute("data-enabled", enableMic.toString()); - } catch (e2) { - console.log(e2); - } - }; - const $btnDefault = createButton({ - style: ButtonStyle.GHOST, - icon: BxIcon.MICROPHONE, - title: t("show-touch-controller"), - onClick, - classes: ["bx-activated"] - }); - const $btnMuted = createButton({ - style: ButtonStyle.GHOST, - icon: BxIcon.MICROPHONE_MUTED, - title: t("hide-touch-controller"), - onClick - }); - this.$content = CE("div", {}, $btnDefault, $btnMuted); - this.reset(); - window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, (e) => { - const microphoneState = e.microphoneState; - const enabled = microphoneState === MicrophoneState.ENABLED; - this.$content.setAttribute("data-enabled", enabled.toString()); - this.$content.classList.remove("bx-gone"); - }); - } - render() { - return this.$content; - } - reset() { - this.visible = false; - this.$content.classList.add("bx-gone"); - this.$content.setAttribute("data-enabled", "false"); - } -} - -// src/modules/game-bar/game-bar.ts -class GameBar { - static instance; - static getInstance() { - if (!GameBar.instance) { - GameBar.instance = new GameBar; - } - return GameBar.instance; - } - static VISIBLE_DURATION = 2000; - $gameBar; - $container; - timeout = null; - actions = []; - constructor() { - let $container; - const position = getPref(PrefKey.GAME_BAR_POSITION); - const $gameBar = CE("div", { id: "bx-game-bar", class: "bx-gone", "data-position": position }, $container = CE("div", { class: "bx-game-bar-container bx-offscreen" }), createSvgIcon(position === "bottom-left" ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT)); - this.actions = [ - new ScreenshotAction, - ...STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== "off" ? [new TouchControlAction] : [], - new MicrophoneAction - ]; - if (position === "bottom-right") { - this.actions.reverse(); - } - for (const action of this.actions) { - $container.appendChild(action.render()); - } - $gameBar.addEventListener("click", (e) => { - if (e.target !== $gameBar) { - return; - } - $container.classList.contains("bx-show") ? this.hideBar() : this.showBar(); - }); - window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar.bind(this)); - $container.addEventListener("pointerover", this.clearHideTimeout.bind(this)); - $container.addEventListener("pointerout", this.beginHideTimeout.bind(this)); - $container.addEventListener("transitionend", (e) => { - const classList = $container.classList; + Toast.#$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, Toast.#$msg = CE("span", { class: "bx-toast-msg" }), Toast.#$status = CE("span", { class: "bx-toast-status" })); + Toast.#$wrapper.addEventListener("transitionend", (e) => { + const classList = Toast.#$wrapper.classList; if (classList.contains("bx-hide")) { classList.remove("bx-offscreen", "bx-hide"); classList.add("bx-offscreen"); + Toast.#showNext(); } }); - document.documentElement.appendChild($gameBar); - this.$gameBar = $gameBar; - this.$container = $container; - } - beginHideTimeout() { - this.clearHideTimeout(); - this.timeout = window.setTimeout(() => { - this.timeout = null; - this.hideBar(); - }, GameBar.VISIBLE_DURATION); - } - clearHideTimeout() { - this.timeout && clearTimeout(this.timeout); - this.timeout = null; - } - enable() { - this.$gameBar && this.$gameBar.classList.remove("bx-gone"); - } - disable() { - this.hideBar(); - this.$gameBar && this.$gameBar.classList.add("bx-gone"); - } - showBar() { - if (!this.$container) { - return; - } - this.$container.classList.remove("bx-offscreen", "bx-hide"); - this.$container.classList.add("bx-show"); - this.beginHideTimeout(); - } - hideBar() { - if (!this.$container) { - return; - } - this.$container.classList.remove("bx-show"); - this.$container.classList.add("bx-hide"); - } - reset() { - for (const action of this.actions) { - action.reset(); - } - } -} - -// src/utils/bx-exposed.ts -var InputType; -(function(InputType2) { - InputType2["CONTROLLER"] = "Controller"; - InputType2["MKB"] = "MKB"; - InputType2["CUSTOM_TOUCH_OVERLAY"] = "CustomTouchOverlay"; - InputType2["GENERIC_TOUCH"] = "GenericTouch"; - InputType2["NATIVE_TOUCH"] = "NativeTouch"; - InputType2["BATIVE_SENSOR"] = "NativeSensor"; -})(InputType || (InputType = {})); -var BxExposed = { - onPollingModeChanged: (mode) => { - if (getPref(PrefKey.GAME_BAR_POSITION) === "off") { - return; - } - const gameBar = GameBar.getInstance(); - if (!STATES.isPlaying) { - gameBar.disable(); - return; - } - mode !== "None" ? gameBar.disable() : gameBar.enable(); - }, - getTitleInfo: () => STATES.currentStream.titleInfo, - modifyTitleInfo: (titleInfo) => { - titleInfo = structuredClone(titleInfo); - let supportedInputTypes = titleInfo.details.supportedInputTypes; - if (getPref(PrefKey.NATIVE_MKB_DISABLED) || UserAgent.isMobile()) { - supportedInputTypes = supportedInputTypes.filter((i) => i !== InputType.MKB); - } - titleInfo.details.hasMkbSupport = supportedInputTypes.includes(InputType.MKB); - if (STATES.hasTouchSupport) { - let touchControllerAvailability = getPref(PrefKey.STREAM_TOUCH_CONTROLLER); - if (touchControllerAvailability !== "off" && getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) { - const gamepads = window.navigator.getGamepads(); - let gamepadFound = false; - for (let gamepad of gamepads) { - if (gamepad && gamepad.connected) { - gamepadFound = true; - break; - } - } - gamepadFound && (touchControllerAvailability = "off"); - } - if (touchControllerAvailability === "off") { - supportedInputTypes = supportedInputTypes.filter((i) => i !== InputType.CUSTOM_TOUCH_OVERLAY && i !== InputType.GENERIC_TOUCH); - } - titleInfo.details.hasTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH) || supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY) || supportedInputTypes.includes(InputType.GENERIC_TOUCH); - if (!titleInfo.details.hasTouchSupport && touchControllerAvailability === "all") { - titleInfo.details.hasFakeTouchSupport = true; - supportedInputTypes.push(InputType.GENERIC_TOUCH); - } - } - titleInfo.details.supportedInputTypes = supportedInputTypes; - STATES.currentStream.titleInfo = titleInfo; - BxEvent.dispatch(window, BxEvent.TITLE_INFO_READY); - return titleInfo; - }, - setupGainNode: ($media, audioStream) => { - if ($media instanceof HTMLAudioElement) { - $media.muted = true; - $media.addEventListener("playing", (e) => { - $media.muted = true; - $media.pause(); - }); - } else { - $media.muted = true; - $media.addEventListener("playing", (e) => { - $media.muted = true; - }); - } - const audioCtx = STATES.currentStream.audioContext; - const source = audioCtx.createMediaStreamSource(audioStream); - const gainNode = audioCtx.createGain(); - source.connect(gainNode).connect(audioCtx.destination); - } -}; - -// src/utils/region.ts -function getPreferredServerRegion(shortName = false) { - let preferredRegion = getPref(PrefKey.SERVER_REGION); - if (preferredRegion in STATES.serverRegions) { - if (shortName && STATES.serverRegions[preferredRegion].shortName) { - return STATES.serverRegions[preferredRegion].shortName; - } else { - return preferredRegion; - } - } - for (let regionName in STATES.serverRegions) { - const region = STATES.serverRegions[regionName]; - if (!region.isDefault) { - continue; - } - if (shortName && region.shortName) { - return region.shortName; - } else { - return regionName; - } - } - return "???"; -} - -// src/modules/loading-screen.ts -class LoadingScreen { - static #$bgStyle; - static #$waitTimeBox; - static #waitTimeInterval = null; - static #orgWebTitle; - static #secondsToString(seconds) { - const m = Math.floor(seconds / 60); - const s = Math.floor(seconds % 60); - const mDisplay = m > 0 ? `${m}m` : ""; - const sDisplay = `${s}s`.padStart(s >= 0 ? 3 : 4, "0"); - return mDisplay + sDisplay; - } - static setup() { - const titleInfo = STATES.currentStream.titleInfo; - if (!titleInfo) { - return; - } - if (!LoadingScreen.#$bgStyle) { - const $bgStyle = CE("style"); - document.documentElement.appendChild($bgStyle); - LoadingScreen.#$bgStyle = $bgStyle; - } - LoadingScreen.#setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl); - if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === "hide") { - LoadingScreen.#hideRocket(); - } - } - static #hideRocket() { - let $bgStyle = LoadingScreen.#$bgStyle; - const css = ` -#game-stream div[class*=RocketAnimation-module__container] > svg { - display: none; -} -`; - $bgStyle.textContent += css; - } - static #setBackground(imageUrl) { - let $bgStyle = LoadingScreen.#$bgStyle; - imageUrl = imageUrl + "?w=1920"; - const css = ` -#game-stream { - background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important; - background-color: transparent !important; - background-position: center center !important; - background-repeat: no-repeat !important; - background-size: cover !important; -} - -#game-stream rect[width="800"] { - transition: opacity 0.3s ease-in-out !important; -} -`; - $bgStyle.textContent += css; - const bg = new Image; - bg.onload = (e) => { - $bgStyle.textContent += ` -#game-stream rect[width="800"] { - opacity: 0 !important; -} -`; - }; - bg.src = imageUrl; - } - static setupWaitTime(waitTime) { - if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === "hide-queue") { - LoadingScreen.#hideRocket(); - } - let secondsLeft = waitTime; - let $countDown; - let $estimated; - LoadingScreen.#orgWebTitle = document.title; - const endDate = new Date; - const timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60; - endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds); - let endDateStr = endDate.toISOString().slice(0, 19); - endDateStr = endDateStr.substring(0, 10) + " " + endDateStr.substring(11, 19); - endDateStr += ` (${LoadingScreen.#secondsToString(waitTime)})`; - let $waitTimeBox = LoadingScreen.#$waitTimeBox; - if (!$waitTimeBox) { - $waitTimeBox = CE("div", { class: "bx-wait-time-box" }, CE("label", {}, t("server")), CE("span", {}, getPreferredServerRegion()), CE("label", {}, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", {}, t("wait-time-countdown")), $countDown = CE("span", {})); - document.documentElement.appendChild($waitTimeBox); - LoadingScreen.#$waitTimeBox = $waitTimeBox; - } else { - $waitTimeBox.classList.remove("bx-gone"); - $estimated = $waitTimeBox.querySelector(".bx-wait-time-estimated"); - $countDown = $waitTimeBox.querySelector(".bx-wait-time-countdown"); - } - $estimated.textContent = endDateStr; - $countDown.textContent = LoadingScreen.#secondsToString(secondsLeft); - document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`; - LoadingScreen.#waitTimeInterval = window.setInterval(() => { - secondsLeft--; - $countDown.textContent = LoadingScreen.#secondsToString(secondsLeft); - document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`; - if (secondsLeft <= 0) { - LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval); - LoadingScreen.#waitTimeInterval = null; - } - }, 1000); - } - static hide() { - LoadingScreen.#orgWebTitle && (document.title = LoadingScreen.#orgWebTitle); - LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add("bx-gone"); - if (getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.#$bgStyle) { - const $rocketBg = document.querySelector('#game-stream rect[width="800"]'); - $rocketBg && $rocketBg.addEventListener("transitionend", (e) => { - LoadingScreen.#$bgStyle.textContent += ` -#game-stream { - background: #000 !important; -} -`; - }); - LoadingScreen.#$bgStyle.textContent += ` -#game-stream rect[width="800"] { - opacity: 1 !important; -} -`; - } - LoadingScreen.reset(); - } - static reset() { - LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add("bx-gone"); - LoadingScreen.#$bgStyle && (LoadingScreen.#$bgStyle.textContent = ""); - LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval); - LoadingScreen.#waitTimeInterval = null; - } -} - -// src/modules/mkb/definitions.ts -var GamepadKey; -(function(GamepadKey2) { - GamepadKey2[GamepadKey2["A"] = 0] = "A"; - GamepadKey2[GamepadKey2["B"] = 1] = "B"; - GamepadKey2[GamepadKey2["X"] = 2] = "X"; - GamepadKey2[GamepadKey2["Y"] = 3] = "Y"; - GamepadKey2[GamepadKey2["LB"] = 4] = "LB"; - GamepadKey2[GamepadKey2["RB"] = 5] = "RB"; - GamepadKey2[GamepadKey2["LT"] = 6] = "LT"; - GamepadKey2[GamepadKey2["RT"] = 7] = "RT"; - GamepadKey2[GamepadKey2["SELECT"] = 8] = "SELECT"; - GamepadKey2[GamepadKey2["START"] = 9] = "START"; - GamepadKey2[GamepadKey2["L3"] = 10] = "L3"; - GamepadKey2[GamepadKey2["R3"] = 11] = "R3"; - GamepadKey2[GamepadKey2["UP"] = 12] = "UP"; - GamepadKey2[GamepadKey2["DOWN"] = 13] = "DOWN"; - GamepadKey2[GamepadKey2["LEFT"] = 14] = "LEFT"; - GamepadKey2[GamepadKey2["RIGHT"] = 15] = "RIGHT"; - GamepadKey2[GamepadKey2["HOME"] = 16] = "HOME"; - GamepadKey2[GamepadKey2["LS_UP"] = 100] = "LS_UP"; - GamepadKey2[GamepadKey2["LS_DOWN"] = 101] = "LS_DOWN"; - GamepadKey2[GamepadKey2["LS_LEFT"] = 102] = "LS_LEFT"; - GamepadKey2[GamepadKey2["LS_RIGHT"] = 103] = "LS_RIGHT"; - GamepadKey2[GamepadKey2["RS_UP"] = 200] = "RS_UP"; - GamepadKey2[GamepadKey2["RS_DOWN"] = 201] = "RS_DOWN"; - GamepadKey2[GamepadKey2["RS_LEFT"] = 202] = "RS_LEFT"; - GamepadKey2[GamepadKey2["RS_RIGHT"] = 203] = "RS_RIGHT"; -})(GamepadKey || (GamepadKey = {})); -var GamepadKeyName = { - [GamepadKey.A]: ["A", "⇓"], - [GamepadKey.B]: ["B", "⇒"], - [GamepadKey.X]: ["X", "⇐"], - [GamepadKey.Y]: ["Y", "⇑"], - [GamepadKey.LB]: ["LB", "↘"], - [GamepadKey.RB]: ["RB", "↙"], - [GamepadKey.LT]: ["LT", "↖"], - [GamepadKey.RT]: ["RT", "↗"], - [GamepadKey.SELECT]: ["Select", "⇺"], - [GamepadKey.START]: ["Start", "⇻"], - [GamepadKey.HOME]: ["Home", ""], - [GamepadKey.UP]: ["D-Pad Up", "≻"], - [GamepadKey.DOWN]: ["D-Pad Down", "≽"], - [GamepadKey.LEFT]: ["D-Pad Left", "≺"], - [GamepadKey.RIGHT]: ["D-Pad Right", "≼"], - [GamepadKey.L3]: ["L3", "↺"], - [GamepadKey.LS_UP]: ["Left Stick Up", "↾"], - [GamepadKey.LS_DOWN]: ["Left Stick Down", "⇂"], - [GamepadKey.LS_LEFT]: ["Left Stick Left", "↼"], - [GamepadKey.LS_RIGHT]: ["Left Stick Right", "⇀"], - [GamepadKey.R3]: ["R3", "↻"], - [GamepadKey.RS_UP]: ["Right Stick Up", "↿"], - [GamepadKey.RS_DOWN]: ["Right Stick Down", "⇃"], - [GamepadKey.RS_LEFT]: ["Right Stick Left", "↽"], - [GamepadKey.RS_RIGHT]: ["Right Stick Right", "⇁"] -}; -var GamepadStick; -(function(GamepadStick2) { - GamepadStick2[GamepadStick2["LEFT"] = 0] = "LEFT"; - GamepadStick2[GamepadStick2["RIGHT"] = 1] = "RIGHT"; -})(GamepadStick || (GamepadStick = {})); -var MouseButtonCode; -(function(MouseButtonCode2) { - MouseButtonCode2["LEFT_CLICK"] = "Mouse0"; - MouseButtonCode2["RIGHT_CLICK"] = "Mouse2"; - MouseButtonCode2["MIDDLE_CLICK"] = "Mouse1"; -})(MouseButtonCode || (MouseButtonCode = {})); -var MouseMapTo; -(function(MouseMapTo2) { - MouseMapTo2[MouseMapTo2["OFF"] = 0] = "OFF"; - MouseMapTo2[MouseMapTo2["LS"] = 1] = "LS"; - MouseMapTo2[MouseMapTo2["RS"] = 2] = "RS"; -})(MouseMapTo || (MouseMapTo = {})); -var WheelCode; -(function(WheelCode2) { - WheelCode2["SCROLL_UP"] = "ScrollUp"; - WheelCode2["SCROLL_DOWN"] = "ScrollDown"; - WheelCode2["SCROLL_LEFT"] = "ScrollLeft"; - WheelCode2["SCROLL_RIGHT"] = "ScrollRight"; -})(WheelCode || (WheelCode = {})); -var MkbPresetKey; -(function(MkbPresetKey2) { - MkbPresetKey2["MOUSE_MAP_TO"] = "map_to"; - MkbPresetKey2["MOUSE_SENSITIVITY_X"] = "sensitivity_x"; - MkbPresetKey2["MOUSE_SENSITIVITY_Y"] = "sensitivity_y"; - MkbPresetKey2["MOUSE_DEADZONE_COUNTERWEIGHT"] = "deadzone_counterweight"; - MkbPresetKey2["MOUSE_STICK_DECAY_STRENGTH"] = "stick_decay_strength"; - MkbPresetKey2["MOUSE_STICK_DECAY_MIN"] = "stick_decay_min"; -})(MkbPresetKey || (MkbPresetKey = {})); - -// src/modules/dialog.ts -class Dialog { - $dialog; - $title; - $content; - $overlay; - onClose; - constructor(options) { - const { - title, - className, - content, - hideCloseButton, - onClose, - helpUrl - } = options; - const $overlay = document.querySelector(".bx-dialog-overlay"); - if (!$overlay) { - this.$overlay = CE("div", { class: "bx-dialog-overlay bx-gone" }); - this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()); - document.documentElement.appendChild(this.$overlay); - } else { - this.$overlay = $overlay; - } - let $close; - this.onClose = onClose; - this.$dialog = CE("div", { class: `bx-dialog ${className || ""} bx-gone` }, this.$title = CE("h2", {}, CE("b", {}, title), helpUrl && createButton({ - icon: BxIcon.QUESTION, - style: ButtonStyle.GHOST, - title: t("help"), - url: helpUrl - })), this.$content = CE("div", { class: "bx-dialog-content" }, content), !hideCloseButton && ($close = CE("button", { type: "button" }, t("close")))); - $close && $close.addEventListener("click", (e) => { - this.hide(e); - }); - !title && this.$title.classList.add("bx-gone"); - !content && this.$content.classList.add("bx-gone"); - this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()); - document.documentElement.appendChild(this.$dialog); - } - show(newOptions) { - document.activeElement && document.activeElement.blur(); - if (newOptions && newOptions.title) { - this.$title.querySelector("b").textContent = newOptions.title; - this.$title.classList.remove("bx-gone"); - } - this.$dialog.classList.remove("bx-gone"); - this.$overlay.classList.remove("bx-gone"); - document.body.classList.add("bx-no-scroll"); - } - hide(e) { - this.$dialog.classList.add("bx-gone"); - this.$overlay.classList.add("bx-gone"); - document.body.classList.remove("bx-no-scroll"); - this.onClose && this.onClose(e); - } - toggle() { - this.$dialog.classList.toggle("bx-gone"); - this.$overlay.classList.toggle("bx-gone"); - } -} - -// src/modules/mkb/key-helper.ts -class KeyHelper { - static #NON_PRINTABLE_KEYS = { - Backquote: "`", - [MouseButtonCode.LEFT_CLICK]: "Left Click", - [MouseButtonCode.RIGHT_CLICK]: "Right Click", - [MouseButtonCode.MIDDLE_CLICK]: "Middle Click", - [WheelCode.SCROLL_UP]: "Scroll Up", - [WheelCode.SCROLL_DOWN]: "Scroll Down", - [WheelCode.SCROLL_LEFT]: "Scroll Left", - [WheelCode.SCROLL_RIGHT]: "Scroll Right" - }; - static getKeyFromEvent(e) { - let code; - let name; - if (e instanceof KeyboardEvent) { - code = e.code; - } else if (e instanceof WheelEvent) { - if (e.deltaY < 0) { - code = WheelCode.SCROLL_UP; - } else if (e.deltaY > 0) { - code = WheelCode.SCROLL_DOWN; - } else if (e.deltaX < 0) { - code = WheelCode.SCROLL_LEFT; - } else { - code = WheelCode.SCROLL_RIGHT; - } - } else if (e instanceof MouseEvent) { - code = "Mouse" + e.button; - } - if (code) { - name = KeyHelper.codeToKeyName(code); - } - return code ? { code, name } : null; - } - static codeToKeyName(code) { - return KeyHelper.#NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code; + document.documentElement.appendChild(Toast.#$wrapper); } } @@ -3163,6 +2486,134 @@ class LocalDb { } } +// src/modules/mkb/key-helper.ts +class KeyHelper { + static #NON_PRINTABLE_KEYS = { + Backquote: "`", + [MouseButtonCode.LEFT_CLICK]: "Left Click", + [MouseButtonCode.RIGHT_CLICK]: "Right Click", + [MouseButtonCode.MIDDLE_CLICK]: "Middle Click", + [WheelCode.SCROLL_UP]: "Scroll Up", + [WheelCode.SCROLL_DOWN]: "Scroll Down", + [WheelCode.SCROLL_LEFT]: "Scroll Left", + [WheelCode.SCROLL_RIGHT]: "Scroll Right" + }; + static getKeyFromEvent(e) { + let code; + let name; + if (e instanceof KeyboardEvent) { + code = e.code; + } else if (e instanceof WheelEvent) { + if (e.deltaY < 0) { + code = WheelCode.SCROLL_UP; + } else if (e.deltaY > 0) { + code = WheelCode.SCROLL_DOWN; + } else if (e.deltaX < 0) { + code = WheelCode.SCROLL_LEFT; + } else { + code = WheelCode.SCROLL_RIGHT; + } + } else if (e instanceof MouseEvent) { + code = "Mouse" + e.button; + } + if (code) { + name = KeyHelper.codeToKeyName(code); + } + return code ? { code, name } : null; + } + static codeToKeyName(code) { + return KeyHelper.#NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code; + } +} + +// src/assets/svg/command.svg +var command_default = "\n \n \n\n"; + +// src/assets/svg/controller.svg +var controller_default = "\n \n\n"; + +// src/assets/svg/copy.svg +var copy_default = "\n \n\n"; + +// src/assets/svg/cursor-text.svg +var cursor_text_default = "\n \n\n"; + +// src/assets/svg/display.svg +var display_default = "\n \n\n"; + +// src/assets/svg/mouse-settings.svg +var mouse_settings_default = "\n \n\n"; + +// src/assets/svg/mouse.svg +var mouse_default = "\n \n\n"; + +// src/assets/svg/new.svg +var new_default = "\n \n\n"; + +// src/assets/svg/question.svg +var question_default = "\n \n\n"; + +// src/assets/svg/refresh.svg +var refresh_default = "\n \n\n"; + +// src/assets/svg/remote-play.svg +var remote_play_default = "\n \n\n"; + +// src/assets/svg/stream-settings.svg +var stream_settings_default = "\n \n\n"; + +// src/assets/svg/stream-stats.svg +var stream_stats_default = "\n \n\n"; + +// src/assets/svg/trash.svg +var trash_default = "\n \n\n"; + +// src/assets/svg/touch-control-enable.svg +var touch_control_enable_default = "\n \n \n \n \n\n"; + +// src/assets/svg/touch-control-disable.svg +var touch_control_disable_default = "\n \n \n \n \n \n \n \n\n"; + +// src/assets/svg/caret-left.svg +var caret_left_default = "\n \n\n"; + +// src/assets/svg/caret-right.svg +var caret_right_default = "\n \n\n"; + +// src/assets/svg/camera.svg +var camera_default = "\n \n \n \n \n\n"; + +// src/assets/svg/microphone.svg +var microphone_default = "\n \n\n"; + +// src/assets/svg/microphone-slash.svg +var microphone_slash_default = "\n \n \n\n"; + +// src/utils/bx-icon.ts +var BxIcon = { + STREAM_SETTINGS: stream_settings_default, + STREAM_STATS: stream_stats_default, + COMMAND: command_default, + CONTROLLER: controller_default, + DISPLAY: display_default, + MOUSE: mouse_default, + MOUSE_SETTINGS: mouse_settings_default, + NEW: new_default, + COPY: copy_default, + TRASH: trash_default, + CURSOR_TEXT: cursor_text_default, + QUESTION: question_default, + REFRESH: refresh_default, + REMOTE_PLAY: remote_play_default, + CARET_LEFT: caret_left_default, + CARET_RIGHT: caret_right_default, + SCREENSHOT: camera_default, + TOUCH_CONTROL_ENABLE: touch_control_enable_default, + TOUCH_CONTROL_DISABLE: touch_control_disable_default, + MICROPHONE: microphone_default, + MICROPHONE_MUTED: microphone_slash_default +}; + // src/modules/stream/stream-ui.ts var cloneStreamHudButton = function($orgButton, label, svgIcon) { const $container = $orgButton.cloneNode(true); @@ -3309,7 +2760,7 @@ function injectStreamMenuButtons() { return; } if (!$btnStreamSettings) { - $btnStreamSettings = cloneStreamHudButton($orgButton, t("menu-stream-settings"), BxIcon.STREAM_SETTINGS); + $btnStreamSettings = cloneStreamHudButton($orgButton, t("stream-settings"), BxIcon.STREAM_SETTINGS); $btnStreamSettings.addEventListener("click", (e) => { hideGripHandle(); e.preventDefault(); @@ -3320,7 +2771,7 @@ function injectStreamMenuButtons() { }); } if (!$btnStreamStats) { - $btnStreamStats = cloneStreamHudButton($orgButton, t("menu-stream-stats"), BxIcon.STREAM_STATS); + $btnStreamStats = cloneStreamHudButton($orgButton, t("stream-stats"), BxIcon.STREAM_STATS); $btnStreamStats.addEventListener("click", (e) => { hideGripHandle(); e.preventDefault(); @@ -3370,7 +2821,7 @@ function showStreamSettings(tabId) { } // src/modules/mkb/mkb-handler.ts -var LOG_TAG2 = "MkbHandler"; +var LOG_TAG = "MkbHandler"; class MkbHandler { static #instance; @@ -3708,142 +3159,983 @@ class MkbHandler { static setupEvents() { getPref(PrefKey.MKB_ENABLED) && !UserAgent.isMobile() && window.addEventListener(BxEvent.STREAM_PLAYING, () => { if (!STATES.currentStream.titleInfo?.details.hasMkbSupport) { - BxLogger.info(LOG_TAG2, "Emulate MKB"); + BxLogger.info(LOG_TAG, "Emulate MKB"); MkbHandler.INSTANCE.init(); } }); } } -// src/modules/mkb/mkb-preset.ts -class MkbPreset { - static MOUSE_SETTINGS = { - [MkbPresetKey.MOUSE_MAP_TO]: { - label: t("map-mouse-to"), - type: SettingElementType.OPTIONS, - default: MouseMapTo[MouseMapTo.RS], - options: { - [MouseMapTo[MouseMapTo.RS]]: t("right-stick"), - [MouseMapTo[MouseMapTo.LS]]: t("left-stick"), - [MouseMapTo[MouseMapTo.OFF]]: t("off") +// src/modules/shortcuts/shortcut-microphone.ts +class MicrophoneShortcut { + static toggle(showToast = true) { + if (!window.BX_EXPOSED.streamSession) { + return false; + } + const state = window.BX_EXPOSED.streamSession._microphoneState; + const enableMic = state === "Enabled" ? false : true; + try { + window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic); + showToast && Toast.show(t("microphone"), t(enableMic ? "unmuted" : "muted"), { instant: true }); + return enableMic; + } catch (e) { + console.log(e); + } + return false; + } +} + +// src/modules/shortcuts/shortcut-stream-ui.ts +class StreamUiShortcut { + static showHideStreamMenu() { + window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu(); + } +} + +// src/modules/controller-shortcut.ts +var ShortcutAction; +(function(ShortcutAction2) { + ShortcutAction2["STREAM_SCREENSHOT_CAPTURE"] = "stream-screenshot-capture"; + ShortcutAction2["STREAM_MENU_TOGGLE"] = "stream-menu-toggle"; + ShortcutAction2["STREAM_STATS_TOGGLE"] = "stream-stats-toggle"; + ShortcutAction2["STREAM_SOUND_TOGGLE"] = "stream-sound-toggle"; + ShortcutAction2["STREAM_MICROPHONE_TOGGLE"] = "stream-microphone-toggle"; + ShortcutAction2["STREAM_VOLUME_INC"] = "stream-volume-inc"; + ShortcutAction2["STREAM_VOLUME_DEC"] = "stream-volume-dec"; + ShortcutAction2["DEVICE_VOLUME_INC"] = "device-volume-inc"; + ShortcutAction2["DEVICE_VOLUME_DEC"] = "device-volume-dec"; + ShortcutAction2["SCREEN_BRIGHTNESS_INC"] = "screen-brightness-inc"; + ShortcutAction2["SCREEN_BRIGHTNESS_DEC"] = "screen-brightness-dec"; +})(ShortcutAction || (ShortcutAction = {})); + +class ControllerShortcut { + static #STORAGE_KEY = "better_xcloud_controller_shortcuts"; + static #buttonsCache = {}; + static #buttonsStatus = {}; + static #$selectProfile; + static #$selectActions = {}; + static #$remap; + static #ACTIONS = {}; + static reset(index) { + ControllerShortcut.#buttonsCache[index] = []; + ControllerShortcut.#buttonsStatus[index] = []; + } + static handle(gamepad) { + const gamepadIndex = gamepad.index; + const actions = ControllerShortcut.#ACTIONS[gamepad.id]; + if (!actions) { + return false; + } + ControllerShortcut.#buttonsCache[gamepadIndex] = ControllerShortcut.#buttonsStatus[gamepadIndex].slice(0); + ControllerShortcut.#buttonsStatus[gamepadIndex] = []; + const pressed = []; + let otherButtonPressed = false; + gamepad.buttons.forEach((button, index) => { + if (button.pressed && index !== GamepadKey.HOME) { + otherButtonPressed = true; + pressed[index] = true; + if (actions[index] && !ControllerShortcut.#buttonsCache[gamepadIndex][index]) { + ControllerShortcut.#runAction(actions[index]); + } } - }, - [MkbPresetKey.MOUSE_SENSITIVITY_Y]: { - label: t("horizontal-sensitivity"), - type: SettingElementType.NUMBER_STEPPER, - default: 50, - min: 1, - max: 200, - params: { - suffix: "%", - exactTicks: 20 + }); + ControllerShortcut.#buttonsStatus[gamepadIndex] = pressed; + return otherButtonPressed; + } + static #runAction(action) { + switch (action) { + case ShortcutAction.STREAM_SCREENSHOT_CAPTURE: + Screenshot.takeScreenshot(); + break; + case ShortcutAction.STREAM_STATS_TOGGLE: + StreamStats.toggle(); + break; + case ShortcutAction.STREAM_MICROPHONE_TOGGLE: + MicrophoneShortcut.toggle(); + break; + case ShortcutAction.STREAM_MENU_TOGGLE: + StreamUiShortcut.showHideStreamMenu(); + break; + } + } + static #updateAction(profile, button, action) { + if (!(profile in ControllerShortcut.#ACTIONS)) { + ControllerShortcut.#ACTIONS[profile] = []; + } + if (!action) { + action = null; + } + ControllerShortcut.#ACTIONS[profile][button] = action; + for (const key in ControllerShortcut.#ACTIONS) { + let empty = true; + for (const value of ControllerShortcut.#ACTIONS[key]) { + if (!!value) { + empty = false; + break; + } } - }, - [MkbPresetKey.MOUSE_SENSITIVITY_X]: { - label: t("vertical-sensitivity"), - type: SettingElementType.NUMBER_STEPPER, - default: 50, - min: 1, - max: 200, - params: { - suffix: "%", - exactTicks: 20 - } - }, - [MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: { - label: t("deadzone-counterweight"), - type: SettingElementType.NUMBER_STEPPER, - default: 20, - min: 1, - max: 100, - params: { - suffix: "%", - exactTicks: 10 - } - }, - [MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: { - label: t("stick-decay-strength"), - type: SettingElementType.NUMBER_STEPPER, - default: 100, - min: 10, - max: 100, - params: { - suffix: "%", - exactTicks: 10 - } - }, - [MkbPresetKey.MOUSE_STICK_DECAY_MIN]: { - label: t("stick-decay-minimum"), - type: SettingElementType.NUMBER_STEPPER, - default: 10, - min: 1, - max: 10, - params: { - suffix: "%" + if (empty) { + delete ControllerShortcut.#ACTIONS[key]; } } - }; - static DEFAULT_PRESET = { - mapping: { - [GamepadKey.UP]: ["ArrowUp"], - [GamepadKey.DOWN]: ["ArrowDown"], - [GamepadKey.LEFT]: ["ArrowLeft"], - [GamepadKey.RIGHT]: ["ArrowRight"], - [GamepadKey.LS_UP]: ["KeyW"], - [GamepadKey.LS_DOWN]: ["KeyS"], - [GamepadKey.LS_LEFT]: ["KeyA"], - [GamepadKey.LS_RIGHT]: ["KeyD"], - [GamepadKey.RS_UP]: ["KeyI"], - [GamepadKey.RS_DOWN]: ["KeyK"], - [GamepadKey.RS_LEFT]: ["KeyJ"], - [GamepadKey.RS_RIGHT]: ["KeyL"], - [GamepadKey.A]: ["Space", "KeyE"], - [GamepadKey.X]: ["KeyR"], - [GamepadKey.B]: ["ControlLeft", "Backspace"], - [GamepadKey.Y]: ["KeyV"], - [GamepadKey.START]: ["Enter"], - [GamepadKey.SELECT]: ["Tab"], - [GamepadKey.LB]: ["KeyC", "KeyG"], - [GamepadKey.RB]: ["KeyQ"], - [GamepadKey.HOME]: ["Backquote"], - [GamepadKey.RT]: [MouseButtonCode.LEFT_CLICK], - [GamepadKey.LT]: [MouseButtonCode.RIGHT_CLICK], - [GamepadKey.L3]: ["ShiftLeft"], - [GamepadKey.R3]: ["KeyF"] - }, - mouse: { - [MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo[MouseMapTo.RS], - [MkbPresetKey.MOUSE_SENSITIVITY_X]: 50, - [MkbPresetKey.MOUSE_SENSITIVITY_Y]: 50, - [MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20, - [MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: 100, - [MkbPresetKey.MOUSE_STICK_DECAY_MIN]: 10 + window.localStorage.setItem(ControllerShortcut.#STORAGE_KEY, JSON.stringify(ControllerShortcut.#ACTIONS)); + console.log(ControllerShortcut.#ACTIONS); + } + static #updateProfileList(e) { + const $select = ControllerShortcut.#$selectProfile; + const $remap = ControllerShortcut.#$remap; + const $fragment = document.createDocumentFragment(); + while ($select.firstElementChild) { + $select.firstElementChild.remove(); } - }; - static convert(preset) { - const obj = { - mapping: {}, - mouse: Object.assign({}, preset.mouse) - }; - for (const buttonIndex in preset.mapping) { - for (const keyName of preset.mapping[parseInt(buttonIndex)]) { - obj.mapping[keyName] = parseInt(buttonIndex); + const gamepads = navigator.getGamepads(); + let hasGamepad = false; + for (const gamepad of gamepads) { + if (!gamepad || !gamepad.connected) { + continue; } + if (gamepad.id === MkbHandler.VIRTUAL_GAMEPAD_ID) { + continue; + } + hasGamepad = true; + const $option = CE("option", { value: gamepad.id }, gamepad.id); + $fragment.appendChild($option); } - const mouse2 = obj.mouse; - mouse2[MkbPresetKey.MOUSE_SENSITIVITY_X] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY; - mouse2[MkbPresetKey.MOUSE_SENSITIVITY_Y] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY; - mouse2[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT] *= MkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT; - mouse2[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH] *= 0.01; - mouse2[MkbPresetKey.MOUSE_STICK_DECAY_MIN] *= 0.01; - const mouseMapTo = MouseMapTo[mouse2[MkbPresetKey.MOUSE_MAP_TO]]; - if (typeof mouseMapTo !== "undefined") { - mouse2[MkbPresetKey.MOUSE_MAP_TO] = mouseMapTo; + if (hasGamepad) { + $select.appendChild($fragment); + $remap.classList.remove("bx-gone"); + $select.disabled = false; + $select.selectedIndex = 0; + $select.dispatchEvent(new Event("change")); } else { - mouse2[MkbPresetKey.MOUSE_MAP_TO] = MkbPreset.MOUSE_SETTINGS[MkbPresetKey.MOUSE_MAP_TO].default; + $remap.classList.add("bx-gone"); + $select.disabled = true; + const $option = CE("option", {}, "---"); + $fragment.appendChild($option); + $select.appendChild($fragment); } - console.log(obj); - return obj; + } + static #switchProfile(profile) { + let actions = ControllerShortcut.#ACTIONS[profile]; + if (!actions) { + actions = []; + } + let button; + for (button in ControllerShortcut.#$selectActions) { + const $select = ControllerShortcut.#$selectActions[button]; + $select.value = actions[button] || ""; + } + } + static renderSettings() { + ControllerShortcut.#ACTIONS = JSON.parse(window.localStorage.getItem(ControllerShortcut.#STORAGE_KEY) || "{}"); + const buttons = { + [GamepadKey.A]: PrompFont.A, + [GamepadKey.B]: PrompFont.B, + [GamepadKey.X]: PrompFont.X, + [GamepadKey.Y]: PrompFont.Y, + [GamepadKey.LB]: PrompFont.LB, + [GamepadKey.RB]: PrompFont.RB, + [GamepadKey.LT]: PrompFont.LT, + [GamepadKey.RT]: PrompFont.RT, + [GamepadKey.SELECT]: PrompFont.SELECT, + [GamepadKey.START]: PrompFont.START, + [GamepadKey.UP]: PrompFont.UP, + [GamepadKey.DOWN]: PrompFont.DOWN, + [GamepadKey.LEFT]: PrompFont.LEFT, + [GamepadKey.RIGHT]: PrompFont.RIGHT + }; + const actions = { + [t("stream")]: { + [ShortcutAction.STREAM_SCREENSHOT_CAPTURE]: [t("stream"), t("take-screenshot")], + [ShortcutAction.STREAM_STATS_TOGGLE]: [t("stream"), t("stats"), t("show-hide")], + [ShortcutAction.STREAM_MICROPHONE_TOGGLE]: [t("stream"), t("microphone"), t("toggle")], + [ShortcutAction.STREAM_MENU_TOGGLE]: [t("stream"), t("menu"), t("show")] + } + }; + const $baseSelect = CE("select", { autocomplete: "off" }, CE("option", { value: "" }, "---")); + for (const groupLabel in actions) { + const items = actions[groupLabel]; + if (!items) { + continue; + } + const $optGroup = CE("optgroup", { label: groupLabel }); + for (const action in items) { + let label = items[action]; + if (!label) { + continue; + } + if (Array.isArray(label)) { + label = label.join(" > "); + } + const $option = CE("option", { value: action }, label); + $optGroup.appendChild($option); + } + $baseSelect.appendChild($optGroup); + } + const $container = CE("div", {}); + const $profile = CE("select", { class: "bx-shortcut-profile", autocomplete: "off" }); + $profile.addEventListener("change", (e) => { + ControllerShortcut.#switchProfile($profile.value); + }); + $container.appendChild($profile); + const onActionChanged = (e) => { + const $target = e.target; + const profile = $profile.value; + const button2 = $target.dataset.button; + const action = $target.value; + ControllerShortcut.#updateAction(profile, button2, action); + }; + const $remap = CE("div", { class: "bx-gone" }); + let button; + for (button in buttons) { + const $row = CE("div", { class: "bx-shortcut-row" }); + const prompt2 = buttons[button]; + const $label = CE("label", { class: "bx-prompt" }, `${PrompFont.HOME} + ${prompt2}`); + const $select = $baseSelect.cloneNode(true); + $select.dataset.button = button.toString(); + $select.addEventListener("change", onActionChanged); + ControllerShortcut.#$selectActions[button] = $select; + $row.appendChild($label); + $row.appendChild($select); + $remap.appendChild($row); + } + $container.appendChild($remap); + ControllerShortcut.#$selectProfile = $profile; + ControllerShortcut.#$remap = $remap; + window.addEventListener("gamepadconnected", ControllerShortcut.#updateProfileList); + window.addEventListener("gamepaddisconnected", ControllerShortcut.#updateProfileList); + ControllerShortcut.#updateProfileList(); + return $container; + } +} + +// src/modules/game-bar/action-base.ts +class BaseGameBarAction { + constructor() { + } + reset() { + } +} + +// src/modules/game-bar/action-screenshot.ts +class ScreenshotAction extends BaseGameBarAction { + $content; + constructor() { + super(); + const onClick = (e) => { + BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); + Screenshot.takeScreenshot(); + }; + this.$content = createButton({ + style: ButtonStyle.GHOST, + icon: BxIcon.SCREENSHOT, + title: t("take-screenshot"), + onClick + }); + } + render() { + return this.$content; + } +} + +// src/modules/touch-controller.ts +var LOG_TAG2 = "TouchController"; + +class TouchController { + static #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent("message", { + data: JSON.stringify({ + content: '{"layoutId":""}', + target: "/streaming/touchcontrols/showlayoutv2", + type: "Message" + }), + origin: "better-xcloud" + }); + static #$style; + static #enable = false; + static #dataChannel; + static #customLayouts = {}; + static #baseCustomLayouts = {}; + static #currentLayoutId; + static #customList; + static enable() { + TouchController.#enable = true; + } + static disable() { + TouchController.#enable = false; + } + static isEnabled() { + return TouchController.#enable; + } + static #showDefault() { + TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER); + } + static #show() { + document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.remove("bx-offscreen"); + } + static #hide() { + document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.add("bx-offscreen"); + } + static toggleVisibility(status) { + if (!TouchController.#dataChannel) { + return; + } + status ? TouchController.#hide() : TouchController.#show(); + } + static reset() { + TouchController.#enable = false; + TouchController.#dataChannel = null; + TouchController.#$style && (TouchController.#$style.textContent = ""); + } + static #dispatchMessage(msg) { + TouchController.#dataChannel && window.setTimeout(() => { + TouchController.#dataChannel.dispatchEvent(msg); + }, 10); + } + static #dispatchLayouts(data) { + BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, { + data + }); + } + static async getCustomLayouts(xboxTitleId, retries = 1) { + if (xboxTitleId in TouchController.#customLayouts) { + TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]); + return; + } + retries = retries || 1; + if (retries > 2) { + TouchController.#customLayouts[xboxTitleId] = null; + window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000); + return; + } + const baseUrl = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts${BX_FLAGS.UseDevTouchLayout ? "/dev" : ""}`; + const url = `${baseUrl}/${xboxTitleId}.json`; + try { + const resp = await NATIVE_FETCH(url); + const json = await resp.json(); + const layouts = {}; + json.layouts.forEach(async (layoutName) => { + let baseLayouts = {}; + if (layoutName in TouchController.#baseCustomLayouts) { + baseLayouts = TouchController.#baseCustomLayouts[layoutName]; + } else { + try { + const layoutUrl = `${baseUrl}/layouts/${layoutName}.json`; + const resp2 = await NATIVE_FETCH(layoutUrl); + const json2 = await resp2.json(); + baseLayouts = json2.layouts; + TouchController.#baseCustomLayouts[layoutName] = baseLayouts; + } catch (e) { + } + } + Object.assign(layouts, baseLayouts); + }); + json.layouts = layouts; + TouchController.#customLayouts[xboxTitleId] = json; + window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000); + } catch (e) { + TouchController.getCustomLayouts(xboxTitleId, retries + 1); + } + } + static loadCustomLayout(xboxTitleId, layoutId, delay = 0) { + if (!window.BX_EXPOSED.touchLayoutManager) { + const listener = (e) => { + window.removeEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener); + if (TouchController.#enable) { + TouchController.loadCustomLayout(xboxTitleId, layoutId, 0); + } + }; + window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener); + return; + } + const layoutChanged = TouchController.#currentLayoutId !== layoutId; + TouchController.#currentLayoutId = layoutId; + const layoutData = TouchController.#customLayouts[xboxTitleId]; + if (!xboxTitleId || !layoutId || !layoutData) { + TouchController.#enable && TouchController.#showDefault(); + return; + } + const layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout]; + if (!layout) { + return; + } + let msg; + let html12 = false; + if (layout.author) { + const author = `${escapeHtml(layout.author)}`; + msg = t("touch-control-layout-by", { name: author }); + html12 = true; + } else { + msg = t("touch-control-layout"); + } + layoutChanged && Toast.show(msg, layout.name, { html: html12 }); + window.setTimeout(() => { + window.BX_EXPOSED.shouldShowSensorControls = JSON.stringify(layout).includes("gyroscope"); + window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({ + type: "showLayout", + scope: xboxTitleId, + subscope: "base", + layout: { + id: "System.Standard", + displayName: "System", + layoutFile: layout + } + }); + }, delay); + } + static updateCustomList() { + const key = "better_xcloud_custom_touch_layouts"; + TouchController.#customList = JSON.parse(window.localStorage.getItem(key) || "[]"); + NATIVE_FETCH("https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/ids.json").then((response) => response.json()).then((json) => { + TouchController.#customList = json; + window.localStorage.setItem(key, JSON.stringify(json)); + }); + } + static getCustomList() { + return TouchController.#customList; + } + static setup() { + window.testTouchLayout = (layout) => { + const { touchLayoutManager } = window.BX_EXPOSED; + touchLayoutManager && touchLayoutManager.changeLayoutForScope({ + type: "showLayout", + scope: "" + STATES.currentStream?.xboxTitleId, + subscope: "base", + layout: { + id: "System.Standard", + displayName: "Custom", + layoutFile: layout + } + }); + }; + const $style = document.createElement("style"); + document.documentElement.appendChild($style); + TouchController.#$style = $style; + const PREF_STYLE_STANDARD = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD); + const PREF_STYLE_CUSTOM = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM); + window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { + const dataChannel = e.dataChannel; + if (!dataChannel || dataChannel.label !== "message") { + return; + } + let filter = ""; + if (TouchController.#enable) { + if (PREF_STYLE_STANDARD === "white") { + filter = "grayscale(1) brightness(2)"; + } else if (PREF_STYLE_STANDARD === "muted") { + filter = "sepia(0.5)"; + } + } else if (PREF_STYLE_CUSTOM === "muted") { + filter = "sepia(0.5)"; + } + if (filter) { + $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`; + } else { + $style.textContent = ""; + } + TouchController.#dataChannel = dataChannel; + dataChannel.addEventListener("open", () => { + window.setTimeout(TouchController.#show, 1000); + }); + let focused = false; + dataChannel.addEventListener("message", (msg) => { + if (msg.origin === "better-xcloud" || typeof msg.data !== "string") { + return; + } + if (msg.data.includes("touchcontrols/showtitledefault")) { + if (TouchController.#enable) { + if (focused) { + TouchController.getCustomLayouts(STATES.currentStream?.xboxTitleId); + } else { + TouchController.#showDefault(); + } + } + return; + } + try { + if (msg.data.includes("/titleinfo")) { + const json = JSON.parse(JSON.parse(msg.data).content); + focused = json.focused; + if (!json.focused) { + TouchController.#show(); + } + STATES.currentStream.xboxTitleId = parseInt(json.titleid, 16).toString(); + } + } catch (e2) { + BxLogger.error(LOG_TAG2, "Load custom layout", e2); + } + }); + }); + } +} + +// src/modules/game-bar/action-touch-control.ts +class TouchControlAction extends BaseGameBarAction { + $content; + constructor() { + super(); + const onClick = (e) => { + BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); + const $parent = e.target.closest("div[data-enabled]"); + let enabled = $parent.getAttribute("data-enabled", "true") === "true"; + $parent.setAttribute("data-enabled", (!enabled).toString()); + TouchController.toggleVisibility(enabled); + }; + const $btnEnable = createButton({ + style: ButtonStyle.GHOST, + icon: BxIcon.TOUCH_CONTROL_ENABLE, + title: t("show-touch-controller"), + onClick, + classes: ["bx-activated"] + }); + const $btnDisable = createButton({ + style: ButtonStyle.GHOST, + icon: BxIcon.TOUCH_CONTROL_DISABLE, + title: t("hide-touch-controller"), + onClick + }); + this.$content = CE("div", {}, $btnEnable, $btnDisable); + this.reset(); + } + render() { + return this.$content; + } + reset() { + this.$content.setAttribute("data-enabled", "true"); + } +} + +// src/modules/game-bar/action-microphone.ts +var MicrophoneState; +(function(MicrophoneState2) { + MicrophoneState2["REQUESTED"] = "Requested"; + MicrophoneState2["ENABLED"] = "Enabled"; + MicrophoneState2["MUTED"] = "Muted"; + MicrophoneState2["NOT_ALLOWED"] = "NotAllowed"; + MicrophoneState2["NOT_FOUND"] = "NotFound"; +})(MicrophoneState || (MicrophoneState = {})); + +class MicrophoneAction extends BaseGameBarAction { + $content; + visible = false; + constructor() { + super(); + const onClick = (e) => { + BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); + const enabled = MicrophoneShortcut.toggle(false); + this.$content.setAttribute("data-enabled", enabled.toString()); + }; + const $btnDefault = createButton({ + style: ButtonStyle.GHOST, + icon: BxIcon.MICROPHONE, + title: t("show-touch-controller"), + onClick, + classes: ["bx-activated"] + }); + const $btnMuted = createButton({ + style: ButtonStyle.GHOST, + icon: BxIcon.MICROPHONE_MUTED, + title: t("hide-touch-controller"), + onClick + }); + this.$content = CE("div", {}, $btnDefault, $btnMuted); + this.reset(); + window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, (e) => { + const microphoneState = e.microphoneState; + const enabled = microphoneState === MicrophoneState.ENABLED; + this.$content.setAttribute("data-enabled", enabled.toString()); + this.$content.classList.remove("bx-gone"); + }); + } + render() { + return this.$content; + } + reset() { + this.visible = false; + this.$content.classList.add("bx-gone"); + this.$content.setAttribute("data-enabled", "false"); + } +} + +// src/modules/game-bar/game-bar.ts +class GameBar { + static instance; + static getInstance() { + if (!GameBar.instance) { + GameBar.instance = new GameBar; + } + return GameBar.instance; + } + static VISIBLE_DURATION = 2000; + $gameBar; + $container; + timeout = null; + actions = []; + constructor() { + let $container; + const position = getPref(PrefKey.GAME_BAR_POSITION); + const $gameBar = CE("div", { id: "bx-game-bar", class: "bx-gone", "data-position": position }, $container = CE("div", { class: "bx-game-bar-container bx-offscreen" }), createSvgIcon(position === "bottom-left" ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT)); + this.actions = [ + new ScreenshotAction, + ...STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== "off" ? [new TouchControlAction] : [], + new MicrophoneAction + ]; + if (position === "bottom-right") { + this.actions.reverse(); + } + for (const action of this.actions) { + $container.appendChild(action.render()); + } + $gameBar.addEventListener("click", (e) => { + if (e.target !== $gameBar) { + return; + } + $container.classList.contains("bx-show") ? this.hideBar() : this.showBar(); + }); + window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar.bind(this)); + $container.addEventListener("pointerover", this.clearHideTimeout.bind(this)); + $container.addEventListener("pointerout", this.beginHideTimeout.bind(this)); + $container.addEventListener("transitionend", (e) => { + const classList = $container.classList; + if (classList.contains("bx-hide")) { + classList.remove("bx-offscreen", "bx-hide"); + classList.add("bx-offscreen"); + } + }); + document.documentElement.appendChild($gameBar); + this.$gameBar = $gameBar; + this.$container = $container; + } + beginHideTimeout() { + this.clearHideTimeout(); + this.timeout = window.setTimeout(() => { + this.timeout = null; + this.hideBar(); + }, GameBar.VISIBLE_DURATION); + } + clearHideTimeout() { + this.timeout && clearTimeout(this.timeout); + this.timeout = null; + } + enable() { + this.$gameBar && this.$gameBar.classList.remove("bx-gone"); + } + disable() { + this.hideBar(); + this.$gameBar && this.$gameBar.classList.add("bx-gone"); + } + showBar() { + if (!this.$container) { + return; + } + this.$container.classList.remove("bx-offscreen", "bx-hide"); + this.$container.classList.add("bx-show"); + this.beginHideTimeout(); + } + hideBar() { + if (!this.$container) { + return; + } + this.$container.classList.remove("bx-show"); + this.$container.classList.add("bx-hide"); + } + reset() { + for (const action of this.actions) { + action.reset(); + } + } +} + +// src/utils/bx-exposed.ts +var InputType; +(function(InputType2) { + InputType2["CONTROLLER"] = "Controller"; + InputType2["MKB"] = "MKB"; + InputType2["CUSTOM_TOUCH_OVERLAY"] = "CustomTouchOverlay"; + InputType2["GENERIC_TOUCH"] = "GenericTouch"; + InputType2["NATIVE_TOUCH"] = "NativeTouch"; + InputType2["BATIVE_SENSOR"] = "NativeSensor"; +})(InputType || (InputType = {})); +var BxExposed = { + onPollingModeChanged: (mode) => { + if (getPref(PrefKey.GAME_BAR_POSITION) === "off") { + return; + } + const gameBar = GameBar.getInstance(); + if (!STATES.isPlaying) { + gameBar.disable(); + return; + } + mode !== "None" ? gameBar.disable() : gameBar.enable(); + }, + getTitleInfo: () => STATES.currentStream.titleInfo, + modifyTitleInfo: (titleInfo) => { + titleInfo = structuredClone(titleInfo); + let supportedInputTypes = titleInfo.details.supportedInputTypes; + if (getPref(PrefKey.NATIVE_MKB_DISABLED) || UserAgent.isMobile()) { + supportedInputTypes = supportedInputTypes.filter((i) => i !== InputType.MKB); + } + titleInfo.details.hasMkbSupport = supportedInputTypes.includes(InputType.MKB); + if (STATES.hasTouchSupport) { + let touchControllerAvailability = getPref(PrefKey.STREAM_TOUCH_CONTROLLER); + if (touchControllerAvailability !== "off" && getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) { + const gamepads = window.navigator.getGamepads(); + let gamepadFound = false; + for (let gamepad of gamepads) { + if (gamepad && gamepad.connected) { + gamepadFound = true; + break; + } + } + gamepadFound && (touchControllerAvailability = "off"); + } + if (touchControllerAvailability === "off") { + supportedInputTypes = supportedInputTypes.filter((i) => i !== InputType.CUSTOM_TOUCH_OVERLAY && i !== InputType.GENERIC_TOUCH); + } + titleInfo.details.hasTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH) || supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY) || supportedInputTypes.includes(InputType.GENERIC_TOUCH); + if (!titleInfo.details.hasTouchSupport && touchControllerAvailability === "all") { + titleInfo.details.hasFakeTouchSupport = true; + supportedInputTypes.push(InputType.GENERIC_TOUCH); + } + } + titleInfo.details.supportedInputTypes = supportedInputTypes; + STATES.currentStream.titleInfo = titleInfo; + BxEvent.dispatch(window, BxEvent.TITLE_INFO_READY); + return titleInfo; + }, + setupGainNode: ($media, audioStream) => { + if ($media instanceof HTMLAudioElement) { + $media.muted = true; + $media.addEventListener("playing", (e) => { + $media.muted = true; + $media.pause(); + }); + } else { + $media.muted = true; + $media.addEventListener("playing", (e) => { + $media.muted = true; + }); + } + const audioCtx = STATES.currentStream.audioContext; + const source = audioCtx.createMediaStreamSource(audioStream); + const gainNode = audioCtx.createGain(); + source.connect(gainNode).connect(audioCtx.destination); + }, + handleControllerShortcut: ControllerShortcut.handle, + resetControllerShortcut: ControllerShortcut.reset +}; + +// src/utils/region.ts +function getPreferredServerRegion(shortName = false) { + let preferredRegion = getPref(PrefKey.SERVER_REGION); + if (preferredRegion in STATES.serverRegions) { + if (shortName && STATES.serverRegions[preferredRegion].shortName) { + return STATES.serverRegions[preferredRegion].shortName; + } else { + return preferredRegion; + } + } + for (let regionName in STATES.serverRegions) { + const region = STATES.serverRegions[regionName]; + if (!region.isDefault) { + continue; + } + if (shortName && region.shortName) { + return region.shortName; + } else { + return regionName; + } + } + return "???"; +} + +// src/modules/loading-screen.ts +class LoadingScreen { + static #$bgStyle; + static #$waitTimeBox; + static #waitTimeInterval = null; + static #orgWebTitle; + static #secondsToString(seconds) { + const m = Math.floor(seconds / 60); + const s = Math.floor(seconds % 60); + const mDisplay = m > 0 ? `${m}m` : ""; + const sDisplay = `${s}s`.padStart(s >= 0 ? 3 : 4, "0"); + return mDisplay + sDisplay; + } + static setup() { + const titleInfo = STATES.currentStream.titleInfo; + if (!titleInfo) { + return; + } + if (!LoadingScreen.#$bgStyle) { + const $bgStyle = CE("style"); + document.documentElement.appendChild($bgStyle); + LoadingScreen.#$bgStyle = $bgStyle; + } + LoadingScreen.#setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl); + if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === "hide") { + LoadingScreen.#hideRocket(); + } + } + static #hideRocket() { + let $bgStyle = LoadingScreen.#$bgStyle; + const css = ` +#game-stream div[class*=RocketAnimation-module__container] > svg { + display: none; +} +`; + $bgStyle.textContent += css; + } + static #setBackground(imageUrl) { + let $bgStyle = LoadingScreen.#$bgStyle; + imageUrl = imageUrl + "?w=1920"; + const css = ` +#game-stream { + background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important; + background-color: transparent !important; + background-position: center center !important; + background-repeat: no-repeat !important; + background-size: cover !important; +} + +#game-stream rect[width="800"] { + transition: opacity 0.3s ease-in-out !important; +} +`; + $bgStyle.textContent += css; + const bg = new Image; + bg.onload = (e) => { + $bgStyle.textContent += ` +#game-stream rect[width="800"] { + opacity: 0 !important; +} +`; + }; + bg.src = imageUrl; + } + static setupWaitTime(waitTime) { + if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === "hide-queue") { + LoadingScreen.#hideRocket(); + } + let secondsLeft = waitTime; + let $countDown; + let $estimated; + LoadingScreen.#orgWebTitle = document.title; + const endDate = new Date; + const timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60; + endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds); + let endDateStr = endDate.toISOString().slice(0, 19); + endDateStr = endDateStr.substring(0, 10) + " " + endDateStr.substring(11, 19); + endDateStr += ` (${LoadingScreen.#secondsToString(waitTime)})`; + let $waitTimeBox = LoadingScreen.#$waitTimeBox; + if (!$waitTimeBox) { + $waitTimeBox = CE("div", { class: "bx-wait-time-box" }, CE("label", {}, t("server")), CE("span", {}, getPreferredServerRegion()), CE("label", {}, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", {}, t("wait-time-countdown")), $countDown = CE("span", {})); + document.documentElement.appendChild($waitTimeBox); + LoadingScreen.#$waitTimeBox = $waitTimeBox; + } else { + $waitTimeBox.classList.remove("bx-gone"); + $estimated = $waitTimeBox.querySelector(".bx-wait-time-estimated"); + $countDown = $waitTimeBox.querySelector(".bx-wait-time-countdown"); + } + $estimated.textContent = endDateStr; + $countDown.textContent = LoadingScreen.#secondsToString(secondsLeft); + document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`; + LoadingScreen.#waitTimeInterval = window.setInterval(() => { + secondsLeft--; + $countDown.textContent = LoadingScreen.#secondsToString(secondsLeft); + document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`; + if (secondsLeft <= 0) { + LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval); + LoadingScreen.#waitTimeInterval = null; + } + }, 1000); + } + static hide() { + LoadingScreen.#orgWebTitle && (document.title = LoadingScreen.#orgWebTitle); + LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add("bx-gone"); + if (getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.#$bgStyle) { + const $rocketBg = document.querySelector('#game-stream rect[width="800"]'); + $rocketBg && $rocketBg.addEventListener("transitionend", (e) => { + LoadingScreen.#$bgStyle.textContent += ` +#game-stream { + background: #000 !important; +} +`; + }); + LoadingScreen.#$bgStyle.textContent += ` +#game-stream rect[width="800"] { + opacity: 1 !important; +} +`; + } + LoadingScreen.reset(); + } + static reset() { + LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add("bx-gone"); + LoadingScreen.#$bgStyle && (LoadingScreen.#$bgStyle.textContent = ""); + LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval); + LoadingScreen.#waitTimeInterval = null; + } +} + +// src/modules/dialog.ts +class Dialog { + $dialog; + $title; + $content; + $overlay; + onClose; + constructor(options) { + const { + title, + className, + content, + hideCloseButton, + onClose, + helpUrl + } = options; + const $overlay = document.querySelector(".bx-dialog-overlay"); + if (!$overlay) { + this.$overlay = CE("div", { class: "bx-dialog-overlay bx-gone" }); + this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()); + document.documentElement.appendChild(this.$overlay); + } else { + this.$overlay = $overlay; + } + let $close; + this.onClose = onClose; + this.$dialog = CE("div", { class: `bx-dialog ${className || ""} bx-gone` }, this.$title = CE("h2", {}, CE("b", {}, title), helpUrl && createButton({ + icon: BxIcon.QUESTION, + style: ButtonStyle.GHOST, + title: t("help"), + url: helpUrl + })), this.$content = CE("div", { class: "bx-dialog-content" }, content), !hideCloseButton && ($close = CE("button", { type: "button" }, t("close")))); + $close && $close.addEventListener("click", (e) => { + this.hide(e); + }); + !title && this.$title.classList.add("bx-gone"); + !content && this.$content.classList.add("bx-gone"); + this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()); + document.documentElement.appendChild(this.$dialog); + } + show(newOptions) { + document.activeElement && document.activeElement.blur(); + if (newOptions && newOptions.title) { + this.$title.querySelector("b").textContent = newOptions.title; + this.$title.classList.remove("bx-gone"); + } + this.$dialog.classList.remove("bx-gone"); + this.$overlay.classList.remove("bx-gone"); + document.body.classList.add("bx-no-scroll"); + } + hide(e) { + this.$dialog.classList.add("bx-gone"); + this.$overlay.classList.add("bx-gone"); + document.body.classList.remove("bx-no-scroll"); + this.onClose && this.onClose(e); + } + toggle() { + this.$dialog.classList.toggle("bx-gone"); + this.$overlay.classList.toggle("bx-gone"); } } @@ -4510,13 +4802,24 @@ var setupQuickSettingsBar = function() { } ] }, + { + icon: BxIcon.COMMAND, + group: "shortcuts", + items: [ + { + group: "shortcuts_controller", + label: t("controller-shortcuts"), + content: ControllerShortcut.renderSettings() + } + ] + }, { icon: BxIcon.STREAM_STATS, group: "stats", items: [ { group: "stats", - label: t("menu-stream-stats"), + label: t("stream-stats"), help_url: "https://better-xcloud.github.io/stream-stats/", items: [ { @@ -4696,8 +4999,19 @@ var resizeVideoPlayer = function() { $video.style.objectFit = PREF_RATIO; } }; +var preloadFonts = function() { + const $link = CE("link", { + rel: "preload", + href: "https://redphx.github.io/better-xcloud/fonts/promptfont.otf", + as: "font", + type: "font/otf", + crossorigin: "" + }); + document.querySelector("head")?.appendChild($link); +}; function setupStreamUi() { if (!document.querySelector(".bx-quick-settings-bar")) { + preloadFonts(); window.addEventListener("resize", updateVideoPlayerCss); setupQuickSettingsBar(); StreamStats.render(); @@ -6517,6 +6831,24 @@ div[data-testid=media-container].bx-taking-screenshot:before { font-style: italic; padding-top: 16px; } +.bx-quick-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-profile { + width: 100%; + height: 36px; + display: block; + margin-bottom: 10px; +} +.bx-quick-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row { + display: flex; + margin-bottom: 10px; +} +.bx-quick-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row label.bx-prompt { + flex: 1; + font-family: var(--bx-promptfont-font); + font-size: 26px; +} +.bx-quick-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row select { + flex: 2; +} .bx-mkb-settings { display: flex; flex-direction: column; @@ -6839,6 +7171,30 @@ function hashCode(str2) { } return hash; } +function renderString(str2, obj) { + return str2.replace(/\$\{.+?\}/g, (match) => { + const key = match.substring(2, match.length - 1); + if (key in obj) { + return obj[key]; + } + return match; + }); +} + +// src/modules/patches/controller-shortcuts.js +var controller_shortcuts_default = "const currentGamepad = ${gamepadVar};\n\n// Share button on XS controller\nif (currentGamepad.buttons[17] && currentGamepad.buttons[17].value === 1) {\n window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));\n}\n\nconst btnHome = currentGamepad.buttons[16];\nif (btnHome) {\n if (!this.bxHomeStates) {\n this.bxHomeStates = {};\n }\n\n if (btnHome.pressed) {\n this.gamepadIsIdle.set(currentGamepad.index, false);\n\n if (this.bxHomeStates[currentGamepad.index]) {\n const lastTimestamp = this.bxHomeStates[currentGamepad.index].timestamp;\n\n if (currentGamepad.timestamp !== lastTimestamp) {\n this.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp;\n\n const handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad);\n if (handled) {\n this.bxHomeStates[currentGamepad.index].shortcutPressed += 1;\n }\n }\n } else {\n // First time pressing > save current timestamp\n window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index);\n this.bxHomeStates[currentGamepad.index] = {\n shortcutPressed: 0,\n timestamp: currentGamepad.timestamp,\n };\n }\n\n // Listen to next button press\n const intervalMs = 50;\n this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs);\n\n // Hijack this button\n return;\n } else if (this.bxHomeStates[currentGamepad.index]) {\n const info = structuredClone(this.bxHomeStates[currentGamepad.index]);\n\n // Home button released\n this.bxHomeStates[currentGamepad.index] = null;\n\n if (info.shortcutPressed === 0) {\n const fakeGamepadMappings = [{\n GamepadIndex: 0,\n A: 0,\n B: 0,\n X: 0,\n Y: 0,\n LeftShoulder: 0,\n RightShoulder: 0,\n LeftTrigger: 0,\n RightTrigger: 0,\n View: 0,\n Menu: 0,\n LeftThumb: 0,\n RightThumb: 0,\n DPadUp: 0,\n DPadDown: 0,\n DPadLeft: 0,\n DPadRight: 0,\n Nexus: 1,\n LeftThumbXAxis: 0,\n LeftThumbYAxis: 0,\n RightThumbXAxis: 0,\n RightThumbYAxis: 0,\n PhysicalPhysicality: 0,\n VirtualPhysicality: 0,\n Dirty: true,\n Virtual: false,\n }];\n\n const isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500;\n const intervalMs = isLongPress ? 500 : 100;\n\n this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings);\n this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs);\n return;\n }\n }\n}\n"; + +// src/modules/patches/local-co-op-enable.js +var local_co_op_enable_default = "let match;\nlet onGamepadChangedStr = this.onGamepadChanged.toString();\n\nonGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]');\neval(`this.onGamepadChanged = function ${onGamepadChangedStr}`);\n\nlet onGamepadInputStr = this.onGamepadInput.toString();\n\nmatch = onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/);\nif (match) {\n const gamepadIndexVar = match[0];\n onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', `this.gamepadStates.get(${gamepadIndexVar},`);\n eval(`this.onGamepadInput = function ${onGamepadInputStr}`);\n BxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support');\n} else {\n BxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support');\n}\n"; + +// src/modules/patches/remote-play-enable.js +var remote_play_enable_default = "connectMode: window.BX_REMOTE_PLAY_CONFIG ? \"xhome-connect\" : \"cloud-connect\",\nremotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',\n"; + +// src/modules/patches/remote-play-keep-alive.js +var remote_play_keep_alive_default = "const msg = JSON.parse(e);\nif (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) {\n try {\n this.sendKeepAlive();\n return;\n } catch (ex) { console.log(ex); }\n}\n"; + +// src/modules/patches/vibration-adjust.js +var vibration_adjust_default = "if (!window.BX_ENABLE_CONTROLLER_VIBRATION) {\n return void(0);\n}\n\nconst intensity = window.BX_VIBRATION_INTENSITY;\nif (intensity && intensity < 1) {\n e.leftMotorPercent *= intensity;\n e.rightMotorPercent *= intensity;\n e.leftTriggerMotorPercent *= intensity;\n e.rightTriggerMotorPercent *= intensity;\n}\n"; // src/modules/patcher.ts var ENDING_CHUNKS_PATCH_NAME = "loadingEndingChunks"; @@ -6903,26 +7259,19 @@ var PATCHES = { return str2.replace(str2.substring(index - 9, index + 15), "https://www.xbox.com/play"); }, remotePlayKeepAlive(str2) { - if (!str2.includes("onServerDisconnectMessage(e){")) { - return false; - } - str2 = str2.replace("onServerDisconnectMessage(e){", `onServerDisconnectMessage(e) { - const msg = JSON.parse(e); - if (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) { - try { - this.sendKeepAlive(); - return; - } catch (ex) { console.log(ex); } - } - `); - return str2; - }, - remotePlayConnectMode(str2) { - const text = 'connectMode:"cloud-connect"'; + const text = "onServerDisconnectMessage(e){"; if (!str2.includes(text)) { return false; } - return str2.replace(text, `connectMode:window.BX_REMOTE_PLAY_CONFIG?"xhome-connect":"cloud-connect",remotePlayServerId:(window.BX_REMOTE_PLAY_CONFIG&&window.BX_REMOTE_PLAY_CONFIG.serverId)||''`); + str2 = str2.replace(text, text + remote_play_keep_alive_default); + return str2; + }, + remotePlayConnectMode(str2) { + const text = 'connectMode:"cloud-connect",'; + if (!str2.includes(text)) { + return false; + } + return str2.replace(text, remote_play_enable_default); }, remotePlayDisableAchievementToast(str2) { const text = ".AchievementUnlock:{"; @@ -6966,11 +7315,9 @@ if (!!window.BX_REMOTE_PLAY_CONFIG) { const match = codeBlock.match(/this\.gamepadTimestamps\.set\((\w+)\.index/); if (match) { const gamepadVar = match[1]; - const newCode = ` -if (${gamepadVar}.buttons[17] && ${gamepadVar}.buttons[17].value === 1) { - window.dispatchEvent(new Event('${BxEvent.CAPTURE_SCREENSHOT}')); -} -`; + const newCode = renderString(controller_shortcuts_default, { + gamepadVar + }); codeBlock = codeBlock.replace("this.gamepadTimestamps.set", newCode + "this.gamepadTimestamps.set"); } return str2.substring(0, index) + codeBlock + str2.substring(nextIndex); @@ -6996,19 +7343,8 @@ if (${gamepadVar}.buttons[17] && ${gamepadVar}.buttons[17].value === 1) { if (!str2.includes(text)) { return false; } - const newCode = ` -if (!window.BX_ENABLE_CONTROLLER_VIBRATION) { - return void(0); -} -if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) { - e.leftMotorPercent = e.leftMotorPercent * window.BX_VIBRATION_INTENSITY; - e.rightMotorPercent = e.rightMotorPercent * window.BX_VIBRATION_INTENSITY; - e.leftTriggerMotorPercent = e.leftTriggerMotorPercent * window.BX_VIBRATION_INTENSITY; - e.rightTriggerMotorPercent = e.rightTriggerMotorPercent * window.BX_VIBRATION_INTENSITY; -} -`; VibrationManager.updateGlobalVars(); - str2 = str2.replaceAll(text, text + newCode); + str2 = str2.replaceAll(text, text + vibration_adjust_default); return str2; }, overrideSettings(str2) { @@ -7078,26 +7414,7 @@ window.dispatchEvent(new Event("${BxEvent.TOUCH_LAYOUT_MANAGER_READY}")); if (!str2.includes(text)) { return false; } - let patchstr = ` -let match; -let onGamepadChangedStr = this.onGamepadChanged.toString(); - -onGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]'); -eval(\`this.onGamepadChanged = function \${onGamepadChangedStr}\`); - -let onGamepadInputStr = this.onGamepadInput.toString(); - -match = onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/); -if (match) { - const gamepadIndexVar = match[0]; - onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', \`this.gamepadStates.get(\${gamepadIndexVar},\`); - eval(\`this.onGamepadInput = function \${onGamepadInputStr}\`); - BxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support'); -} else { - BxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support'); -} -`; - const newCode = `true; ${patchstr}; true,`; + const newCode = `true; ${local_co_op_enable_default}; true,`; str2 = str2.replace(text, text + newCode); return str2; }, @@ -7159,10 +7476,16 @@ if (!!window.BX_REMOTE_PLAY_CONFIG) { if (!str2.includes(text)) { return false; } - str2 = str2.replace(text, "e.guideUI = null;" + text); + let newCode = ` +// Expose onShowStreamMenu +window.BX_EXPOSED.showStreamMenu = e.onShowStreamMenu; +// Restore the "..." button +e.guideUI = null; +`; if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "off") { - str2 = str2.replace(text, "e.canShowTakHUD = false;" + text); + newCode += "e.canShowTakHUD = false;"; } + str2 = str2.replace(text, newCode + text); return str2; }, broadcastPollingMode(str2) {