From 8f6bc5cb1b5ba9f2624b8ee860b30c48168c8193 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:22:33 +0700 Subject: [PATCH] Detach VIRTUAL_GAMEPAD_ID from EmulatedMkbHandler --- dist/better-xcloud.lite.user.js | 1340 ++++++++------------ dist/better-xcloud.user.js | 1161 +++++++++-------- src/modules/controller-shortcut.ts | 4 +- src/modules/mkb/mkb-handler.ts | 5 +- src/modules/ui/dialog/navigation-dialog.ts | 4 +- src/modules/ui/dialog/settings-dialog.ts | 2 +- src/utils/gamepad.ts | 4 +- src/utils/xhome-interceptor.ts | 2 - 8 files changed, 1109 insertions(+), 1413 deletions(-) diff --git a/dist/better-xcloud.lite.user.js b/dist/better-xcloud.lite.user.js index 3488c43..d473586 100644 --- a/dist/better-xcloud.lite.user.js +++ b/dist/better-xcloud.lite.user.js @@ -1732,6 +1732,208 @@ var MouseMapTo; MouseMapTo2[MouseMapTo2.LS = 1] = "LS"; MouseMapTo2[MouseMapTo2.RS = 2] = "RS"; })(MouseMapTo ||= {}); +class Toast { + static #$wrapper; + static #$msg; + static #$status; + static #stack = []; + static #isShowing = !1; + static #timeout; + static #DURATION = 3000; + static show(msg, status, options = {}) { + options = options || {}; + const args = Array.from(arguments); + if (options.instant) Toast.#stack = [args], Toast.#showNext(); + else Toast.#stack.push(args), !Toast.#isShowing && Toast.#showNext(); + } + static #showNext() { + if (!Toast.#stack.length) { + Toast.#isShowing = !1; + return; + } + Toast.#isShowing = !0, Toast.#timeout && clearTimeout(Toast.#timeout), Toast.#timeout = window.setTimeout(Toast.#hide, Toast.#DURATION); + const [msg, status, options] = Toast.#stack.shift(); + if (options && options.html) Toast.#$msg.innerHTML = msg; + else Toast.#$msg.textContent = msg; + if (status) Toast.#$status.classList.remove("bx-gone"), Toast.#$status.textContent = status; + else Toast.#$status.classList.add("bx-gone"); + const classList = Toast.#$wrapper.classList; + classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); + } + static #hide() { + Toast.#timeout = null; + const classList = Toast.#$wrapper.classList; + classList.remove("bx-show"), classList.add("bx-hide"); + } + static setup() { + Toast.#$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, Toast.#$msg = CE("span", { class: "bx-toast-msg" }), Toast.#$status = CE("span", { class: "bx-toast-status" })), Toast.#$wrapper.addEventListener("transitionend", (e) => { + const classList = Toast.#$wrapper.classList; + if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), Toast.#showNext(); + }), document.documentElement.appendChild(Toast.#$wrapper); + } +} +function ceilToNearest(value, interval) { + return Math.ceil(value / interval) * interval; +} +function floorToNearest(value, interval) { + return Math.floor(value / interval) * interval; +} +async function copyToClipboard(text, showToast = !0) { + try { + return await navigator.clipboard.writeText(text), showToast && Toast.show("Copied to clipboard", "", { instant: !0 }), !0; + } catch (err) { + console.error("Failed to copy: ", err), showToast && Toast.show("Failed to copy", "", { instant: !0 }); + } + return !1; +} +function productTitleToSlug(title) { + return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase(); +} +class SoundShortcut { + static adjustGainNodeVolume(amount) { + if (!getPref("audio_enable_volume_control")) return 0; + const currentValue = getPref("audio_volume"); + let nearestValue; + if (amount > 0) nearestValue = ceilToNearest(currentValue, amount); + else nearestValue = floorToNearest(currentValue, -1 * amount); + let newValue; + if (currentValue !== nearestValue) newValue = nearestValue; + else newValue = currentValue + amount; + return newValue = setPref("audio_volume", newValue, !0), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue; + } + static setGainNodeVolume(value) { + STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); + } + static muteUnmute() { + if (getPref("audio_enable_volume_control") && STATES.currentStream.audioGainNode) { + const gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio_volume"); + let targetValue; + if (settingValue === 0) targetValue = 100, setPref("audio_volume", targetValue, !0); + else if (gainValue === 0) targetValue = settingValue; + else targetValue = 0; + let status; + if (targetValue === 0) status = t("muted"); + else status = targetValue + "%"; + SoundShortcut.setGainNodeVolume(targetValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { + speakerState: targetValue === 0 ? 1 : 0 + }); + return; + } + let $media; + if ($media = document.querySelector("div[data-testid=media-container] audio"), !$media) $media = document.querySelector("div[data-testid=media-container] video"); + if ($media) { + $media.muted = !$media.muted; + const status = $media.muted ? t("muted") : t("unmuted"); + Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { + speakerState: $media.muted ? 1 : 0 + }); + } + } +} +class BxSelectElement { + static wrap($select) { + $select.removeAttribute("tabindex"); + const $btnPrev = createButton({ + label: "<", + style: 32 + }), $btnNext = createButton({ + label: ">", + style: 32 + }), isMultiple = $select.multiple; + let $checkBox, $label, visibleIndex = $select.selectedIndex, $content; + if (isMultiple) $content = CE("button", { + class: "bx-select-value bx-focusable", + tabindex: 0 + }, $checkBox = CE("input", { type: "checkbox" }), $label = CE("span", {}, "")), $content.addEventListener("click", (e) => { + $checkBox.click(); + }), $checkBox.addEventListener("input", (e) => { + const $option = getOptionAtIndex(visibleIndex); + $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); + }); + else $content = CE("div", {}, $label = CE("label", { for: $select.id + "_checkbox" }, "")); + const getOptionAtIndex = (index) => { + return Array.from($select.querySelectorAll("option"))[index]; + }, render = (e) => { + if (e && e.manualTrigger) visibleIndex = $select.selectedIndex; + visibleIndex = normalizeIndex(visibleIndex); + const $option = getOptionAtIndex(visibleIndex); + let content = ""; + if ($option) if (content = $option.textContent || "", content && $option.parentElement.tagName === "OPTGROUP") { + $label.innerHTML = ""; + const fragment = document.createDocumentFragment(); + fragment.appendChild(CE("span", {}, $option.parentElement.label)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment); + } else $label.textContent = content; + else $label.textContent = content; + if ($label.classList.toggle("bx-line-through", $option && $option.disabled), isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); + const disablePrev = visibleIndex <= 0, disableNext = visibleIndex === $select.querySelectorAll("option").length - 1; + $btnPrev.classList.toggle("bx-inactive", disablePrev), $btnNext.classList.toggle("bx-inactive", disableNext), disablePrev && !disableNext && document.activeElement === $btnPrev && $btnNext.focus(), disableNext && !disablePrev && document.activeElement === $btnNext && $btnPrev.focus(); + }, normalizeIndex = (index) => { + return Math.min(Math.max(index, 0), $select.querySelectorAll("option").length - 1); + }, onPrevNext = (e) => { + if (!e.target) return; + const goNext = e.target.closest("button") === $btnNext, currentIndex = visibleIndex; + let newIndex = goNext ? currentIndex + 1 : currentIndex - 1; + if (newIndex = normalizeIndex(newIndex), visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; + if (isMultiple) render(); + else BxEvent.dispatch($select, "input"); + }; + $select.addEventListener("input", render), $btnPrev.addEventListener("click", onPrevNext), $btnNext.addEventListener("click", onPrevNext), new MutationObserver((mutationList, observer2) => { + mutationList.forEach((mutation) => { + if (mutation.type === "childList" || mutation.type === "attributes") render(); + }); + }).observe($select, { + subtree: !0, + childList: !0, + attributes: !0 + }), render(); + const $div = CE("div", { + class: "bx-select", + _nearby: { + orientation: "horizontal", + focus: $btnNext + } + }, $select, $btnPrev, $content, $btnNext); + return Object.defineProperty($div, "value", { + get() { + return $select.value; + }, + set(value) { + $div.setValue(value); + } + }), $div.addEventListener = function() { + $select.addEventListener.apply($select, arguments); + }, $div.removeEventListener = function() { + $select.removeEventListener.apply($select, arguments); + }, $div.dispatchEvent = function() { + return $select.dispatchEvent.apply($select, arguments); + }, $div.setValue = (value) => { + if ("setValue" in $select) $select.setValue(value); + else $select.value = value; + }, $div; + } +} +function onChangeVideoPlayerType() { + const playerType = getPref("video_player_type"), $videoProcessing = document.getElementById("bx_setting_video_processing"), $videoSharpness = document.getElementById("bx_setting_video_sharpness"), $videoPowerPreference = document.getElementById("bx_setting_video_power_preference"); + if (!$videoProcessing) return; + let isDisabled = !1; + const $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); + if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); + else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; + $videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), updateVideoPlayer(); +} +function updateVideoPlayer() { + const streamPlayer = STATES.currentStream.streamPlayer; + if (!streamPlayer) return; + const options = { + processing: getPref("video_processing"), + sharpness: getPref("video_sharpness"), + saturation: getPref("video_saturation"), + contrast: getPref("video_contrast"), + brightness: getPref("video_brightness") + }; + streamPlayer.setPlayerType(getPref("video_player_type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); +} +window.addEventListener("resize", updateVideoPlayer); class MkbPreset { static MOUSE_SETTINGS = { map_to: { @@ -1829,46 +2031,6 @@ class MkbPreset { return console.log(obj), obj; } } -class Toast { - static #$wrapper; - static #$msg; - static #$status; - static #stack = []; - static #isShowing = !1; - static #timeout; - static #DURATION = 3000; - static show(msg, status, options = {}) { - options = options || {}; - const args = Array.from(arguments); - if (options.instant) Toast.#stack = [args], Toast.#showNext(); - else Toast.#stack.push(args), !Toast.#isShowing && Toast.#showNext(); - } - static #showNext() { - if (!Toast.#stack.length) { - Toast.#isShowing = !1; - return; - } - Toast.#isShowing = !0, Toast.#timeout && clearTimeout(Toast.#timeout), Toast.#timeout = window.setTimeout(Toast.#hide, Toast.#DURATION); - const [msg, status, options] = Toast.#stack.shift(); - if (options && options.html) Toast.#$msg.innerHTML = msg; - else Toast.#$msg.textContent = msg; - if (status) Toast.#$status.classList.remove("bx-gone"), Toast.#$status.textContent = status; - else Toast.#$status.classList.add("bx-gone"); - const classList = Toast.#$wrapper.classList; - classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); - } - static #hide() { - Toast.#timeout = null; - const classList = Toast.#$wrapper.classList; - classList.remove("bx-show"), classList.add("bx-hide"); - } - static setup() { - Toast.#$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, Toast.#$msg = CE("span", { class: "bx-toast-msg" }), Toast.#$status = CE("span", { class: "bx-toast-status" })), Toast.#$wrapper.addEventListener("transitionend", (e) => { - const classList = Toast.#$wrapper.classList; - if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), Toast.#showNext(); - }), document.documentElement.appendChild(Toast.#$wrapper); - } -} class LocalDb { static #instance; static get INSTANCE() { @@ -2229,28 +2391,324 @@ class NativeMkbHandler extends MkbHandler { }); } } -function onChangeVideoPlayerType() { - const playerType = getPref("video_player_type"), $videoProcessing = document.getElementById("bx_setting_video_processing"), $videoSharpness = document.getElementById("bx_setting_video_sharpness"), $videoPowerPreference = document.getElementById("bx_setting_video_power_preference"); - if (!$videoProcessing) return; - let isDisabled = !1; - const $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); - if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); - else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; - $videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), updateVideoPlayer(); +var LOG_TAG2 = "MkbHandler", PointerToMouseButton = { + 1: 0, + 2: 2, + 4: 1 +}, VIRTUAL_GAMEPAD_ID = "Xbox 360 Controller"; +class WebSocketMouseDataProvider extends MouseDataProvider { + #pointerClient; + #connected = !1; + init() { + this.#pointerClient = PointerClient.getInstance(), this.#connected = !1; + try { + this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.#connected = !0; + } catch (e) { + Toast.show("Cannot enable Mouse & Keyboard feature"); + } + } + start() { + this.#connected && AppInterface.requestPointerCapture(); + } + stop() { + this.#connected && AppInterface.releasePointerCapture(); + } + destroy() { + this.#connected && this.#pointerClient?.stop(); + } } -function updateVideoPlayer() { - const streamPlayer = STATES.currentStream.streamPlayer; - if (!streamPlayer) return; - const options = { - processing: getPref("video_processing"), - sharpness: getPref("video_sharpness"), - saturation: getPref("video_saturation"), - contrast: getPref("video_contrast"), - brightness: getPref("video_brightness") +class PointerLockMouseDataProvider extends MouseDataProvider { + init() {} + start() { + window.addEventListener("mousemove", this.#onMouseMoveEvent), window.addEventListener("mousedown", this.#onMouseEvent), window.addEventListener("mouseup", this.#onMouseEvent), window.addEventListener("wheel", this.#onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.#disableContextMenu); + } + stop() { + document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.#onMouseMoveEvent), window.removeEventListener("mousedown", this.#onMouseEvent), window.removeEventListener("mouseup", this.#onMouseEvent), window.removeEventListener("wheel", this.#onWheelEvent), window.removeEventListener("contextmenu", this.#disableContextMenu); + } + destroy() {} + #onMouseMoveEvent = (e) => { + this.mkbHandler.handleMouseMove({ + movementX: e.movementX, + movementY: e.movementY + }); }; - streamPlayer.setPlayerType(getPref("video_player_type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); + #onMouseEvent = (e) => { + e.preventDefault(); + const isMouseDown = e.type === "mousedown", data = { + mouseButton: e.button, + pressed: isMouseDown + }; + this.mkbHandler.handleMouseClick(data); + }; + #onWheelEvent = (e) => { + if (!KeyHelper.getKeyFromEvent(e)) return; + const data = { + vertical: e.deltaY, + horizontal: e.deltaX + }; + if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); + }; + #disableContextMenu = (e) => e.preventDefault(); +} +class EmulatedMkbHandler extends MkbHandler { + static #instance; + static getInstance() { + if (!EmulatedMkbHandler.#instance) EmulatedMkbHandler.#instance = new EmulatedMkbHandler; + return EmulatedMkbHandler.#instance; + } + #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); + static DEFAULT_PANNING_SENSITIVITY = 0.001; + static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; + static MAXIMUM_STICK_RANGE = 1.1; + #VIRTUAL_GAMEPAD = { + id: VIRTUAL_GAMEPAD_ID, + index: 3, + connected: !1, + hapticActuators: null, + mapping: "standard", + axes: [0, 0, 0, 0], + buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })), + timestamp: performance.now(), + vibrationActuator: null + }; + #nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator); + #enabled = !1; + #mouseDataProvider; + #isPolling = !1; + #prevWheelCode = null; + #wheelStoppedTimeout; + #detectMouseStoppedTimeout; + #$message; + #escKeyDownTime = -1; + #STICK_MAP; + #LEFT_STICK_X = []; + #LEFT_STICK_Y = []; + #RIGHT_STICK_X = []; + #RIGHT_STICK_Y = []; + constructor() { + super(); + this.#STICK_MAP = { + 102: [this.#LEFT_STICK_X, 0, -1], + 103: [this.#LEFT_STICK_X, 0, 1], + 100: [this.#LEFT_STICK_Y, 1, -1], + 101: [this.#LEFT_STICK_Y, 1, 1], + 202: [this.#RIGHT_STICK_X, 2, -1], + 203: [this.#RIGHT_STICK_X, 2, 1], + 200: [this.#RIGHT_STICK_Y, 3, -1], + 201: [this.#RIGHT_STICK_Y, 3, 1] + }; + } + isEnabled = () => this.#enabled; + #patchedGetGamepads = () => { + const gamepads = this.#nativeGetGamepads() || []; + return gamepads[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD, gamepads; + }; + #getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD; + #updateStick(stick, x, y) { + const virtualGamepad = this.#getVirtualGamepad(); + virtualGamepad.axes[stick * 2] = x, virtualGamepad.axes[stick * 2 + 1] = y, virtualGamepad.timestamp = performance.now(); + } + #vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); + #resetGamepad = () => { + const gamepad = this.#getVirtualGamepad(); + gamepad.axes = [0, 0, 0, 0]; + for (let button of gamepad.buttons) + button.pressed = !1, button.value = 0; + gamepad.timestamp = performance.now(); + }; + #pressButton = (buttonIndex, pressed) => { + const virtualGamepad = this.#getVirtualGamepad(); + if (buttonIndex >= 100) { + let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]; + valueArr = valueArr, axisIndex = axisIndex; + for (let i = valueArr.length - 1;i >= 0; i--) + if (valueArr[i] === buttonIndex) valueArr.splice(i, 1); + pressed && valueArr.push(buttonIndex); + let value; + if (valueArr.length) value = this.#STICK_MAP[valueArr[valueArr.length - 1]][2]; + else value = 0; + virtualGamepad.axes[axisIndex] = value; + } else virtualGamepad.buttons[buttonIndex].pressed = pressed, virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0; + virtualGamepad.timestamp = performance.now(); + }; + #onKeyboardEvent = (e) => { + const isKeyDown = e.type === "keydown"; + if (e.code === "F8") { + if (!isKeyDown) e.preventDefault(), this.toggle(); + return; + } + if (e.code === "Escape") { + if (e.preventDefault(), this.#enabled && isKeyDown) { + if (this.#escKeyDownTime === -1) this.#escKeyDownTime = performance.now(); + else if (performance.now() - this.#escKeyDownTime >= 1000) this.stop(); + } else this.#escKeyDownTime = -1; + return; + } + if (!this.#isPolling) return; + const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]; + if (typeof buttonIndex === "undefined") return; + if (e.repeat) return; + e.preventDefault(), this.#pressButton(buttonIndex, isKeyDown); + }; + #onMouseStopped = () => { + this.#detectMouseStoppedTimeout = null; + const analog = this.#CURRENT_PRESET_DATA.mouse["map_to"] === 1 ? 0 : 1; + this.#updateStick(analog, 0, 0); + }; + handleMouseClick = (data) => { + let mouseButton; + if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton; + else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton]; + const keyCode = "Mouse" + mouseButton, key = { + code: keyCode, + name: KeyHelper.codeToKeyName(keyCode) + }; + if (!key.name) return; + const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + if (typeof buttonIndex === "undefined") return; + this.#pressButton(buttonIndex, data.pressed); + }; + handleMouseMove = (data) => { + const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse["map_to"]; + if (mouseMapTo === 0) return; + this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout), this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50); + const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse["deadzone_counterweight"]; + let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse["sensitivity_x"], y = data.movementY * this.#CURRENT_PRESET_DATA.mouse["sensitivity_y"], length = this.#vectorLength(x, y); + if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; + else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length, y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length; + const analog = mouseMapTo === 1 ? 0 : 1; + this.#updateStick(analog, x, y); + }; + handleMouseWheel = (data) => { + let code = ""; + if (data.vertical < 0) code = "ScrollUp"; + else if (data.vertical > 0) code = "ScrollDown"; + else if (data.horizontal < 0) code = "ScrollLeft"; + else if (data.horizontal > 0) code = "ScrollRight"; + if (!code) return !1; + const key = { + code, + name: KeyHelper.codeToKeyName(code) + }, buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + if (typeof buttonIndex === "undefined") return !1; + if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout), this.#pressButton(buttonIndex, !0); + return this.#wheelStoppedTimeout = window.setTimeout(() => { + this.#prevWheelCode = null, this.#pressButton(buttonIndex, !1); + }, 20), !0; + }; + toggle = (force) => { + if (typeof force !== "undefined") this.#enabled = force; + else this.#enabled = !this.#enabled; + if (this.#enabled) document.body.requestPointerLock(); + else document.pointerLockElement && document.exitPointerLock(); + }; + #getCurrentPreset = () => { + return new Promise((resolve) => { + const presetId = getPref("mkb_default_preset_id"); + LocalDb.INSTANCE.getPreset(presetId).then((preset) => { + resolve(preset); + }); + }); + }; + refreshPresetData = () => { + this.#getCurrentPreset().then((preset) => { + this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET), this.#resetGamepad(); + }); + }; + waitForMouseData = (wait) => { + this.#$message && this.#$message.classList.toggle("bx-gone", !wait); + }; + #onPollingModeChanged = (e) => { + if (!this.#$message) return; + if (e.mode === "none") this.#$message.classList.remove("bx-offscreen"); + else this.#$message.classList.add("bx-offscreen"); + }; + #onDialogShown = () => { + document.pointerLockElement && document.exitPointerLock(); + }; + #initMessage = () => { + if (!this.#$message) this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, CE("div", {}, CE("p", {}, t("virtual-controller")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "virtual" }, createButton({ + style: 1 | 256 | 64, + label: t("activate"), + onClick: ((e) => { + e.preventDefault(), e.stopPropagation(), this.toggle(!0); + }).bind(this) + }), CE("div", {}, createButton({ + label: t("ignore"), + style: 4, + onClick: (e) => { + e.preventDefault(), e.stopPropagation(), this.toggle(!1), this.waitForMouseData(!1); + } + }), createButton({ + label: t("edit"), + onClick: (e) => { + e.preventDefault(), e.stopPropagation(); + const dialog = SettingsNavigationDialog.getInstance(); + dialog.focusTab("mkb"), NavigationDialogManager.getInstance().show(dialog); + } + })))); + if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); + }; + #onPointerLockChange = () => { + if (document.pointerLockElement) this.start(); + else this.stop(); + }; + #onPointerLockError = (e) => { + console.log(e), this.stop(); + }; + #onPointerLockRequested = () => { + this.start(); + }; + #onPointerLockExited = () => { + this.#mouseDataProvider?.stop(); + }; + handleEvent(event) { + switch (event.type) { + case BxEvent.POINTER_LOCK_REQUESTED: + this.#onPointerLockRequested(); + break; + case BxEvent.POINTER_LOCK_EXITED: + this.#onPointerLockExited(); + break; + } + } + init = () => { + if (this.refreshPresetData(), this.#enabled = !1, AppInterface) this.#mouseDataProvider = new WebSocketMouseDataProvider(this); + else this.#mouseDataProvider = new PointerLockMouseDataProvider(this); + if (this.#mouseDataProvider.init(), window.addEventListener("keydown", this.#onKeyboardEvent), window.addEventListener("keyup", this.#onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this); + else document.addEventListener("pointerlockchange", this.#onPointerLockChange), document.addEventListener("pointerlockerror", this.#onPointerLockError); + if (this.#initMessage(), this.#$message?.classList.add("bx-gone"), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("virtual-controller"), { html: !0 }), this.waitForMouseData(!1); + else this.waitForMouseData(!0); + }; + destroy = () => { + if (this.#isPolling = !1, this.#enabled = !1, this.stop(), this.waitForMouseData(!1), document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("keydown", this.#onKeyboardEvent), window.removeEventListener("keyup", this.#onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this); + else document.removeEventListener("pointerlockchange", this.#onPointerLockChange), document.removeEventListener("pointerlockerror", this.#onPointerLockError); + window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), this.#mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged); + }; + start = () => { + if (!this.#enabled) this.#enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); + this.#isPolling = !0, this.#escKeyDownTime = -1, this.#resetGamepad(), window.navigator.getGamepads = this.#patchedGetGamepads, this.waitForMouseData(!1), this.#mouseDataProvider?.start(); + const virtualGamepad = this.#getVirtualGamepad(); + virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", { + gamepad: virtualGamepad + }), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); + }; + stop = () => { + this.#enabled = !1, this.#isPolling = !1, this.#escKeyDownTime = -1; + const virtualGamepad = this.#getVirtualGamepad(); + if (virtualGamepad.connected) this.#resetGamepad(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", { + gamepad: virtualGamepad + }), window.navigator.getGamepads = this.#nativeGetGamepads; + this.waitForMouseData(!0), this.#mouseDataProvider?.stop(); + }; + static setupEvents() { + window.addEventListener(BxEvent.STREAM_PLAYING, () => { + if (STATES.currentStream.titleInfo?.details.hasMkbSupport) { + if (AppInterface && getPref("native_mkb_enabled") === "on") AppInterface && NativeMkbHandler.getInstance().init(); + } else if (getPref("mkb_enabled") && (AppInterface || !UserAgent.isMobile())) BxLogger.info(LOG_TAG2, "Emulate MKB"), EmulatedMkbHandler.getInstance().init(); + }); + } } -window.addEventListener("resize", updateVideoPlayer); class NavigationDialog { dialogManager; constructor() { @@ -2376,7 +2834,7 @@ class NavigationDialogManager { const gamepads = window.navigator.getGamepads(); for (let gamepad of gamepads) { if (!gamepad || !gamepad.connected) continue; - if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) continue; + if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; const { axes, buttons } = gamepad; let releasedButton = null, heldButton = null, lastState = this.gamepadLastStates[gamepad.index], lastTimestamp, lastKey, lastKeyPressed; if (lastState) [lastTimestamp, lastKey, lastKeyPressed] = lastState; @@ -2568,363 +3026,6 @@ var BxIcon = { UPLOAD: "", AUDIO: "" }; -class Dialog { - $dialog; - $title; - $content; - $overlay; - onClose; - constructor(options) { - const { - title, - className, - content, - hideCloseButton, - onClose, - helpUrl - } = options, $overlay = document.querySelector(".bx-dialog-overlay"); - if (!$overlay) this.$overlay = CE("div", { class: "bx-dialog-overlay bx-gone" }), this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$overlay); - else this.$overlay = $overlay; - let $close; - this.onClose = onClose, this.$dialog = CE("div", { class: `bx-dialog ${className || ""} bx-gone` }, this.$title = CE("h2", {}, CE("b", {}, title), helpUrl && createButton({ - icon: BxIcon.QUESTION, - style: 4, - title: t("help"), - url: helpUrl - })), this.$content = CE("div", { class: "bx-dialog-content" }, content), !hideCloseButton && ($close = CE("button", { type: "button" }, t("close")))), $close && $close.addEventListener("click", (e) => { - this.hide(e); - }), !title && this.$title.classList.add("bx-gone"), !content && this.$content.classList.add("bx-gone"), this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$dialog); - } - show(newOptions) { - if (document.activeElement && document.activeElement.blur(), newOptions && newOptions.title) this.$title.querySelector("b").textContent = newOptions.title, this.$title.classList.remove("bx-gone"); - this.$dialog.classList.remove("bx-gone"), this.$overlay.classList.remove("bx-gone"), document.body.classList.add("bx-no-scroll"); - } - hide(e) { - this.$dialog.classList.add("bx-gone"), this.$overlay.classList.add("bx-gone"), document.body.classList.remove("bx-no-scroll"), this.onClose && this.onClose(e); - } - toggle() { - this.$dialog.classList.toggle("bx-gone"), this.$overlay.classList.toggle("bx-gone"); - } -} -class MkbRemapper { - #BUTTON_ORDERS = [ - 12, - 13, - 14, - 15, - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 16, - 10, - 100, - 101, - 102, - 103, - 11, - 200, - 201, - 202, - 203 - ]; - static #instance; - static get INSTANCE() { - if (!MkbRemapper.#instance) MkbRemapper.#instance = new MkbRemapper; - return MkbRemapper.#instance; - } - #STATE = { - currentPresetId: 0, - presets: {}, - editingPresetData: null, - isEditing: !1 - }; - #$ = { - wrapper: null, - presetsSelect: null, - activateButton: null, - currentBindingKey: null, - allKeyElements: [], - allMouseElements: {} - }; - bindingDialog; - constructor() { - this.#STATE.currentPresetId = getPref("mkb_default_preset_id"), this.bindingDialog = new Dialog({ - className: "bx-binding-dialog", - content: CE("div", {}, CE("p", {}, t("press-to-bind")), CE("i", {}, t("press-esc-to-cancel"))), - hideCloseButton: !0 - }); - } - #clearEventListeners = () => { - window.removeEventListener("keydown", this.#onKeyDown), window.removeEventListener("mousedown", this.#onMouseDown), window.removeEventListener("wheel", this.#onWheel); - }; - #bindKey = ($elm, key) => { - const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")); - if ($elm.getAttribute("data-key-code") === key.code) return; - for (let $otherElm of this.#$.allKeyElements) - if ($otherElm.getAttribute("data-key-code") === key.code) this.#unbindKey($otherElm); - this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.setAttribute("data-key-code", key.code); - }; - #unbindKey = ($elm) => { - const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")); - this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", $elm.removeAttribute("data-key-code"); - }; - #onWheel = (e) => { - e.preventDefault(), this.#clearEventListeners(), this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200); - }; - #onMouseDown = (e) => { - e.preventDefault(), this.#clearEventListeners(), this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200); - }; - #onKeyDown = (e) => { - if (e.preventDefault(), e.stopPropagation(), this.#clearEventListeners(), e.code !== "Escape") this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)); - window.setTimeout(() => this.bindingDialog.hide(), 200); - }; - #onBindingKey = (e) => { - if (!this.#STATE.isEditing || e.button !== 0) return; - console.log(e), this.#$.currentBindingKey = e.target, window.addEventListener("keydown", this.#onKeyDown), window.addEventListener("mousedown", this.#onMouseDown), window.addEventListener("wheel", this.#onWheel), this.bindingDialog.show({ title: this.#$.currentBindingKey.getAttribute("data-prompt") }); - }; - #onContextMenu = (e) => { - if (e.preventDefault(), !this.#STATE.isEditing) return; - this.#unbindKey(e.target); - }; - #getPreset = (presetId) => { - return this.#STATE.presets[presetId]; - }; - #getCurrentPreset = () => { - return this.#getPreset(this.#STATE.currentPresetId); - }; - #switchPreset = (presetId) => { - this.#STATE.currentPresetId = presetId; - const presetData = this.#getCurrentPreset().data; - for (let $elm of this.#$.allKeyElements) { - const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")), buttonKeys = presetData.mapping[buttonIndex]; - if (buttonKeys && buttonKeys[keySlot]) $elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]), $elm.setAttribute("data-key-code", buttonKeys[keySlot]); - else $elm.textContent = "", $elm.removeAttribute("data-key-code"); - } - let key; - for (key in this.#$.allMouseElements) { - const $elm = this.#$.allMouseElements[key]; - let value = presetData.mouse[key]; - if (typeof value === "undefined") value = MkbPreset.MOUSE_SETTINGS[key].default; - "setValue" in $elm && $elm.setValue(value); - } - const activated = getPref("mkb_default_preset_id") === this.#STATE.currentPresetId; - this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"); - }; - #refresh() { - while (this.#$.presetsSelect.firstChild) - this.#$.presetsSelect.removeChild(this.#$.presetsSelect.firstChild); - LocalDb.INSTANCE.getPresets().then((presets) => { - this.#STATE.presets = presets; - const $fragment = document.createDocumentFragment(); - let defaultPresetId; - if (this.#STATE.currentPresetId === 0) this.#STATE.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.#STATE.currentPresetId, setPref("mkb_default_preset_id", defaultPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(); - else defaultPresetId = getPref("mkb_default_preset_id"); - for (let id in presets) { - let name = presets[id].name; - if (id === defaultPresetId) name = "🎮 " + name; - const $options = CE("option", { value: id }, name); - $options.selected = parseInt(id) === this.#STATE.currentPresetId, $fragment.appendChild($options); - } - this.#$.presetsSelect.appendChild($fragment); - const activated = defaultPresetId === this.#STATE.currentPresetId; - this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.#STATE.isEditing && this.#switchPreset(this.#STATE.currentPresetId); - }); - } - #toggleEditing = (force) => { - if (this.#STATE.isEditing = typeof force !== "undefined" ? force : !this.#STATE.isEditing, this.#$.wrapper.classList.toggle("bx-editing", this.#STATE.isEditing), this.#STATE.isEditing) this.#STATE.editingPresetData = deepClone(this.#getCurrentPreset().data); - else this.#STATE.editingPresetData = null; - const childElements = this.#$.wrapper.querySelectorAll("select, button, input"); - for (let $elm of Array.from(childElements)) { - if ($elm.parentElement.parentElement.classList.contains("bx-mkb-action-buttons")) continue; - let disable = !this.#STATE.isEditing; - if ($elm.parentElement.classList.contains("bx-mkb-preset-tools")) disable = !disable; - $elm.disabled = disable; - } - }; - render() { - this.#$.wrapper = CE("div", { class: "bx-mkb-settings" }), this.#$.presetsSelect = CE("select", { tabindex: -1 }), this.#$.presetsSelect.addEventListener("change", (e) => { - this.#switchPreset(parseInt(e.target.value)); - }); - const promptNewName = (value) => { - let newName = ""; - while (!newName) { - if (newName = prompt(t("prompt-preset-name"), value), newName === null) return !1; - newName = newName.trim(); - } - return newName ? newName : !1; - }, $header = CE("div", { class: "bx-mkb-preset-tools" }, this.#$.presetsSelect, createButton({ - title: t("rename"), - icon: BxIcon.CURSOR_TEXT, - tabIndex: -1, - onClick: (e) => { - const preset = this.#getCurrentPreset(); - let newName = promptNewName(preset.name); - if (!newName || newName === preset.name) return; - preset.name = newName, LocalDb.INSTANCE.updatePreset(preset).then((id) => this.#refresh()); - } - }), createButton({ - icon: BxIcon.NEW, - title: t("new"), - tabIndex: -1, - onClick: (e) => { - let newName = promptNewName(""); - if (!newName) return; - LocalDb.INSTANCE.newPreset(newName, MkbPreset.DEFAULT_PRESET).then((id) => { - this.#STATE.currentPresetId = id, this.#refresh(); - }); - } - }), createButton({ - icon: BxIcon.COPY, - title: t("copy"), - tabIndex: -1, - onClick: (e) => { - const preset = this.#getCurrentPreset(); - let newName = promptNewName(`${preset.name} (2)`); - if (!newName) return; - LocalDb.INSTANCE.newPreset(newName, preset.data).then((id) => { - this.#STATE.currentPresetId = id, this.#refresh(); - }); - } - }), createButton({ - icon: BxIcon.TRASH, - style: 2, - title: t("delete"), - tabIndex: -1, - onClick: (e) => { - if (!confirm(t("confirm-delete-preset"))) return; - LocalDb.INSTANCE.deletePreset(this.#STATE.currentPresetId).then((id) => { - this.#STATE.currentPresetId = 0, this.#refresh(); - }); - } - })); - this.#$.wrapper.appendChild($header); - const $rows = CE("div", { class: "bx-mkb-settings-rows" }, CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind"))), keysPerButton = 2; - for (let buttonIndex of this.#BUTTON_ORDERS) { - const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex]; - let $elm; - const $fragment = document.createDocumentFragment(); - for (let i = 0;i < keysPerButton; i++) - $elm = CE("button", { - type: "button", - "data-prompt": buttonPrompt, - "data-button-index": buttonIndex, - "data-key-slot": i - }, " "), $elm.addEventListener("mouseup", this.#onBindingKey), $elm.addEventListener("contextmenu", this.#onContextMenu), $fragment.appendChild($elm), this.#$.allKeyElements.push($elm); - const $keyRow = CE("div", { class: "bx-mkb-key-row" }, CE("label", { title: buttonName }, buttonPrompt), $fragment); - $rows.appendChild($keyRow); - } - $rows.appendChild(CE("i", { class: "bx-mkb-note" }, t("mkb-adjust-ingame-settings"))); - const $mouseSettings = document.createDocumentFragment(); - for (let key in MkbPreset.MOUSE_SETTINGS) { - const setting = MkbPreset.MOUSE_SETTINGS[key], value = setting.default; - let $elm; - const onChange = (e, value2) => { - this.#STATE.editingPresetData.mouse[key] = value2; - }, $row = CE("label", { - class: "bx-settings-row", - for: `bx_setting_${key}` - }, CE("span", { class: "bx-settings-label" }, setting.label), $elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params)); - $mouseSettings.appendChild($row), this.#$.allMouseElements[key] = $elm; - } - $rows.appendChild($mouseSettings), this.#$.wrapper.appendChild($rows); - const $actionButtons = CE("div", { class: "bx-mkb-action-buttons" }, CE("div", {}, createButton({ - label: t("edit"), - tabIndex: -1, - onClick: (e) => this.#toggleEditing(!0) - }), this.#$.activateButton = createButton({ - label: t("activate"), - style: 1, - tabIndex: -1, - onClick: (e) => { - setPref("mkb_default_preset_id", this.#STATE.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.#refresh(); - } - })), CE("div", {}, createButton({ - label: t("cancel"), - style: 4, - tabIndex: -1, - onClick: (e) => { - this.#switchPreset(this.#STATE.currentPresetId), this.#toggleEditing(!1); - } - }), createButton({ - label: t("save"), - style: 1, - tabIndex: -1, - onClick: (e) => { - const updatedPreset = deepClone(this.#getCurrentPreset()); - updatedPreset.data = this.#STATE.editingPresetData, LocalDb.INSTANCE.updatePreset(updatedPreset).then((id) => { - if (id === getPref("mkb_default_preset_id")) EmulatedMkbHandler.getInstance().refreshPresetData(); - this.#toggleEditing(!1), this.#refresh(); - }); - } - }))); - return this.#$.wrapper.appendChild($actionButtons), this.#toggleEditing(!1), this.#refresh(), this.#$.wrapper; - } -} -function ceilToNearest(value, interval) { - return Math.ceil(value / interval) * interval; -} -function floorToNearest(value, interval) { - return Math.floor(value / interval) * interval; -} -async function copyToClipboard(text, showToast = !0) { - try { - return await navigator.clipboard.writeText(text), showToast && Toast.show("Copied to clipboard", "", { instant: !0 }), !0; - } catch (err) { - console.error("Failed to copy: ", err), showToast && Toast.show("Failed to copy", "", { instant: !0 }); - } - return !1; -} -function productTitleToSlug(title) { - return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase(); -} -class SoundShortcut { - static adjustGainNodeVolume(amount) { - if (!getPref("audio_enable_volume_control")) return 0; - const currentValue = getPref("audio_volume"); - let nearestValue; - if (amount > 0) nearestValue = ceilToNearest(currentValue, amount); - else nearestValue = floorToNearest(currentValue, -1 * amount); - let newValue; - if (currentValue !== nearestValue) newValue = nearestValue; - else newValue = currentValue + amount; - return newValue = setPref("audio_volume", newValue, !0), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue; - } - static setGainNodeVolume(value) { - STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); - } - static muteUnmute() { - if (getPref("audio_enable_volume_control") && STATES.currentStream.audioGainNode) { - const gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio_volume"); - let targetValue; - if (settingValue === 0) targetValue = 100, setPref("audio_volume", targetValue, !0); - else if (gainValue === 0) targetValue = settingValue; - else targetValue = 0; - let status; - if (targetValue === 0) status = t("muted"); - else status = targetValue + "%"; - SoundShortcut.setGainNodeVolume(targetValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { - speakerState: targetValue === 0 ? 1 : 0 - }); - return; - } - let $media; - if ($media = document.querySelector("div[data-testid=media-container] audio"), !$media) $media = document.querySelector("div[data-testid=media-container] video"); - if ($media) { - $media.muted = !$media.muted; - const status = $media.muted ? t("muted") : t("unmuted"); - Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { - speakerState: $media.muted ? 1 : 0 - }); - } - } -} var VIBRATION_DATA_MAP = { gamepadIndex: 8, leftMotorPercent: 8, @@ -2998,88 +3099,6 @@ class VibrationManager { }); } } -class BxSelectElement { - static wrap($select) { - $select.removeAttribute("tabindex"); - const $btnPrev = createButton({ - label: "<", - style: 32 - }), $btnNext = createButton({ - label: ">", - style: 32 - }), isMultiple = $select.multiple; - let $checkBox, $label, visibleIndex = $select.selectedIndex, $content; - if (isMultiple) $content = CE("button", { - class: "bx-select-value bx-focusable", - tabindex: 0 - }, $checkBox = CE("input", { type: "checkbox" }), $label = CE("span", {}, "")), $content.addEventListener("click", (e) => { - $checkBox.click(); - }), $checkBox.addEventListener("input", (e) => { - const $option = getOptionAtIndex(visibleIndex); - $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); - }); - else $content = CE("div", {}, $label = CE("label", { for: $select.id + "_checkbox" }, "")); - const getOptionAtIndex = (index) => { - return Array.from($select.querySelectorAll("option"))[index]; - }, render = (e) => { - if (e && e.manualTrigger) visibleIndex = $select.selectedIndex; - visibleIndex = normalizeIndex(visibleIndex); - const $option = getOptionAtIndex(visibleIndex); - let content = ""; - if ($option) if (content = $option.textContent || "", content && $option.parentElement.tagName === "OPTGROUP") { - $label.innerHTML = ""; - const fragment = document.createDocumentFragment(); - fragment.appendChild(CE("span", {}, $option.parentElement.label)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment); - } else $label.textContent = content; - else $label.textContent = content; - if ($label.classList.toggle("bx-line-through", $option && $option.disabled), isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); - const disablePrev = visibleIndex <= 0, disableNext = visibleIndex === $select.querySelectorAll("option").length - 1; - $btnPrev.classList.toggle("bx-inactive", disablePrev), $btnNext.classList.toggle("bx-inactive", disableNext), disablePrev && !disableNext && document.activeElement === $btnPrev && $btnNext.focus(), disableNext && !disablePrev && document.activeElement === $btnNext && $btnPrev.focus(); - }, normalizeIndex = (index) => { - return Math.min(Math.max(index, 0), $select.querySelectorAll("option").length - 1); - }, onPrevNext = (e) => { - if (!e.target) return; - const goNext = e.target.closest("button") === $btnNext, currentIndex = visibleIndex; - let newIndex = goNext ? currentIndex + 1 : currentIndex - 1; - if (newIndex = normalizeIndex(newIndex), visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; - if (isMultiple) render(); - else BxEvent.dispatch($select, "input"); - }; - $select.addEventListener("input", render), $btnPrev.addEventListener("click", onPrevNext), $btnNext.addEventListener("click", onPrevNext), new MutationObserver((mutationList, observer2) => { - mutationList.forEach((mutation) => { - if (mutation.type === "childList" || mutation.type === "attributes") render(); - }); - }).observe($select, { - subtree: !0, - childList: !0, - attributes: !0 - }), render(); - const $div = CE("div", { - class: "bx-select", - _nearby: { - orientation: "horizontal", - focus: $btnNext - } - }, $select, $btnPrev, $content, $btnNext); - return Object.defineProperty($div, "value", { - get() { - return $select.value; - }, - set(value) { - $div.setValue(value); - } - }), $div.addEventListener = function() { - $select.addEventListener.apply($select, arguments); - }, $div.removeEventListener = function() { - $select.removeEventListener.apply($select, arguments); - }, $div.dispatchEvent = function() { - return $select.dispatchEvent.apply($select, arguments); - }, $div.setValue = (value) => { - if ("setValue" in $select) $select.setValue(value); - else $select.value = value; - }, $div; - } -} var FeatureGates = { PwaPrompt: !1, EnableWifiWarnings: !1, @@ -3422,7 +3441,7 @@ class SettingsNavigationDialog extends NavigationDialog { group: "mkb", label: t("virtual-controller"), helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/", - content: MkbRemapper.INSTANCE.render() + content: !1 }]; TAB_NATIVE_MKB_ITEMS = [{ requiredVariants: "full", @@ -3994,325 +4013,6 @@ class SettingsNavigationDialog extends NavigationDialog { return handled; } } -var LOG_TAG2 = "MkbHandler", PointerToMouseButton = { - 1: 0, - 2: 2, - 4: 1 -}; -class WebSocketMouseDataProvider extends MouseDataProvider { - #pointerClient; - #connected = !1; - init() { - this.#pointerClient = PointerClient.getInstance(), this.#connected = !1; - try { - this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.#connected = !0; - } catch (e) { - Toast.show("Cannot enable Mouse & Keyboard feature"); - } - } - start() { - this.#connected && AppInterface.requestPointerCapture(); - } - stop() { - this.#connected && AppInterface.releasePointerCapture(); - } - destroy() { - this.#connected && this.#pointerClient?.stop(); - } -} -class PointerLockMouseDataProvider extends MouseDataProvider { - init() {} - start() { - window.addEventListener("mousemove", this.#onMouseMoveEvent), window.addEventListener("mousedown", this.#onMouseEvent), window.addEventListener("mouseup", this.#onMouseEvent), window.addEventListener("wheel", this.#onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.#disableContextMenu); - } - stop() { - document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.#onMouseMoveEvent), window.removeEventListener("mousedown", this.#onMouseEvent), window.removeEventListener("mouseup", this.#onMouseEvent), window.removeEventListener("wheel", this.#onWheelEvent), window.removeEventListener("contextmenu", this.#disableContextMenu); - } - destroy() {} - #onMouseMoveEvent = (e) => { - this.mkbHandler.handleMouseMove({ - movementX: e.movementX, - movementY: e.movementY - }); - }; - #onMouseEvent = (e) => { - e.preventDefault(); - const isMouseDown = e.type === "mousedown", data = { - mouseButton: e.button, - pressed: isMouseDown - }; - this.mkbHandler.handleMouseClick(data); - }; - #onWheelEvent = (e) => { - if (!KeyHelper.getKeyFromEvent(e)) return; - const data = { - vertical: e.deltaY, - horizontal: e.deltaX - }; - if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); - }; - #disableContextMenu = (e) => e.preventDefault(); -} -class EmulatedMkbHandler extends MkbHandler { - static #instance; - static getInstance() { - if (!EmulatedMkbHandler.#instance) EmulatedMkbHandler.#instance = new EmulatedMkbHandler; - return EmulatedMkbHandler.#instance; - } - #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); - static DEFAULT_PANNING_SENSITIVITY = 0.001; - static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; - static MAXIMUM_STICK_RANGE = 1.1; - static VIRTUAL_GAMEPAD_ID = "Xbox 360 Controller"; - #VIRTUAL_GAMEPAD = { - id: EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID, - index: 3, - connected: !1, - hapticActuators: null, - mapping: "standard", - axes: [0, 0, 0, 0], - buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })), - timestamp: performance.now(), - vibrationActuator: null - }; - #nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator); - #enabled = !1; - #mouseDataProvider; - #isPolling = !1; - #prevWheelCode = null; - #wheelStoppedTimeout; - #detectMouseStoppedTimeout; - #$message; - #escKeyDownTime = -1; - #STICK_MAP; - #LEFT_STICK_X = []; - #LEFT_STICK_Y = []; - #RIGHT_STICK_X = []; - #RIGHT_STICK_Y = []; - constructor() { - super(); - this.#STICK_MAP = { - 102: [this.#LEFT_STICK_X, 0, -1], - 103: [this.#LEFT_STICK_X, 0, 1], - 100: [this.#LEFT_STICK_Y, 1, -1], - 101: [this.#LEFT_STICK_Y, 1, 1], - 202: [this.#RIGHT_STICK_X, 2, -1], - 203: [this.#RIGHT_STICK_X, 2, 1], - 200: [this.#RIGHT_STICK_Y, 3, -1], - 201: [this.#RIGHT_STICK_Y, 3, 1] - }; - } - isEnabled = () => this.#enabled; - #patchedGetGamepads = () => { - const gamepads = this.#nativeGetGamepads() || []; - return gamepads[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD, gamepads; - }; - #getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD; - #updateStick(stick, x, y) { - const virtualGamepad = this.#getVirtualGamepad(); - virtualGamepad.axes[stick * 2] = x, virtualGamepad.axes[stick * 2 + 1] = y, virtualGamepad.timestamp = performance.now(); - } - #vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); - #resetGamepad = () => { - const gamepad = this.#getVirtualGamepad(); - gamepad.axes = [0, 0, 0, 0]; - for (let button of gamepad.buttons) - button.pressed = !1, button.value = 0; - gamepad.timestamp = performance.now(); - }; - #pressButton = (buttonIndex, pressed) => { - const virtualGamepad = this.#getVirtualGamepad(); - if (buttonIndex >= 100) { - let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]; - valueArr = valueArr, axisIndex = axisIndex; - for (let i = valueArr.length - 1;i >= 0; i--) - if (valueArr[i] === buttonIndex) valueArr.splice(i, 1); - pressed && valueArr.push(buttonIndex); - let value; - if (valueArr.length) value = this.#STICK_MAP[valueArr[valueArr.length - 1]][2]; - else value = 0; - virtualGamepad.axes[axisIndex] = value; - } else virtualGamepad.buttons[buttonIndex].pressed = pressed, virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0; - virtualGamepad.timestamp = performance.now(); - }; - #onKeyboardEvent = (e) => { - const isKeyDown = e.type === "keydown"; - if (e.code === "F8") { - if (!isKeyDown) e.preventDefault(), this.toggle(); - return; - } - if (e.code === "Escape") { - if (e.preventDefault(), this.#enabled && isKeyDown) { - if (this.#escKeyDownTime === -1) this.#escKeyDownTime = performance.now(); - else if (performance.now() - this.#escKeyDownTime >= 1000) this.stop(); - } else this.#escKeyDownTime = -1; - return; - } - if (!this.#isPolling) return; - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]; - if (typeof buttonIndex === "undefined") return; - if (e.repeat) return; - e.preventDefault(), this.#pressButton(buttonIndex, isKeyDown); - }; - #onMouseStopped = () => { - this.#detectMouseStoppedTimeout = null; - const analog = this.#CURRENT_PRESET_DATA.mouse["map_to"] === 1 ? 0 : 1; - this.#updateStick(analog, 0, 0); - }; - handleMouseClick = (data) => { - let mouseButton; - if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton; - else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton]; - const keyCode = "Mouse" + mouseButton, key = { - code: keyCode, - name: KeyHelper.codeToKeyName(keyCode) - }; - if (!key.name) return; - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; - if (typeof buttonIndex === "undefined") return; - this.#pressButton(buttonIndex, data.pressed); - }; - handleMouseMove = (data) => { - const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse["map_to"]; - if (mouseMapTo === 0) return; - this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout), this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50); - const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse["deadzone_counterweight"]; - let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse["sensitivity_x"], y = data.movementY * this.#CURRENT_PRESET_DATA.mouse["sensitivity_y"], length = this.#vectorLength(x, y); - if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; - else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length, y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length; - const analog = mouseMapTo === 1 ? 0 : 1; - this.#updateStick(analog, x, y); - }; - handleMouseWheel = (data) => { - let code = ""; - if (data.vertical < 0) code = "ScrollUp"; - else if (data.vertical > 0) code = "ScrollDown"; - else if (data.horizontal < 0) code = "ScrollLeft"; - else if (data.horizontal > 0) code = "ScrollRight"; - if (!code) return !1; - const key = { - code, - name: KeyHelper.codeToKeyName(code) - }, buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; - if (typeof buttonIndex === "undefined") return !1; - if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout), this.#pressButton(buttonIndex, !0); - return this.#wheelStoppedTimeout = window.setTimeout(() => { - this.#prevWheelCode = null, this.#pressButton(buttonIndex, !1); - }, 20), !0; - }; - toggle = (force) => { - if (typeof force !== "undefined") this.#enabled = force; - else this.#enabled = !this.#enabled; - if (this.#enabled) document.body.requestPointerLock(); - else document.pointerLockElement && document.exitPointerLock(); - }; - #getCurrentPreset = () => { - return new Promise((resolve) => { - const presetId = getPref("mkb_default_preset_id"); - LocalDb.INSTANCE.getPreset(presetId).then((preset) => { - resolve(preset); - }); - }); - }; - refreshPresetData = () => { - this.#getCurrentPreset().then((preset) => { - this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET), this.#resetGamepad(); - }); - }; - waitForMouseData = (wait) => { - this.#$message && this.#$message.classList.toggle("bx-gone", !wait); - }; - #onPollingModeChanged = (e) => { - if (!this.#$message) return; - if (e.mode === "none") this.#$message.classList.remove("bx-offscreen"); - else this.#$message.classList.add("bx-offscreen"); - }; - #onDialogShown = () => { - document.pointerLockElement && document.exitPointerLock(); - }; - #initMessage = () => { - if (!this.#$message) this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, CE("div", {}, CE("p", {}, t("virtual-controller")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "virtual" }, createButton({ - style: 1 | 256 | 64, - label: t("activate"), - onClick: ((e) => { - e.preventDefault(), e.stopPropagation(), this.toggle(!0); - }).bind(this) - }), CE("div", {}, createButton({ - label: t("ignore"), - style: 4, - onClick: (e) => { - e.preventDefault(), e.stopPropagation(), this.toggle(!1), this.waitForMouseData(!1); - } - }), createButton({ - label: t("edit"), - onClick: (e) => { - e.preventDefault(), e.stopPropagation(); - const dialog = SettingsNavigationDialog.getInstance(); - dialog.focusTab("mkb"), NavigationDialogManager.getInstance().show(dialog); - } - })))); - if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); - }; - #onPointerLockChange = () => { - if (document.pointerLockElement) this.start(); - else this.stop(); - }; - #onPointerLockError = (e) => { - console.log(e), this.stop(); - }; - #onPointerLockRequested = () => { - this.start(); - }; - #onPointerLockExited = () => { - this.#mouseDataProvider?.stop(); - }; - handleEvent(event) { - switch (event.type) { - case BxEvent.POINTER_LOCK_REQUESTED: - this.#onPointerLockRequested(); - break; - case BxEvent.POINTER_LOCK_EXITED: - this.#onPointerLockExited(); - break; - } - } - init = () => { - if (this.refreshPresetData(), this.#enabled = !1, AppInterface) this.#mouseDataProvider = new WebSocketMouseDataProvider(this); - else this.#mouseDataProvider = new PointerLockMouseDataProvider(this); - if (this.#mouseDataProvider.init(), window.addEventListener("keydown", this.#onKeyboardEvent), window.addEventListener("keyup", this.#onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this); - else document.addEventListener("pointerlockchange", this.#onPointerLockChange), document.addEventListener("pointerlockerror", this.#onPointerLockError); - if (this.#initMessage(), this.#$message?.classList.add("bx-gone"), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("virtual-controller"), { html: !0 }), this.waitForMouseData(!1); - else this.waitForMouseData(!0); - }; - destroy = () => { - if (this.#isPolling = !1, this.#enabled = !1, this.stop(), this.waitForMouseData(!1), document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("keydown", this.#onKeyboardEvent), window.removeEventListener("keyup", this.#onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this); - else document.removeEventListener("pointerlockchange", this.#onPointerLockChange), document.removeEventListener("pointerlockerror", this.#onPointerLockError); - window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), this.#mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged); - }; - start = () => { - if (!this.#enabled) this.#enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); - this.#isPolling = !0, this.#escKeyDownTime = -1, this.#resetGamepad(), window.navigator.getGamepads = this.#patchedGetGamepads, this.waitForMouseData(!1), this.#mouseDataProvider?.start(); - const virtualGamepad = this.#getVirtualGamepad(); - virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", { - gamepad: virtualGamepad - }), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); - }; - stop = () => { - this.#enabled = !1, this.#isPolling = !1, this.#escKeyDownTime = -1; - const virtualGamepad = this.#getVirtualGamepad(); - if (virtualGamepad.connected) this.#resetGamepad(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", { - gamepad: virtualGamepad - }), window.navigator.getGamepads = this.#nativeGetGamepads; - this.waitForMouseData(!0), this.#mouseDataProvider?.stop(); - }; - static setupEvents() { - window.addEventListener(BxEvent.STREAM_PLAYING, () => { - if (STATES.currentStream.titleInfo?.details.hasMkbSupport) { - if (AppInterface && getPref("native_mkb_enabled") === "on") AppInterface && NativeMkbHandler.getInstance().init(); - } else if (getPref("mkb_enabled") && (AppInterface || !UserAgent.isMobile())) BxLogger.info(LOG_TAG2, "Emulate MKB"), EmulatedMkbHandler.getInstance().init(); - }); - } -} var BxExposed = { getTitleInfo: () => STATES.currentStream.titleInfo, modifyTitleInfo: !1, @@ -5189,7 +4889,7 @@ function interceptHttpRequests() { }; } function showGamepadToast(gamepad) { - if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) return; + if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; BxLogger.info("Gamepad", gamepad); let text = "🎮"; if (getPref("local_co_op_enabled")) text += ` #${gamepad.index + 1}`; diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index 5a5c0eb..a1844d4 100644 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -1795,6 +1795,254 @@ var MouseMapTo; MouseMapTo2[MouseMapTo2.LS = 1] = "LS"; MouseMapTo2[MouseMapTo2.RS = 2] = "RS"; })(MouseMapTo ||= {}); +class Toast { + static #$wrapper; + static #$msg; + static #$status; + static #stack = []; + static #isShowing = !1; + static #timeout; + static #DURATION = 3000; + static show(msg, status, options = {}) { + options = options || {}; + const args = Array.from(arguments); + if (options.instant) Toast.#stack = [args], Toast.#showNext(); + else Toast.#stack.push(args), !Toast.#isShowing && Toast.#showNext(); + } + static #showNext() { + if (!Toast.#stack.length) { + Toast.#isShowing = !1; + return; + } + Toast.#isShowing = !0, Toast.#timeout && clearTimeout(Toast.#timeout), Toast.#timeout = window.setTimeout(Toast.#hide, Toast.#DURATION); + const [msg, status, options] = Toast.#stack.shift(); + if (options && options.html) Toast.#$msg.innerHTML = msg; + else Toast.#$msg.textContent = msg; + if (status) Toast.#$status.classList.remove("bx-gone"), Toast.#$status.textContent = status; + else Toast.#$status.classList.add("bx-gone"); + const classList = Toast.#$wrapper.classList; + classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); + } + static #hide() { + Toast.#timeout = null; + const classList = Toast.#$wrapper.classList; + classList.remove("bx-show"), classList.add("bx-hide"); + } + static setup() { + Toast.#$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, Toast.#$msg = CE("span", { class: "bx-toast-msg" }), Toast.#$status = CE("span", { class: "bx-toast-status" })), Toast.#$wrapper.addEventListener("transitionend", (e) => { + const classList = Toast.#$wrapper.classList; + if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), Toast.#showNext(); + }), document.documentElement.appendChild(Toast.#$wrapper); + } +} +class MicrophoneShortcut { + static toggle(showToast = !0) { + if (!window.BX_EXPOSED.streamSession) return !1; + const enableMic = window.BX_EXPOSED.streamSession._microphoneState === "Enabled" ? !1 : !0; + try { + return window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic), showToast && Toast.show(t("microphone"), t(enableMic ? "unmuted" : "muted"), { instant: !0 }), enableMic; + } catch (e) { + console.log(e); + } + return !1; + } +} +class StreamUiShortcut { + static showHideStreamMenu() { + window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu(); + } +} +function checkForUpdate() { + if (SCRIPT_VERSION.includes("beta")) return; + const CHECK_INTERVAL_SECONDS = 7200, currentVersion = getPref("version_current"), lastCheck = getPref("version_last_check"), now = Math.round(+new Date / 1000); + if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) return; + setPref("version_last_check", now), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => { + setPref("version_latest", json.tag_name.substring(1)), setPref("version_current", SCRIPT_VERSION); + }), Translations.updateTranslations(currentVersion === SCRIPT_VERSION); +} +function disablePwa() { + if (!(window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase()) return; + if (!!AppInterface || UserAgent.isSafariMobile()) Object.defineProperty(window.navigator, "standalone", { + value: !0 + }); +} +function hashCode(str) { + let hash = 0; + for (let i = 0, len = str.length;i < len; i++) { + const chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr, hash |= 0; + } + return hash; +} +function renderString(str, obj) { + return str.replace(/\$\{.+?\}/g, (match) => { + const key = match.substring(2, match.length - 1); + if (key in obj) return obj[key]; + return match; + }); +} +function ceilToNearest(value, interval) { + return Math.ceil(value / interval) * interval; +} +function floorToNearest(value, interval) { + return Math.floor(value / interval) * interval; +} +async function copyToClipboard(text, showToast = !0) { + try { + return await navigator.clipboard.writeText(text), showToast && Toast.show("Copied to clipboard", "", { instant: !0 }), !0; + } catch (err) { + console.error("Failed to copy: ", err), showToast && Toast.show("Failed to copy", "", { instant: !0 }); + } + return !1; +} +function productTitleToSlug(title) { + return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase(); +} +class SoundShortcut { + static adjustGainNodeVolume(amount) { + if (!getPref("audio_enable_volume_control")) return 0; + const currentValue = getPref("audio_volume"); + let nearestValue; + if (amount > 0) nearestValue = ceilToNearest(currentValue, amount); + else nearestValue = floorToNearest(currentValue, -1 * amount); + let newValue; + if (currentValue !== nearestValue) newValue = nearestValue; + else newValue = currentValue + amount; + return newValue = setPref("audio_volume", newValue, !0), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue; + } + static setGainNodeVolume(value) { + STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); + } + static muteUnmute() { + if (getPref("audio_enable_volume_control") && STATES.currentStream.audioGainNode) { + const gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio_volume"); + let targetValue; + if (settingValue === 0) targetValue = 100, setPref("audio_volume", targetValue, !0); + else if (gainValue === 0) targetValue = settingValue; + else targetValue = 0; + let status; + if (targetValue === 0) status = t("muted"); + else status = targetValue + "%"; + SoundShortcut.setGainNodeVolume(targetValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { + speakerState: targetValue === 0 ? 1 : 0 + }); + return; + } + let $media; + if ($media = document.querySelector("div[data-testid=media-container] audio"), !$media) $media = document.querySelector("div[data-testid=media-container] video"); + if ($media) { + $media.muted = !$media.muted; + const status = $media.muted ? t("muted") : t("unmuted"); + Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { + speakerState: $media.muted ? 1 : 0 + }); + } + } +} +class BxSelectElement { + static wrap($select) { + $select.removeAttribute("tabindex"); + const $btnPrev = createButton({ + label: "<", + style: 32 + }), $btnNext = createButton({ + label: ">", + style: 32 + }), isMultiple = $select.multiple; + let $checkBox, $label, visibleIndex = $select.selectedIndex, $content; + if (isMultiple) $content = CE("button", { + class: "bx-select-value bx-focusable", + tabindex: 0 + }, $checkBox = CE("input", { type: "checkbox" }), $label = CE("span", {}, "")), $content.addEventListener("click", (e) => { + $checkBox.click(); + }), $checkBox.addEventListener("input", (e) => { + const $option = getOptionAtIndex(visibleIndex); + $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); + }); + else $content = CE("div", {}, $label = CE("label", { for: $select.id + "_checkbox" }, "")); + const getOptionAtIndex = (index) => { + return Array.from($select.querySelectorAll("option"))[index]; + }, render = (e) => { + if (e && e.manualTrigger) visibleIndex = $select.selectedIndex; + visibleIndex = normalizeIndex(visibleIndex); + const $option = getOptionAtIndex(visibleIndex); + let content = ""; + if ($option) if (content = $option.textContent || "", content && $option.parentElement.tagName === "OPTGROUP") { + $label.innerHTML = ""; + const fragment = document.createDocumentFragment(); + fragment.appendChild(CE("span", {}, $option.parentElement.label)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment); + } else $label.textContent = content; + else $label.textContent = content; + if ($label.classList.toggle("bx-line-through", $option && $option.disabled), isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); + const disablePrev = visibleIndex <= 0, disableNext = visibleIndex === $select.querySelectorAll("option").length - 1; + $btnPrev.classList.toggle("bx-inactive", disablePrev), $btnNext.classList.toggle("bx-inactive", disableNext), disablePrev && !disableNext && document.activeElement === $btnPrev && $btnNext.focus(), disableNext && !disablePrev && document.activeElement === $btnNext && $btnPrev.focus(); + }, normalizeIndex = (index) => { + return Math.min(Math.max(index, 0), $select.querySelectorAll("option").length - 1); + }, onPrevNext = (e) => { + if (!e.target) return; + const goNext = e.target.closest("button") === $btnNext, currentIndex = visibleIndex; + let newIndex = goNext ? currentIndex + 1 : currentIndex - 1; + if (newIndex = normalizeIndex(newIndex), visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; + if (isMultiple) render(); + else BxEvent.dispatch($select, "input"); + }; + $select.addEventListener("input", render), $btnPrev.addEventListener("click", onPrevNext), $btnNext.addEventListener("click", onPrevNext), new MutationObserver((mutationList, observer2) => { + mutationList.forEach((mutation) => { + if (mutation.type === "childList" || mutation.type === "attributes") render(); + }); + }).observe($select, { + subtree: !0, + childList: !0, + attributes: !0 + }), render(); + const $div = CE("div", { + class: "bx-select", + _nearby: { + orientation: "horizontal", + focus: $btnNext + } + }, $select, $btnPrev, $content, $btnNext); + return Object.defineProperty($div, "value", { + get() { + return $select.value; + }, + set(value) { + $div.setValue(value); + } + }), $div.addEventListener = function() { + $select.addEventListener.apply($select, arguments); + }, $div.removeEventListener = function() { + $select.removeEventListener.apply($select, arguments); + }, $div.dispatchEvent = function() { + return $select.dispatchEvent.apply($select, arguments); + }, $div.setValue = (value) => { + if ("setValue" in $select) $select.setValue(value); + else $select.value = value; + }, $div; + } +} +function onChangeVideoPlayerType() { + const playerType = getPref("video_player_type"), $videoProcessing = document.getElementById("bx_setting_video_processing"), $videoSharpness = document.getElementById("bx_setting_video_sharpness"), $videoPowerPreference = document.getElementById("bx_setting_video_power_preference"); + if (!$videoProcessing) return; + let isDisabled = !1; + const $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); + if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); + else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; + $videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), updateVideoPlayer(); +} +function updateVideoPlayer() { + const streamPlayer = STATES.currentStream.streamPlayer; + if (!streamPlayer) return; + const options = { + processing: getPref("video_processing"), + sharpness: getPref("video_sharpness"), + saturation: getPref("video_saturation"), + contrast: getPref("video_contrast"), + brightness: getPref("video_brightness") + }; + streamPlayer.setPlayerType(getPref("video_player_type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); +} +window.addEventListener("resize", updateVideoPlayer); class MkbPreset { static MOUSE_SETTINGS = { map_to: { @@ -1892,46 +2140,6 @@ class MkbPreset { return console.log(obj), obj; } } -class Toast { - static #$wrapper; - static #$msg; - static #$status; - static #stack = []; - static #isShowing = !1; - static #timeout; - static #DURATION = 3000; - static show(msg, status, options = {}) { - options = options || {}; - const args = Array.from(arguments); - if (options.instant) Toast.#stack = [args], Toast.#showNext(); - else Toast.#stack.push(args), !Toast.#isShowing && Toast.#showNext(); - } - static #showNext() { - if (!Toast.#stack.length) { - Toast.#isShowing = !1; - return; - } - Toast.#isShowing = !0, Toast.#timeout && clearTimeout(Toast.#timeout), Toast.#timeout = window.setTimeout(Toast.#hide, Toast.#DURATION); - const [msg, status, options] = Toast.#stack.shift(); - if (options && options.html) Toast.#$msg.innerHTML = msg; - else Toast.#$msg.textContent = msg; - if (status) Toast.#$status.classList.remove("bx-gone"), Toast.#$status.textContent = status; - else Toast.#$status.classList.add("bx-gone"); - const classList = Toast.#$wrapper.classList; - classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); - } - static #hide() { - Toast.#timeout = null; - const classList = Toast.#$wrapper.classList; - classList.remove("bx-show"), classList.add("bx-hide"); - } - static setup() { - Toast.#$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, Toast.#$msg = CE("span", { class: "bx-toast-msg" }), Toast.#$status = CE("span", { class: "bx-toast-status" })), Toast.#$wrapper.addEventListener("transitionend", (e) => { - const classList = Toast.#$wrapper.classList; - if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), Toast.#showNext(); - }), document.documentElement.appendChild(Toast.#$wrapper); - } -} class LocalDb { static #instance; static get INSTANCE() { @@ -2292,28 +2500,324 @@ class NativeMkbHandler extends MkbHandler { }); } } -function onChangeVideoPlayerType() { - const playerType = getPref("video_player_type"), $videoProcessing = document.getElementById("bx_setting_video_processing"), $videoSharpness = document.getElementById("bx_setting_video_sharpness"), $videoPowerPreference = document.getElementById("bx_setting_video_power_preference"); - if (!$videoProcessing) return; - let isDisabled = !1; - const $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); - if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); - else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; - $videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), updateVideoPlayer(); +var LOG_TAG2 = "MkbHandler", PointerToMouseButton = { + 1: 0, + 2: 2, + 4: 1 +}, VIRTUAL_GAMEPAD_ID = "Xbox 360 Controller"; +class WebSocketMouseDataProvider extends MouseDataProvider { + #pointerClient; + #connected = !1; + init() { + this.#pointerClient = PointerClient.getInstance(), this.#connected = !1; + try { + this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.#connected = !0; + } catch (e) { + Toast.show("Cannot enable Mouse & Keyboard feature"); + } + } + start() { + this.#connected && AppInterface.requestPointerCapture(); + } + stop() { + this.#connected && AppInterface.releasePointerCapture(); + } + destroy() { + this.#connected && this.#pointerClient?.stop(); + } } -function updateVideoPlayer() { - const streamPlayer = STATES.currentStream.streamPlayer; - if (!streamPlayer) return; - const options = { - processing: getPref("video_processing"), - sharpness: getPref("video_sharpness"), - saturation: getPref("video_saturation"), - contrast: getPref("video_contrast"), - brightness: getPref("video_brightness") +class PointerLockMouseDataProvider extends MouseDataProvider { + init() {} + start() { + window.addEventListener("mousemove", this.#onMouseMoveEvent), window.addEventListener("mousedown", this.#onMouseEvent), window.addEventListener("mouseup", this.#onMouseEvent), window.addEventListener("wheel", this.#onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.#disableContextMenu); + } + stop() { + document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.#onMouseMoveEvent), window.removeEventListener("mousedown", this.#onMouseEvent), window.removeEventListener("mouseup", this.#onMouseEvent), window.removeEventListener("wheel", this.#onWheelEvent), window.removeEventListener("contextmenu", this.#disableContextMenu); + } + destroy() {} + #onMouseMoveEvent = (e) => { + this.mkbHandler.handleMouseMove({ + movementX: e.movementX, + movementY: e.movementY + }); }; - streamPlayer.setPlayerType(getPref("video_player_type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); + #onMouseEvent = (e) => { + e.preventDefault(); + const isMouseDown = e.type === "mousedown", data = { + mouseButton: e.button, + pressed: isMouseDown + }; + this.mkbHandler.handleMouseClick(data); + }; + #onWheelEvent = (e) => { + if (!KeyHelper.getKeyFromEvent(e)) return; + const data = { + vertical: e.deltaY, + horizontal: e.deltaX + }; + if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); + }; + #disableContextMenu = (e) => e.preventDefault(); +} +class EmulatedMkbHandler extends MkbHandler { + static #instance; + static getInstance() { + if (!EmulatedMkbHandler.#instance) EmulatedMkbHandler.#instance = new EmulatedMkbHandler; + return EmulatedMkbHandler.#instance; + } + #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); + static DEFAULT_PANNING_SENSITIVITY = 0.001; + static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; + static MAXIMUM_STICK_RANGE = 1.1; + #VIRTUAL_GAMEPAD = { + id: VIRTUAL_GAMEPAD_ID, + index: 3, + connected: !1, + hapticActuators: null, + mapping: "standard", + axes: [0, 0, 0, 0], + buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })), + timestamp: performance.now(), + vibrationActuator: null + }; + #nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator); + #enabled = !1; + #mouseDataProvider; + #isPolling = !1; + #prevWheelCode = null; + #wheelStoppedTimeout; + #detectMouseStoppedTimeout; + #$message; + #escKeyDownTime = -1; + #STICK_MAP; + #LEFT_STICK_X = []; + #LEFT_STICK_Y = []; + #RIGHT_STICK_X = []; + #RIGHT_STICK_Y = []; + constructor() { + super(); + this.#STICK_MAP = { + 102: [this.#LEFT_STICK_X, 0, -1], + 103: [this.#LEFT_STICK_X, 0, 1], + 100: [this.#LEFT_STICK_Y, 1, -1], + 101: [this.#LEFT_STICK_Y, 1, 1], + 202: [this.#RIGHT_STICK_X, 2, -1], + 203: [this.#RIGHT_STICK_X, 2, 1], + 200: [this.#RIGHT_STICK_Y, 3, -1], + 201: [this.#RIGHT_STICK_Y, 3, 1] + }; + } + isEnabled = () => this.#enabled; + #patchedGetGamepads = () => { + const gamepads = this.#nativeGetGamepads() || []; + return gamepads[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD, gamepads; + }; + #getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD; + #updateStick(stick, x, y) { + const virtualGamepad = this.#getVirtualGamepad(); + virtualGamepad.axes[stick * 2] = x, virtualGamepad.axes[stick * 2 + 1] = y, virtualGamepad.timestamp = performance.now(); + } + #vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); + #resetGamepad = () => { + const gamepad = this.#getVirtualGamepad(); + gamepad.axes = [0, 0, 0, 0]; + for (let button of gamepad.buttons) + button.pressed = !1, button.value = 0; + gamepad.timestamp = performance.now(); + }; + #pressButton = (buttonIndex, pressed) => { + const virtualGamepad = this.#getVirtualGamepad(); + if (buttonIndex >= 100) { + let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]; + valueArr = valueArr, axisIndex = axisIndex; + for (let i = valueArr.length - 1;i >= 0; i--) + if (valueArr[i] === buttonIndex) valueArr.splice(i, 1); + pressed && valueArr.push(buttonIndex); + let value; + if (valueArr.length) value = this.#STICK_MAP[valueArr[valueArr.length - 1]][2]; + else value = 0; + virtualGamepad.axes[axisIndex] = value; + } else virtualGamepad.buttons[buttonIndex].pressed = pressed, virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0; + virtualGamepad.timestamp = performance.now(); + }; + #onKeyboardEvent = (e) => { + const isKeyDown = e.type === "keydown"; + if (e.code === "F8") { + if (!isKeyDown) e.preventDefault(), this.toggle(); + return; + } + if (e.code === "Escape") { + if (e.preventDefault(), this.#enabled && isKeyDown) { + if (this.#escKeyDownTime === -1) this.#escKeyDownTime = performance.now(); + else if (performance.now() - this.#escKeyDownTime >= 1000) this.stop(); + } else this.#escKeyDownTime = -1; + return; + } + if (!this.#isPolling) return; + const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]; + if (typeof buttonIndex === "undefined") return; + if (e.repeat) return; + e.preventDefault(), this.#pressButton(buttonIndex, isKeyDown); + }; + #onMouseStopped = () => { + this.#detectMouseStoppedTimeout = null; + const analog = this.#CURRENT_PRESET_DATA.mouse["map_to"] === 1 ? 0 : 1; + this.#updateStick(analog, 0, 0); + }; + handleMouseClick = (data) => { + let mouseButton; + if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton; + else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton]; + const keyCode = "Mouse" + mouseButton, key = { + code: keyCode, + name: KeyHelper.codeToKeyName(keyCode) + }; + if (!key.name) return; + const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + if (typeof buttonIndex === "undefined") return; + this.#pressButton(buttonIndex, data.pressed); + }; + handleMouseMove = (data) => { + const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse["map_to"]; + if (mouseMapTo === 0) return; + this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout), this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50); + const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse["deadzone_counterweight"]; + let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse["sensitivity_x"], y = data.movementY * this.#CURRENT_PRESET_DATA.mouse["sensitivity_y"], length = this.#vectorLength(x, y); + if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; + else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length, y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length; + const analog = mouseMapTo === 1 ? 0 : 1; + this.#updateStick(analog, x, y); + }; + handleMouseWheel = (data) => { + let code = ""; + if (data.vertical < 0) code = "ScrollUp"; + else if (data.vertical > 0) code = "ScrollDown"; + else if (data.horizontal < 0) code = "ScrollLeft"; + else if (data.horizontal > 0) code = "ScrollRight"; + if (!code) return !1; + const key = { + code, + name: KeyHelper.codeToKeyName(code) + }, buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + if (typeof buttonIndex === "undefined") return !1; + if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout), this.#pressButton(buttonIndex, !0); + return this.#wheelStoppedTimeout = window.setTimeout(() => { + this.#prevWheelCode = null, this.#pressButton(buttonIndex, !1); + }, 20), !0; + }; + toggle = (force) => { + if (typeof force !== "undefined") this.#enabled = force; + else this.#enabled = !this.#enabled; + if (this.#enabled) document.body.requestPointerLock(); + else document.pointerLockElement && document.exitPointerLock(); + }; + #getCurrentPreset = () => { + return new Promise((resolve) => { + const presetId = getPref("mkb_default_preset_id"); + LocalDb.INSTANCE.getPreset(presetId).then((preset) => { + resolve(preset); + }); + }); + }; + refreshPresetData = () => { + this.#getCurrentPreset().then((preset) => { + this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET), this.#resetGamepad(); + }); + }; + waitForMouseData = (wait) => { + this.#$message && this.#$message.classList.toggle("bx-gone", !wait); + }; + #onPollingModeChanged = (e) => { + if (!this.#$message) return; + if (e.mode === "none") this.#$message.classList.remove("bx-offscreen"); + else this.#$message.classList.add("bx-offscreen"); + }; + #onDialogShown = () => { + document.pointerLockElement && document.exitPointerLock(); + }; + #initMessage = () => { + if (!this.#$message) this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, CE("div", {}, CE("p", {}, t("virtual-controller")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "virtual" }, createButton({ + style: 1 | 256 | 64, + label: t("activate"), + onClick: ((e) => { + e.preventDefault(), e.stopPropagation(), this.toggle(!0); + }).bind(this) + }), CE("div", {}, createButton({ + label: t("ignore"), + style: 4, + onClick: (e) => { + e.preventDefault(), e.stopPropagation(), this.toggle(!1), this.waitForMouseData(!1); + } + }), createButton({ + label: t("edit"), + onClick: (e) => { + e.preventDefault(), e.stopPropagation(); + const dialog = SettingsNavigationDialog.getInstance(); + dialog.focusTab("mkb"), NavigationDialogManager.getInstance().show(dialog); + } + })))); + if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); + }; + #onPointerLockChange = () => { + if (document.pointerLockElement) this.start(); + else this.stop(); + }; + #onPointerLockError = (e) => { + console.log(e), this.stop(); + }; + #onPointerLockRequested = () => { + this.start(); + }; + #onPointerLockExited = () => { + this.#mouseDataProvider?.stop(); + }; + handleEvent(event) { + switch (event.type) { + case BxEvent.POINTER_LOCK_REQUESTED: + this.#onPointerLockRequested(); + break; + case BxEvent.POINTER_LOCK_EXITED: + this.#onPointerLockExited(); + break; + } + } + init = () => { + if (this.refreshPresetData(), this.#enabled = !1, AppInterface) this.#mouseDataProvider = new WebSocketMouseDataProvider(this); + else this.#mouseDataProvider = new PointerLockMouseDataProvider(this); + if (this.#mouseDataProvider.init(), window.addEventListener("keydown", this.#onKeyboardEvent), window.addEventListener("keyup", this.#onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this); + else document.addEventListener("pointerlockchange", this.#onPointerLockChange), document.addEventListener("pointerlockerror", this.#onPointerLockError); + if (this.#initMessage(), this.#$message?.classList.add("bx-gone"), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("virtual-controller"), { html: !0 }), this.waitForMouseData(!1); + else this.waitForMouseData(!0); + }; + destroy = () => { + if (this.#isPolling = !1, this.#enabled = !1, this.stop(), this.waitForMouseData(!1), document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("keydown", this.#onKeyboardEvent), window.removeEventListener("keyup", this.#onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this); + else document.removeEventListener("pointerlockchange", this.#onPointerLockChange), document.removeEventListener("pointerlockerror", this.#onPointerLockError); + window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), this.#mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged); + }; + start = () => { + if (!this.#enabled) this.#enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); + this.#isPolling = !0, this.#escKeyDownTime = -1, this.#resetGamepad(), window.navigator.getGamepads = this.#patchedGetGamepads, this.waitForMouseData(!1), this.#mouseDataProvider?.start(); + const virtualGamepad = this.#getVirtualGamepad(); + virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", { + gamepad: virtualGamepad + }), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); + }; + stop = () => { + this.#enabled = !1, this.#isPolling = !1, this.#escKeyDownTime = -1; + const virtualGamepad = this.#getVirtualGamepad(); + if (virtualGamepad.connected) this.#resetGamepad(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", { + gamepad: virtualGamepad + }), window.navigator.getGamepads = this.#nativeGetGamepads; + this.waitForMouseData(!0), this.#mouseDataProvider?.stop(); + }; + static setupEvents() { + window.addEventListener(BxEvent.STREAM_PLAYING, () => { + if (STATES.currentStream.titleInfo?.details.hasMkbSupport) { + if (AppInterface && getPref("native_mkb_enabled") === "on") AppInterface && NativeMkbHandler.getInstance().init(); + } else if (getPref("mkb_enabled") && (AppInterface || !UserAgent.isMobile())) BxLogger.info(LOG_TAG2, "Emulate MKB"), EmulatedMkbHandler.getInstance().init(); + }); + } } -window.addEventListener("resize", updateVideoPlayer); class NavigationDialog { dialogManager; constructor() { @@ -2439,7 +2943,7 @@ class NavigationDialogManager { const gamepads = window.navigator.getGamepads(); for (let gamepad of gamepads) { if (!gamepad || !gamepad.connected) continue; - if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) continue; + if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; const { axes, buttons } = gamepad; let releasedButton = null, heldButton = null, lastState = this.gamepadLastStates[gamepad.index], lastTimestamp, lastKey, lastKeyPressed; if (lastState) [lastTimestamp, lastKey, lastKeyPressed] = lastState; @@ -2930,94 +3434,7 @@ class MkbRemapper { return this.#$.wrapper.appendChild($actionButtons), this.#toggleEditing(!1), this.#refresh(), this.#$.wrapper; } } -function checkForUpdate() { - if (SCRIPT_VERSION.includes("beta")) return; - const CHECK_INTERVAL_SECONDS = 7200, currentVersion = getPref("version_current"), lastCheck = getPref("version_last_check"), now = Math.round(+new Date / 1000); - if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) return; - setPref("version_last_check", now), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => { - setPref("version_latest", json.tag_name.substring(1)), setPref("version_current", SCRIPT_VERSION); - }), Translations.updateTranslations(currentVersion === SCRIPT_VERSION); -} -function disablePwa() { - if (!(window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase()) return; - if (!!AppInterface || UserAgent.isSafariMobile()) Object.defineProperty(window.navigator, "standalone", { - value: !0 - }); -} -function hashCode(str) { - let hash = 0; - for (let i = 0, len = str.length;i < len; i++) { - const chr = str.charCodeAt(i); - hash = (hash << 5) - hash + chr, hash |= 0; - } - return hash; -} -function renderString(str, obj) { - return str.replace(/\$\{.+?\}/g, (match) => { - const key = match.substring(2, match.length - 1); - if (key in obj) return obj[key]; - return match; - }); -} -function ceilToNearest(value, interval) { - return Math.ceil(value / interval) * interval; -} -function floorToNearest(value, interval) { - return Math.floor(value / interval) * interval; -} -async function copyToClipboard(text, showToast = !0) { - try { - return await navigator.clipboard.writeText(text), showToast && Toast.show("Copied to clipboard", "", { instant: !0 }), !0; - } catch (err) { - console.error("Failed to copy: ", err), showToast && Toast.show("Failed to copy", "", { instant: !0 }); - } - return !1; -} -function productTitleToSlug(title) { - return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase(); -} -class SoundShortcut { - static adjustGainNodeVolume(amount) { - if (!getPref("audio_enable_volume_control")) return 0; - const currentValue = getPref("audio_volume"); - let nearestValue; - if (amount > 0) nearestValue = ceilToNearest(currentValue, amount); - else nearestValue = floorToNearest(currentValue, -1 * amount); - let newValue; - if (currentValue !== nearestValue) newValue = nearestValue; - else newValue = currentValue + amount; - return newValue = setPref("audio_volume", newValue, !0), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue; - } - static setGainNodeVolume(value) { - STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); - } - static muteUnmute() { - if (getPref("audio_enable_volume_control") && STATES.currentStream.audioGainNode) { - const gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio_volume"); - let targetValue; - if (settingValue === 0) targetValue = 100, setPref("audio_volume", targetValue, !0); - else if (gainValue === 0) targetValue = settingValue; - else targetValue = 0; - let status; - if (targetValue === 0) status = t("muted"); - else status = targetValue + "%"; - SoundShortcut.setGainNodeVolume(targetValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { - speakerState: targetValue === 0 ? 1 : 0 - }); - return; - } - let $media; - if ($media = document.querySelector("div[data-testid=media-container] audio"), !$media) $media = document.querySelector("div[data-testid=media-container] video"); - if ($media) { - $media.muted = !$media.muted; - const status = $media.muted ? t("muted") : t("unmuted"); - Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { - speakerState: $media.muted ? 1 : 0 - }); - } - } -} -var LOG_TAG2 = "TouchController"; +var LOG_TAG3 = "TouchController"; class TouchController { static #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent("message", { data: JSON.stringify({ @@ -3113,12 +3530,12 @@ class TouchController { } const xboxTitleId = TouchController.#xboxTitleId; if (!xboxTitleId) { - BxLogger.error(LOG_TAG2, "Invalid xboxTitleId"); + BxLogger.error(LOG_TAG3, "Invalid xboxTitleId"); return; } if (!layoutId) layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null; if (!layoutId) { - BxLogger.error(LOG_TAG2, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault(); + BxLogger.error(LOG_TAG3, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault(); return; } const layoutChanged = TouchController.#currentLayoutId !== layoutId; @@ -3201,7 +3618,7 @@ class TouchController { TouchController.setXboxTitleId(parseInt(json.titleid, 16).toString()); } } catch (e2) { - BxLogger.error(LOG_TAG2, "Load custom layout", e2); + BxLogger.error(LOG_TAG3, "Load custom layout", e2); } }); }); @@ -3280,88 +3697,6 @@ class VibrationManager { }); } } -class BxSelectElement { - static wrap($select) { - $select.removeAttribute("tabindex"); - const $btnPrev = createButton({ - label: "<", - style: 32 - }), $btnNext = createButton({ - label: ">", - style: 32 - }), isMultiple = $select.multiple; - let $checkBox, $label, visibleIndex = $select.selectedIndex, $content; - if (isMultiple) $content = CE("button", { - class: "bx-select-value bx-focusable", - tabindex: 0 - }, $checkBox = CE("input", { type: "checkbox" }), $label = CE("span", {}, "")), $content.addEventListener("click", (e) => { - $checkBox.click(); - }), $checkBox.addEventListener("input", (e) => { - const $option = getOptionAtIndex(visibleIndex); - $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); - }); - else $content = CE("div", {}, $label = CE("label", { for: $select.id + "_checkbox" }, "")); - const getOptionAtIndex = (index) => { - return Array.from($select.querySelectorAll("option"))[index]; - }, render = (e) => { - if (e && e.manualTrigger) visibleIndex = $select.selectedIndex; - visibleIndex = normalizeIndex(visibleIndex); - const $option = getOptionAtIndex(visibleIndex); - let content = ""; - if ($option) if (content = $option.textContent || "", content && $option.parentElement.tagName === "OPTGROUP") { - $label.innerHTML = ""; - const fragment = document.createDocumentFragment(); - fragment.appendChild(CE("span", {}, $option.parentElement.label)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment); - } else $label.textContent = content; - else $label.textContent = content; - if ($label.classList.toggle("bx-line-through", $option && $option.disabled), isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); - const disablePrev = visibleIndex <= 0, disableNext = visibleIndex === $select.querySelectorAll("option").length - 1; - $btnPrev.classList.toggle("bx-inactive", disablePrev), $btnNext.classList.toggle("bx-inactive", disableNext), disablePrev && !disableNext && document.activeElement === $btnPrev && $btnNext.focus(), disableNext && !disablePrev && document.activeElement === $btnNext && $btnPrev.focus(); - }, normalizeIndex = (index) => { - return Math.min(Math.max(index, 0), $select.querySelectorAll("option").length - 1); - }, onPrevNext = (e) => { - if (!e.target) return; - const goNext = e.target.closest("button") === $btnNext, currentIndex = visibleIndex; - let newIndex = goNext ? currentIndex + 1 : currentIndex - 1; - if (newIndex = normalizeIndex(newIndex), visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; - if (isMultiple) render(); - else BxEvent.dispatch($select, "input"); - }; - $select.addEventListener("input", render), $btnPrev.addEventListener("click", onPrevNext), $btnNext.addEventListener("click", onPrevNext), new MutationObserver((mutationList, observer2) => { - mutationList.forEach((mutation) => { - if (mutation.type === "childList" || mutation.type === "attributes") render(); - }); - }).observe($select, { - subtree: !0, - childList: !0, - attributes: !0 - }), render(); - const $div = CE("div", { - class: "bx-select", - _nearby: { - orientation: "horizontal", - focus: $btnNext - } - }, $select, $btnPrev, $content, $btnNext); - return Object.defineProperty($div, "value", { - get() { - return $select.value; - }, - set(value) { - $div.setValue(value); - } - }), $div.addEventListener = function() { - $select.addEventListener.apply($select, arguments); - }, $div.removeEventListener = function() { - $select.removeEventListener.apply($select, arguments); - }, $div.dispatchEvent = function() { - return $select.dispatchEvent.apply($select, arguments); - }, $div.setValue = (value) => { - if ("setValue" in $select) $select.setValue(value); - else $select.value = value; - }, $div; - } -} var controller_shortcuts_default = "if (window.BX_EXPOSED.disableGamepadPolling) {\n this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(50) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, 50);\n return;\n}\n\nconst currentGamepad = ${gamepadVar};\n\n// Share button on XS controller\nif (currentGamepad.buttons[17] && currentGamepad.buttons[17].pressed) {\n window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));\n}\n\nconst btnHome = currentGamepad.buttons[16];\nif (btnHome) {\n if (!this.bxHomeStates) {\n this.bxHomeStates = {};\n }\n\n let intervalMs = 0;\n let hijack = false;\n\n if (btnHome.pressed) {\n hijack = true;\n intervalMs = 16;\n this.gamepadIsIdle.set(currentGamepad.index, false);\n\n if (this.bxHomeStates[currentGamepad.index]) {\n const lastTimestamp = this.bxHomeStates[currentGamepad.index].timestamp;\n\n if (currentGamepad.timestamp !== lastTimestamp) {\n this.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp;\n\n const handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad);\n if (handled) {\n this.bxHomeStates[currentGamepad.index].shortcutPressed += 1;\n }\n }\n } else {\n // First time pressing > save current timestamp\n window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index);\n this.bxHomeStates[currentGamepad.index] = {\n shortcutPressed: 0,\n timestamp: currentGamepad.timestamp,\n };\n }\n } else if (this.bxHomeStates[currentGamepad.index]) {\n hijack = true;\n const info = structuredClone(this.bxHomeStates[currentGamepad.index]);\n\n // Home button released\n this.bxHomeStates[currentGamepad.index] = null;\n\n if (info.shortcutPressed === 0) {\n const fakeGamepadMappings = [{\n GamepadIndex: currentGamepad.index,\n A: 0,\n B: 0,\n X: 0,\n Y: 0,\n LeftShoulder: 0,\n RightShoulder: 0,\n LeftTrigger: 0,\n RightTrigger: 0,\n View: 0,\n Menu: 0,\n LeftThumb: 0,\n RightThumb: 0,\n DPadUp: 0,\n DPadDown: 0,\n DPadLeft: 0,\n DPadRight: 0,\n Nexus: 1,\n LeftThumbXAxis: 0,\n LeftThumbYAxis: 0,\n RightThumbXAxis: 0,\n RightThumbYAxis: 0,\n PhysicalPhysicality: 0,\n VirtualPhysicality: 0,\n Dirty: true,\n Virtual: false,\n }];\n\n const isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500;\n intervalMs = isLongPress ? 500 : 100;\n\n this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings);\n } else {\n intervalMs = 4;\n }\n }\n\n if (hijack && intervalMs) {\n // Listen to next button press\n this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs);\n\n // Hijack this button\n return;\n }\n}\n"; var expose_stream_session_default = "window.BX_EXPOSED.streamSession = this;\n\nconst orgSetMicrophoneState = this.setMicrophoneState.bind(this);\nthis.setMicrophoneState = state => {\n orgSetMicrophoneState(state);\n\n const evt = new Event(BxEvent.MICROPHONE_STATE_CHANGED);\n evt.microphoneState = state;\n\n window.dispatchEvent(evt);\n};\n\nwindow.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY));\n\n// Patch updateDimensions() to make native touch work correctly with WebGL2\nlet updateDimensionsStr = this.updateDimensions.toString();\n\nif (updateDimensionsStr.startsWith('function ')) {\n updateDimensionsStr = updateDimensionsStr.substring(9);\n}\n\n// if(r){\nconst renderTargetVar = updateDimensionsStr.match(/if\\((\\w+)\\){/)[1];\n\nupdateDimensionsStr = updateDimensionsStr.replaceAll(renderTargetVar + '.scroll', 'scroll');\n\nupdateDimensionsStr = updateDimensionsStr.replace(`if(${renderTargetVar}){`, `\nif (${renderTargetVar}) {\n const scrollWidth = ${renderTargetVar}.dataset.width ? parseInt(${renderTargetVar}.dataset.width) : ${renderTargetVar}.scrollWidth;\n const scrollHeight = ${renderTargetVar}.dataset.height ? parseInt(${renderTargetVar}.dataset.height) : ${renderTargetVar}.scrollHeight;\n`);\n\neval(`this.updateDimensions = function ${updateDimensionsStr}`);\n"; var local_co_op_enable_default = "let match;\nlet onGamepadChangedStr = this.onGamepadChanged.toString();\n\nif (onGamepadChangedStr.startsWith('function ')) {\n onGamepadChangedStr = onGamepadChangedStr.substring(9);\n}\n\nonGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]');\neval(`this.onGamepadChanged = function ${onGamepadChangedStr}`);\n\nlet onGamepadInputStr = this.onGamepadInput.toString();\n\nmatch = onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/);\nif (match) {\n const gamepadIndexVar = match[0];\n onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', `this.gamepadStates.get(${gamepadIndexVar},`);\n eval(`this.onGamepadInput = function ${onGamepadInputStr}`);\n BxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support');\n} else {\n BxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support');\n}\n"; @@ -3396,7 +3731,7 @@ class PatcherUtils { return txt.substring(0, index) + toString + txt.substring(index + fromString.length); } } -var ENDING_CHUNKS_PATCH_NAME = "loadingEndingChunks", LOG_TAG3 = "Patcher", PATCHES = { +var ENDING_CHUNKS_PATCH_NAME = "loadingEndingChunks", LOG_TAG4 = "Patcher", PATCHES = { disableAiTrack(str) { let text = ".track=function("; const index = str.indexOf(text); @@ -3528,7 +3863,7 @@ logFunc(logTag, '//', logMessage); loadingEndingChunks(str) { let text = '"FamilySagaManager"'; if (!str.includes(text)) return !1; - return BxLogger.info(LOG_TAG3, "Remaining patches:", PATCH_ORDERS), PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS), str; + return BxLogger.info(LOG_TAG4, "Remaining patches:", PATCH_ORDERS), PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS), str; }, disableStreamGate(str) { const index = str.indexOf('case"partially-ready":'); @@ -3897,7 +4232,7 @@ class Patcher { if (arguments[1] === 0 || typeof arguments[1] === "function") valid = !0; } if (!valid) return nativeBind.apply(this, arguments); - if (PatcherCache.init(), typeof arguments[1] === "function") BxLogger.info(LOG_TAG3, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind; + if (PatcherCache.init(), typeof arguments[1] === "function") BxLogger.info(LOG_TAG4, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind; const orgFunc = this, newFunc = (a, item2) => { Patcher.patch(item2), orgFunc(a, item2); }; @@ -3921,12 +4256,12 @@ class Patcher { if (!PATCHES[patchName]) continue; const tmpStr = PATCHES[patchName].call(null, patchedFuncStr); if (!tmpStr) continue; - modified = !0, patchedFuncStr = tmpStr, BxLogger.info(LOG_TAG3, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName); + modified = !0, patchedFuncStr = tmpStr, BxLogger.info(LOG_TAG4, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName); } if (modified) try { item[1][id] = eval(patchedFuncStr); } catch (e) { - if (e instanceof Error) BxLogger.error(LOG_TAG3, "Error", appliedPatches, e.message, patchedFuncStr); + if (e instanceof Error) BxLogger.error(LOG_TAG4, "Error", appliedPatches, e.message, patchedFuncStr); } if (appliedPatches.length) patchesMap[id] = appliedPatches; } @@ -3950,8 +4285,8 @@ class PatcherCache { } static checkSignature() { const storedSig = window.localStorage.getItem(PatcherCache.#KEY_SIGNATURE) || 0, currentSig = PatcherCache.#getSignature(); - if (currentSig !== parseInt(storedSig)) BxLogger.warning(LOG_TAG3, "Signature changed"), window.localStorage.setItem(PatcherCache.#KEY_SIGNATURE, currentSig.toString()), PatcherCache.clear(); - else BxLogger.info(LOG_TAG3, "Signature unchanged"); + if (currentSig !== parseInt(storedSig)) BxLogger.warning(LOG_TAG4, "Signature changed"), window.localStorage.setItem(PatcherCache.#KEY_SIGNATURE, currentSig.toString()), PatcherCache.clear(); + else BxLogger.info(LOG_TAG4, "Signature unchanged"); } static #cleanupPatches(patches) { return patches.filter((item2) => { @@ -3975,9 +4310,9 @@ class PatcherCache { } static init() { if (PatcherCache.#isInitialized) return; - if (PatcherCache.#isInitialized = !0, PatcherCache.checkSignature(), PatcherCache.#CACHE = JSON.parse(window.localStorage.getItem(PatcherCache.#KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG3, PatcherCache.#CACHE), window.location.pathname.includes("/play/")) PATCH_ORDERS.push(...PLAYING_PATCH_ORDERS); + if (PatcherCache.#isInitialized = !0, PatcherCache.checkSignature(), PatcherCache.#CACHE = JSON.parse(window.localStorage.getItem(PatcherCache.#KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG4, PatcherCache.#CACHE), window.location.pathname.includes("/play/")) PATCH_ORDERS.push(...PLAYING_PATCH_ORDERS); else PATCH_ORDERS.push(ENDING_CHUNKS_PATCH_NAME); - PATCH_ORDERS = PatcherCache.#cleanupPatches(PATCH_ORDERS), PLAYING_PATCH_ORDERS = PatcherCache.#cleanupPatches(PLAYING_PATCH_ORDERS), BxLogger.info(LOG_TAG3, PATCH_ORDERS.slice(0)), BxLogger.info(LOG_TAG3, PLAYING_PATCH_ORDERS.slice(0)); + PATCH_ORDERS = PatcherCache.#cleanupPatches(PATCH_ORDERS), PLAYING_PATCH_ORDERS = PatcherCache.#cleanupPatches(PLAYING_PATCH_ORDERS), BxLogger.info(LOG_TAG4, PATCH_ORDERS.slice(0)), BxLogger.info(LOG_TAG4, PLAYING_PATCH_ORDERS.slice(0)); } } class FullscreenText { @@ -4927,342 +5262,6 @@ class SettingsNavigationDialog extends NavigationDialog { return handled; } } -var LOG_TAG4 = "MkbHandler", PointerToMouseButton = { - 1: 0, - 2: 2, - 4: 1 -}; -class WebSocketMouseDataProvider extends MouseDataProvider { - #pointerClient; - #connected = !1; - init() { - this.#pointerClient = PointerClient.getInstance(), this.#connected = !1; - try { - this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.#connected = !0; - } catch (e) { - Toast.show("Cannot enable Mouse & Keyboard feature"); - } - } - start() { - this.#connected && AppInterface.requestPointerCapture(); - } - stop() { - this.#connected && AppInterface.releasePointerCapture(); - } - destroy() { - this.#connected && this.#pointerClient?.stop(); - } -} -class PointerLockMouseDataProvider extends MouseDataProvider { - init() {} - start() { - window.addEventListener("mousemove", this.#onMouseMoveEvent), window.addEventListener("mousedown", this.#onMouseEvent), window.addEventListener("mouseup", this.#onMouseEvent), window.addEventListener("wheel", this.#onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.#disableContextMenu); - } - stop() { - document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.#onMouseMoveEvent), window.removeEventListener("mousedown", this.#onMouseEvent), window.removeEventListener("mouseup", this.#onMouseEvent), window.removeEventListener("wheel", this.#onWheelEvent), window.removeEventListener("contextmenu", this.#disableContextMenu); - } - destroy() {} - #onMouseMoveEvent = (e) => { - this.mkbHandler.handleMouseMove({ - movementX: e.movementX, - movementY: e.movementY - }); - }; - #onMouseEvent = (e) => { - e.preventDefault(); - const isMouseDown = e.type === "mousedown", data = { - mouseButton: e.button, - pressed: isMouseDown - }; - this.mkbHandler.handleMouseClick(data); - }; - #onWheelEvent = (e) => { - if (!KeyHelper.getKeyFromEvent(e)) return; - const data = { - vertical: e.deltaY, - horizontal: e.deltaX - }; - if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); - }; - #disableContextMenu = (e) => e.preventDefault(); -} -class EmulatedMkbHandler extends MkbHandler { - static #instance; - static getInstance() { - if (!EmulatedMkbHandler.#instance) EmulatedMkbHandler.#instance = new EmulatedMkbHandler; - return EmulatedMkbHandler.#instance; - } - #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); - static DEFAULT_PANNING_SENSITIVITY = 0.001; - static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; - static MAXIMUM_STICK_RANGE = 1.1; - static VIRTUAL_GAMEPAD_ID = "Xbox 360 Controller"; - #VIRTUAL_GAMEPAD = { - id: EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID, - index: 3, - connected: !1, - hapticActuators: null, - mapping: "standard", - axes: [0, 0, 0, 0], - buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })), - timestamp: performance.now(), - vibrationActuator: null - }; - #nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator); - #enabled = !1; - #mouseDataProvider; - #isPolling = !1; - #prevWheelCode = null; - #wheelStoppedTimeout; - #detectMouseStoppedTimeout; - #$message; - #escKeyDownTime = -1; - #STICK_MAP; - #LEFT_STICK_X = []; - #LEFT_STICK_Y = []; - #RIGHT_STICK_X = []; - #RIGHT_STICK_Y = []; - constructor() { - super(); - this.#STICK_MAP = { - 102: [this.#LEFT_STICK_X, 0, -1], - 103: [this.#LEFT_STICK_X, 0, 1], - 100: [this.#LEFT_STICK_Y, 1, -1], - 101: [this.#LEFT_STICK_Y, 1, 1], - 202: [this.#RIGHT_STICK_X, 2, -1], - 203: [this.#RIGHT_STICK_X, 2, 1], - 200: [this.#RIGHT_STICK_Y, 3, -1], - 201: [this.#RIGHT_STICK_Y, 3, 1] - }; - } - isEnabled = () => this.#enabled; - #patchedGetGamepads = () => { - const gamepads = this.#nativeGetGamepads() || []; - return gamepads[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD, gamepads; - }; - #getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD; - #updateStick(stick, x, y) { - const virtualGamepad = this.#getVirtualGamepad(); - virtualGamepad.axes[stick * 2] = x, virtualGamepad.axes[stick * 2 + 1] = y, virtualGamepad.timestamp = performance.now(); - } - #vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); - #resetGamepad = () => { - const gamepad = this.#getVirtualGamepad(); - gamepad.axes = [0, 0, 0, 0]; - for (let button of gamepad.buttons) - button.pressed = !1, button.value = 0; - gamepad.timestamp = performance.now(); - }; - #pressButton = (buttonIndex, pressed) => { - const virtualGamepad = this.#getVirtualGamepad(); - if (buttonIndex >= 100) { - let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]; - valueArr = valueArr, axisIndex = axisIndex; - for (let i = valueArr.length - 1;i >= 0; i--) - if (valueArr[i] === buttonIndex) valueArr.splice(i, 1); - pressed && valueArr.push(buttonIndex); - let value; - if (valueArr.length) value = this.#STICK_MAP[valueArr[valueArr.length - 1]][2]; - else value = 0; - virtualGamepad.axes[axisIndex] = value; - } else virtualGamepad.buttons[buttonIndex].pressed = pressed, virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0; - virtualGamepad.timestamp = performance.now(); - }; - #onKeyboardEvent = (e) => { - const isKeyDown = e.type === "keydown"; - if (e.code === "F8") { - if (!isKeyDown) e.preventDefault(), this.toggle(); - return; - } - if (e.code === "Escape") { - if (e.preventDefault(), this.#enabled && isKeyDown) { - if (this.#escKeyDownTime === -1) this.#escKeyDownTime = performance.now(); - else if (performance.now() - this.#escKeyDownTime >= 1000) this.stop(); - } else this.#escKeyDownTime = -1; - return; - } - if (!this.#isPolling) return; - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]; - if (typeof buttonIndex === "undefined") return; - if (e.repeat) return; - e.preventDefault(), this.#pressButton(buttonIndex, isKeyDown); - }; - #onMouseStopped = () => { - this.#detectMouseStoppedTimeout = null; - const analog = this.#CURRENT_PRESET_DATA.mouse["map_to"] === 1 ? 0 : 1; - this.#updateStick(analog, 0, 0); - }; - handleMouseClick = (data) => { - let mouseButton; - if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton; - else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton]; - const keyCode = "Mouse" + mouseButton, key = { - code: keyCode, - name: KeyHelper.codeToKeyName(keyCode) - }; - if (!key.name) return; - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; - if (typeof buttonIndex === "undefined") return; - this.#pressButton(buttonIndex, data.pressed); - }; - handleMouseMove = (data) => { - const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse["map_to"]; - if (mouseMapTo === 0) return; - this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout), this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50); - const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse["deadzone_counterweight"]; - let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse["sensitivity_x"], y = data.movementY * this.#CURRENT_PRESET_DATA.mouse["sensitivity_y"], length = this.#vectorLength(x, y); - if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; - else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length, y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length; - const analog = mouseMapTo === 1 ? 0 : 1; - this.#updateStick(analog, x, y); - }; - handleMouseWheel = (data) => { - let code = ""; - if (data.vertical < 0) code = "ScrollUp"; - else if (data.vertical > 0) code = "ScrollDown"; - else if (data.horizontal < 0) code = "ScrollLeft"; - else if (data.horizontal > 0) code = "ScrollRight"; - if (!code) return !1; - const key = { - code, - name: KeyHelper.codeToKeyName(code) - }, buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; - if (typeof buttonIndex === "undefined") return !1; - if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout), this.#pressButton(buttonIndex, !0); - return this.#wheelStoppedTimeout = window.setTimeout(() => { - this.#prevWheelCode = null, this.#pressButton(buttonIndex, !1); - }, 20), !0; - }; - toggle = (force) => { - if (typeof force !== "undefined") this.#enabled = force; - else this.#enabled = !this.#enabled; - if (this.#enabled) document.body.requestPointerLock(); - else document.pointerLockElement && document.exitPointerLock(); - }; - #getCurrentPreset = () => { - return new Promise((resolve) => { - const presetId = getPref("mkb_default_preset_id"); - LocalDb.INSTANCE.getPreset(presetId).then((preset) => { - resolve(preset); - }); - }); - }; - refreshPresetData = () => { - this.#getCurrentPreset().then((preset) => { - this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET), this.#resetGamepad(); - }); - }; - waitForMouseData = (wait) => { - this.#$message && this.#$message.classList.toggle("bx-gone", !wait); - }; - #onPollingModeChanged = (e) => { - if (!this.#$message) return; - if (e.mode === "none") this.#$message.classList.remove("bx-offscreen"); - else this.#$message.classList.add("bx-offscreen"); - }; - #onDialogShown = () => { - document.pointerLockElement && document.exitPointerLock(); - }; - #initMessage = () => { - if (!this.#$message) this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, CE("div", {}, CE("p", {}, t("virtual-controller")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "virtual" }, createButton({ - style: 1 | 256 | 64, - label: t("activate"), - onClick: ((e) => { - e.preventDefault(), e.stopPropagation(), this.toggle(!0); - }).bind(this) - }), CE("div", {}, createButton({ - label: t("ignore"), - style: 4, - onClick: (e) => { - e.preventDefault(), e.stopPropagation(), this.toggle(!1), this.waitForMouseData(!1); - } - }), createButton({ - label: t("edit"), - onClick: (e) => { - e.preventDefault(), e.stopPropagation(); - const dialog = SettingsNavigationDialog.getInstance(); - dialog.focusTab("mkb"), NavigationDialogManager.getInstance().show(dialog); - } - })))); - if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); - }; - #onPointerLockChange = () => { - if (document.pointerLockElement) this.start(); - else this.stop(); - }; - #onPointerLockError = (e) => { - console.log(e), this.stop(); - }; - #onPointerLockRequested = () => { - this.start(); - }; - #onPointerLockExited = () => { - this.#mouseDataProvider?.stop(); - }; - handleEvent(event) { - switch (event.type) { - case BxEvent.POINTER_LOCK_REQUESTED: - this.#onPointerLockRequested(); - break; - case BxEvent.POINTER_LOCK_EXITED: - this.#onPointerLockExited(); - break; - } - } - init = () => { - if (this.refreshPresetData(), this.#enabled = !1, AppInterface) this.#mouseDataProvider = new WebSocketMouseDataProvider(this); - else this.#mouseDataProvider = new PointerLockMouseDataProvider(this); - if (this.#mouseDataProvider.init(), window.addEventListener("keydown", this.#onKeyboardEvent), window.addEventListener("keyup", this.#onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this); - else document.addEventListener("pointerlockchange", this.#onPointerLockChange), document.addEventListener("pointerlockerror", this.#onPointerLockError); - if (this.#initMessage(), this.#$message?.classList.add("bx-gone"), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("virtual-controller"), { html: !0 }), this.waitForMouseData(!1); - else this.waitForMouseData(!0); - }; - destroy = () => { - if (this.#isPolling = !1, this.#enabled = !1, this.stop(), this.waitForMouseData(!1), document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("keydown", this.#onKeyboardEvent), window.removeEventListener("keyup", this.#onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this); - else document.removeEventListener("pointerlockchange", this.#onPointerLockChange), document.removeEventListener("pointerlockerror", this.#onPointerLockError); - window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), this.#mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged); - }; - start = () => { - if (!this.#enabled) this.#enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); - this.#isPolling = !0, this.#escKeyDownTime = -1, this.#resetGamepad(), window.navigator.getGamepads = this.#patchedGetGamepads, this.waitForMouseData(!1), this.#mouseDataProvider?.start(); - const virtualGamepad = this.#getVirtualGamepad(); - virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", { - gamepad: virtualGamepad - }), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); - }; - stop = () => { - this.#enabled = !1, this.#isPolling = !1, this.#escKeyDownTime = -1; - const virtualGamepad = this.#getVirtualGamepad(); - if (virtualGamepad.connected) this.#resetGamepad(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", { - gamepad: virtualGamepad - }), window.navigator.getGamepads = this.#nativeGetGamepads; - this.waitForMouseData(!0), this.#mouseDataProvider?.stop(); - }; - static setupEvents() { - window.addEventListener(BxEvent.STREAM_PLAYING, () => { - if (STATES.currentStream.titleInfo?.details.hasMkbSupport) { - if (AppInterface && getPref("native_mkb_enabled") === "on") AppInterface && NativeMkbHandler.getInstance().init(); - } else if (getPref("mkb_enabled") && (AppInterface || !UserAgent.isMobile())) BxLogger.info(LOG_TAG4, "Emulate MKB"), EmulatedMkbHandler.getInstance().init(); - }); - } -} -class MicrophoneShortcut { - static toggle(showToast = !0) { - if (!window.BX_EXPOSED.streamSession) return !1; - const enableMic = window.BX_EXPOSED.streamSession._microphoneState === "Enabled" ? !1 : !0; - try { - return window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic), showToast && Toast.show(t("microphone"), t(enableMic ? "unmuted" : "muted"), { instant: !0 }), enableMic; - } catch (e) { - console.log(e); - } - return !1; - } -} -class StreamUiShortcut { - static showHideStreamMenu() { - window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu(); - } -} class ControllerShortcut { static #STORAGE_KEY = "better_xcloud_controller_shortcuts"; static #buttonsCache = {}; @@ -5345,7 +5344,7 @@ class ControllerShortcut { let hasGamepad = !1; for (let gamepad of gamepads) { if (!gamepad || !gamepad.connected) continue; - if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) continue; + if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; hasGamepad = !0; const $option = CE("option", { value: gamepad.id }, gamepad.id); $fragment.appendChild($option); @@ -5459,7 +5458,7 @@ class ControllerShortcut { } var BxExposed = { getTitleInfo: () => STATES.currentStream.titleInfo, - modifyTitleInfo: (titleInfo) => { + modifyTitleInfo: function(titleInfo) { titleInfo = deepClone(titleInfo); let supportedInputTypes = titleInfo.details.supportedInputTypes; if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) supportedInputTypes.push("MKB"); @@ -6583,7 +6582,7 @@ function interceptHttpRequests() { }; } function showGamepadToast(gamepad) { - if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) return; + if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; BxLogger.info("Gamepad", gamepad); let text = "🎮"; if (getPref("local_co_op_enabled")) text += ` #${gamepad.index + 1}`; diff --git a/src/modules/controller-shortcut.ts b/src/modules/controller-shortcut.ts index d1a98e4..b42d481 100644 --- a/src/modules/controller-shortcut.ts +++ b/src/modules/controller-shortcut.ts @@ -3,7 +3,6 @@ import { GamepadKey } from "@enums/mkb"; import { PrompFont } from "@enums/prompt-font"; import { CE, removeChildElements } from "@utils/html"; import { t } from "@utils/translation"; -import { EmulatedMkbHandler } from "./mkb/mkb-handler"; import { StreamStats } from "./stream/stream-stats"; import { MicrophoneShortcut } from "./shortcuts/shortcut-microphone"; import { StreamUiShortcut } from "./shortcuts/shortcut-stream-ui"; @@ -15,6 +14,7 @@ import { setNearby } from "@/utils/navigation-utils"; import { PrefKey } from "@/enums/pref-keys"; import { getPref } from "@/utils/settings-storages/global-settings-storage"; import { SettingsNavigationDialog } from "./ui/dialog/settings-dialog"; +import { VIRTUAL_GAMEPAD_ID } from "./mkb/mkb-handler"; const enum ShortcutAction { BETTER_XCLOUD_SETTINGS_SHOW = 'bx-settings-show', @@ -185,7 +185,7 @@ export class ControllerShortcut { } // Ignore emulated gamepad - if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) { + if (gamepad.id === VIRTUAL_GAMEPAD_ID) { continue; } diff --git a/src/modules/mkb/mkb-handler.ts b/src/modules/mkb/mkb-handler.ts index 22b6d99..84ef697 100644 --- a/src/modules/mkb/mkb-handler.ts +++ b/src/modules/mkb/mkb-handler.ts @@ -26,6 +26,7 @@ const PointerToMouseButton = { 4: 1, } +export const VIRTUAL_GAMEPAD_ID = 'Xbox 360 Controller'; class WebSocketMouseDataProvider extends MouseDataProvider { #pointerClient: PointerClient | undefined @@ -136,10 +137,8 @@ export class EmulatedMkbHandler extends MkbHandler { static readonly DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; static readonly MAXIMUM_STICK_RANGE = 1.1; - static VIRTUAL_GAMEPAD_ID = 'Xbox 360 Controller'; - #VIRTUAL_GAMEPAD = { - id: EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID, + id: VIRTUAL_GAMEPAD_ID, index: 3, connected: false, hapticActuators: null, diff --git a/src/modules/ui/dialog/navigation-dialog.ts b/src/modules/ui/dialog/navigation-dialog.ts index 788029b..d1b2c5c 100644 --- a/src/modules/ui/dialog/navigation-dialog.ts +++ b/src/modules/ui/dialog/navigation-dialog.ts @@ -1,6 +1,6 @@ import { GamepadKey } from "@/enums/mkb"; import { PrefKey } from "@/enums/pref-keys"; -import { EmulatedMkbHandler } from "@/modules/mkb/mkb-handler"; +import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler"; import { BxEvent } from "@/utils/bx-event"; import { STATES } from "@/utils/global"; import { CE, isElementVisible } from "@/utils/html"; @@ -263,7 +263,7 @@ export class NavigationDialogManager { } // Ignore virtual controller - if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) { + if (gamepad.id === VIRTUAL_GAMEPAD_ID) { continue; } diff --git a/src/modules/ui/dialog/settings-dialog.ts b/src/modules/ui/dialog/settings-dialog.ts index ea42fc0..355593e 100644 --- a/src/modules/ui/dialog/settings-dialog.ts +++ b/src/modules/ui/dialog/settings-dialog.ts @@ -509,7 +509,7 @@ export class SettingsNavigationDialog extends NavigationDialog { group: 'mkb', label: t('virtual-controller'), helpUrl: 'https://better-xcloud.github.io/mouse-and-keyboard/', - content: MkbRemapper.INSTANCE.render(), + content: isFullVersion() && MkbRemapper.INSTANCE.render(), }]; private readonly TAB_NATIVE_MKB_ITEMS: Array = [{ diff --git a/src/utils/gamepad.ts b/src/utils/gamepad.ts index 561da74..dd4c2ef 100644 --- a/src/utils/gamepad.ts +++ b/src/utils/gamepad.ts @@ -1,4 +1,4 @@ -import { EmulatedMkbHandler } from "@modules/mkb/mkb-handler"; +import { VIRTUAL_GAMEPAD_ID } from "@modules/mkb/mkb-handler"; import { t } from "@utils/translation"; import { Toast } from "@utils/toast"; import { BxLogger } from "@utils/bx-logger"; @@ -8,7 +8,7 @@ import { getPref } from "./settings-storages/global-settings-storage"; // Show a toast when connecting/disconecting controller export function showGamepadToast(gamepad: Gamepad) { // Don't show Toast for virtual controller - if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) { + if (gamepad.id === VIRTUAL_GAMEPAD_ID) { return; } diff --git a/src/utils/xhome-interceptor.ts b/src/utils/xhome-interceptor.ts index 385a9ca..c39073a 100644 --- a/src/utils/xhome-interceptor.ts +++ b/src/utils/xhome-interceptor.ts @@ -1,5 +1,3 @@ -import { isFullVersion } from "@macros/build" with {type: "macro"}; - import { TouchController } from "@/modules/touch-controller"; import { BxEvent } from "./bx-event"; import { SupportedInputType } from "./bx-exposed";