From 07c1757237b0f7292839092d392172714fde0c0b Mon Sep 17 00:00:00 2001
From: redphx <96280+redphx@users.noreply.github.com>
Date: Fri, 24 May 2024 18:10:38 +0700
Subject: [PATCH] Squashed commit of the following:
commit 2faed50e5c2165647e389d794de673038d56241e
Author: redphx <96280+redphx@users.noreply.github.com>
Date: Fri May 24 18:09:25 2024 +0700
Make shortcuts work with controller
commit b8f6c503ba7969de3a232644d3f6b53532a4b7bb
Author: redphx <96280+redphx@users.noreply.github.com>
Date: Fri May 24 17:01:15 2024 +0700
Update translations
commit 6f6c0899e5a09cd5534e06a9e272bf78c74536dc
Author: redphx <96280+redphx@users.noreply.github.com>
Date: Fri May 24 17:00:50 2024 +0700
Preload PrompFont
commit 1bf0f2b9dae77890d35091bed970b942c4d61fbc
Author: redphx <96280+redphx@users.noreply.github.com>
Date: Fri May 24 07:08:05 2024 +0700
Render Controller shortcuts settings
commit 2f24965c73a941be2ebc8a3509dc540a47b4e38d
Author: redphx <96280+redphx@users.noreply.github.com>
Date: Thu May 23 17:21:55 2024 +0700
Fix not able to capture screenshot after switching games
commit 6ac791e2dfb17215ee82d449047d0cd11d185c42
Author: redphx <96280+redphx@users.noreply.github.com>
Date: Thu May 23 17:11:19 2024 +0700
Hijack the Home button
---
src/assets/css/stream-settings.styl | 26 ++
src/assets/svg/command.svg | 4 +
src/modules/controller-shortcut.ts | 321 +++++++++++++++++++
src/modules/game-bar/action-microphone.ts | 11 +-
src/modules/mkb/definitions.ts | 52 +--
src/modules/patcher.ts | 12 +-
src/modules/patches/controller-shortcuts.js | 82 +++++
src/modules/shortcuts/shortcut-microphone.ts | 24 ++
src/modules/shortcuts/shortcut-stream-ui.ts | 6 +
src/modules/stream/stream-ui.ts | 4 +-
src/modules/ui/ui.ts | 30 +-
src/types/index.d.ts | 2 -
src/utils/bx-exposed.ts | 6 +-
src/utils/bx-icon.ts | 2 +
src/utils/prompt-font.ts | 32 ++
src/utils/screenshot.ts | 29 +-
src/utils/translation.ts | 21 +-
17 files changed, 606 insertions(+), 58 deletions(-)
create mode 100644 src/assets/svg/command.svg
create mode 100644 src/modules/controller-shortcut.ts
create mode 100644 src/modules/shortcuts/shortcut-microphone.ts
create mode 100644 src/modules/shortcuts/shortcut-stream-ui.ts
create mode 100644 src/utils/prompt-font.ts
diff --git a/src/assets/css/stream-settings.styl b/src/assets/css/stream-settings.styl
index b42b355..0d87214 100644
--- a/src/assets/css/stream-settings.styl
+++ b/src/assets/css/stream-settings.styl
@@ -124,3 +124,29 @@
font-style: italic;
padding-top: 16px;
}
+
+.bx-quick-settings-tab-contents {
+ div[data-group="shortcuts"] {
+ .bx-shortcut-profile {
+ width: 100%;
+ height: 36px;
+ display: block;
+ margin-bottom: 10px;
+ }
+
+ .bx-shortcut-row {
+ display: flex;
+ margin-bottom: 10px;
+
+ label.bx-prompt {
+ flex: 1;
+ font-family: var(--bx-promptfont-font);
+ font-size: 26px;
+ }
+
+ select {
+ flex: 2;
+ }
+ }
+ }
+}
diff --git a/src/assets/svg/command.svg b/src/assets/svg/command.svg
new file mode 100644
index 0000000..c14a01a
--- /dev/null
+++ b/src/assets/svg/command.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/modules/controller-shortcut.ts b/src/modules/controller-shortcut.ts
new file mode 100644
index 0000000..aafdab5
--- /dev/null
+++ b/src/modules/controller-shortcut.ts
@@ -0,0 +1,321 @@
+import { Screenshot } from "@utils/screenshot";
+import { GamepadKey } from "./mkb/definitions";
+import { PrompFont } from "@utils/prompt-font";
+import { CE } from "@utils/html";
+import { t } from "@utils/translation";
+import { PrefKey, getPref } from "@/utils/preferences";
+import { AppInterface } from "@/utils/global";
+import { MkbHandler } from "./mkb/mkb-handler";
+import { StreamStat, StreamStats } from "./stream/stream-stats";
+import { MicrophoneShortcut } from "./shortcuts/shortcut-microphone";
+import { StreamUiShortcut } from "./shortcuts/shortcut-stream-ui";
+
+enum ShortcutAction {
+ STREAM_SCREENSHOT_CAPTURE = 'stream-screenshot-capture',
+
+ STREAM_MENU_TOGGLE = 'stream-menu-toggle',
+ STREAM_STATS_TOGGLE = 'stream-stats-toggle',
+ STREAM_SOUND_TOGGLE = 'stream-sound-toggle',
+ STREAM_MICROPHONE_TOGGLE = 'stream-microphone-toggle',
+
+ STREAM_VOLUME_INC = 'stream-volume-inc',
+ STREAM_VOLUME_DEC = 'stream-volume-dec',
+
+ DEVICE_VOLUME_INC = 'device-volume-inc',
+ DEVICE_VOLUME_DEC = 'device-volume-dec',
+
+ SCREEN_BRIGHTNESS_INC = 'screen-brightness-inc',
+ SCREEN_BRIGHTNESS_DEC = 'screen-brightness-dec',
+}
+
+export class ControllerShortcut {
+ static readonly #STORAGE_KEY = 'better_xcloud_controller_shortcuts';
+
+ static #buttonsCache: {[key: string]: boolean[]} = {};
+ static #buttonsStatus: {[key: string]: boolean[]} = {};
+
+ static #$selectProfile: HTMLSelectElement;
+ static #$selectActions: Partial<{[key in GamepadKey]: HTMLSelectElement}> = {};
+ static #$remap: HTMLElement;
+
+ static #ACTIONS: {[key: string]: (ShortcutAction | null)[]} = {};
+
+ static reset(index: number) {
+ ControllerShortcut.#buttonsCache[index] = [];
+ ControllerShortcut.#buttonsStatus[index] = [];
+ }
+
+ static handle(gamepad: Gamepad): boolean {
+ const gamepadIndex = gamepad.index;
+ const actions = ControllerShortcut.#ACTIONS[gamepad.id];
+ if (!actions) {
+ return false;
+ }
+
+ // Move the buttons status from the previous frame to the cache
+ ControllerShortcut.#buttonsCache[gamepadIndex] = ControllerShortcut.#buttonsStatus[gamepadIndex].slice(0);
+ // Clear the buttons status
+ ControllerShortcut.#buttonsStatus[gamepadIndex] = [];
+
+ const pressed: boolean[] = [];
+ let otherButtonPressed = false;
+
+ gamepad.buttons.forEach((button, index) => {
+ // Only add the newly pressed button to the array (holding doesn't count)
+ if (button.pressed && index !== GamepadKey.HOME) {
+ otherButtonPressed = true;
+ pressed[index] = true;
+
+ // If this is newly pressed button > run action
+ if (actions[index] && !ControllerShortcut.#buttonsCache[gamepadIndex][index]) {
+ ControllerShortcut.#runAction(actions[index]!);
+ }
+ }
+ });
+
+ ControllerShortcut.#buttonsStatus[gamepadIndex] = pressed;
+ return otherButtonPressed;
+ }
+
+ static #runAction(action: ShortcutAction) {
+ switch (action) {
+ case ShortcutAction.STREAM_SCREENSHOT_CAPTURE:
+ Screenshot.takeScreenshot();
+ break;
+
+ case ShortcutAction.STREAM_STATS_TOGGLE:
+ StreamStats.toggle();
+ break;
+
+ case ShortcutAction.STREAM_MICROPHONE_TOGGLE:
+ MicrophoneShortcut.toggle();
+ break;
+
+ case ShortcutAction.STREAM_MENU_TOGGLE:
+ StreamUiShortcut.showHideStreamMenu();
+ break;
+ }
+ }
+
+ static #updateAction(profile: string, button: GamepadKey, action: ShortcutAction | null) {
+ if (!(profile in ControllerShortcut.#ACTIONS)) {
+ ControllerShortcut.#ACTIONS[profile] = [];
+ }
+
+ if (!action) {
+ action = null;
+ }
+
+ ControllerShortcut.#ACTIONS[profile][button] = action;
+
+ // Remove empty profiles
+ for (const key in ControllerShortcut.#ACTIONS) {
+ let empty = true;
+ for (const value of ControllerShortcut.#ACTIONS[key]) {
+ if (!!value) {
+ empty = false;
+ break;
+ }
+ }
+
+ if (empty) {
+ delete ControllerShortcut.#ACTIONS[key];
+ }
+ }
+
+ // Save to storage
+ window.localStorage.setItem(ControllerShortcut.#STORAGE_KEY, JSON.stringify(ControllerShortcut.#ACTIONS));
+
+ console.log(ControllerShortcut.#ACTIONS);
+ }
+
+ static #updateProfileList(e?: GamepadEvent) {
+ const $select = ControllerShortcut.#$selectProfile;
+ const $remap = ControllerShortcut.#$remap;
+
+ const $fragment = document.createDocumentFragment();
+
+ // Remove old profiles
+ while ($select.firstElementChild) {
+ $select.firstElementChild.remove();
+ }
+
+ const gamepads = navigator.getGamepads();
+ let hasGamepad = false;
+
+ for (const gamepad of gamepads) {
+ if (!gamepad || !gamepad.connected) {
+ continue;
+ }
+
+ // Ignore emulated gamepad
+ if (gamepad.id === MkbHandler.VIRTUAL_GAMEPAD_ID) {
+ continue;
+ }
+
+ hasGamepad = true;
+
+ const $option = CE('option', {value: gamepad.id}, gamepad.id);
+ $fragment.appendChild($option);
+ }
+
+ if (hasGamepad) {
+ $select.appendChild($fragment);
+
+ $remap.classList.remove('bx-gone');
+
+ $select.disabled = false;
+ $select.selectedIndex = 0;
+ $select.dispatchEvent(new Event('change'));
+ } else {
+ $remap.classList.add('bx-gone');
+
+ $select.disabled = true;
+ const $option = CE('option', {}, '---');
+ $fragment.appendChild($option);
+
+ $select.appendChild($fragment);
+ }
+ }
+
+ static #switchProfile(profile: string) {
+ let actions = ControllerShortcut.#ACTIONS[profile];
+ if (!actions) {
+ actions = [];
+ }
+
+ // Reset selects' values
+ let button: any;
+ for (button in ControllerShortcut.#$selectActions) {
+ const $select = ControllerShortcut.#$selectActions[button as GamepadKey]!;
+ $select.value = actions[button] || '';
+ }
+ }
+
+ static renderSettings() {
+ // Read actions from localStorage
+ ControllerShortcut.#ACTIONS = JSON.parse(window.localStorage.getItem(ControllerShortcut.#STORAGE_KEY) || '{}');
+
+ const buttons = {
+ [GamepadKey.A]: PrompFont.A,
+ [GamepadKey.B]: PrompFont.B,
+ [GamepadKey.X]: PrompFont.X,
+ [GamepadKey.Y]: PrompFont.Y,
+
+ [GamepadKey.LB]: PrompFont.LB,
+ [GamepadKey.RB]: PrompFont.RB,
+
+ [GamepadKey.LT]: PrompFont.LT,
+ [GamepadKey.RT]: PrompFont.RT,
+
+ [GamepadKey.SELECT]: PrompFont.SELECT,
+ [GamepadKey.START]: PrompFont.START,
+
+ [GamepadKey.UP]: PrompFont.UP,
+ [GamepadKey.DOWN]: PrompFont.DOWN,
+ [GamepadKey.LEFT]: PrompFont.LEFT,
+ [GamepadKey.RIGHT]: PrompFont.RIGHT,
+ };
+
+ const actions: {[key: string]: Partial<{[key in ShortcutAction]: string | string[]}>} = {
+ /*
+ [t('device')]: AppInterface && {
+ [ShortcutAction.DEVICE_VOLUME_INC]: [t('device'), t('volume'), t('increase')],
+ [ShortcutAction.DEVICE_VOLUME_DEC]: [t('device'), t('volume'), t('decrease')],
+
+ [ShortcutAction.SCREEN_BRIGHTNESS_INC]: [t('screen'), t('brightness'), t('increase')],
+ [ShortcutAction.SCREEN_BRIGHTNESS_DEC]: [t('screen'), t('brightness'), t('decrease')],
+ },
+ */
+
+ [t('stream')]: {
+ [ShortcutAction.STREAM_SCREENSHOT_CAPTURE]: [t('stream'), t('take-screenshot')],
+ [ShortcutAction.STREAM_STATS_TOGGLE]: [t('stream'), t('stats'), t('show-hide')],
+ [ShortcutAction.STREAM_MICROPHONE_TOGGLE]: [t('stream'), t('microphone'), t('toggle')],
+ [ShortcutAction.STREAM_MENU_TOGGLE]: [t('stream'), t('menu'), t('show')],
+ // [ShortcutAction.STREAM_SOUND_TOGGLE]: [t('stream'), t('sound'), t('toggle')],
+ // [ShortcutAction.STREAM_VOLUME_INC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('stream'), t('volume'), t('increase')],
+ // [ShortcutAction.STREAM_VOLUME_DEC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('stream'), t('volume'), t('decrease')],
+ }
+ };
+
+ const $baseSelect = CE('select', {autocomplete: 'off'}, CE('option', {value: ''}, '---'));
+ for (const groupLabel in actions) {
+ const items = actions[groupLabel];
+ if (!items) {
+ continue;
+ }
+
+ const $optGroup = CE('optgroup', {'label': groupLabel});
+
+ for (const action in items) {
+ let label = items[action as keyof typeof items];
+ if (!label) {
+ continue;
+ }
+
+ if (Array.isArray(label)) {
+ label = label.join(' > ');
+ }
+
+ const $option = CE('option', {value: action}, label);
+ $optGroup.appendChild($option);
+ }
+
+ $baseSelect.appendChild($optGroup);
+ }
+
+ const $container = CE('div', {});
+
+ const $profile = CE('select', {'class': 'bx-shortcut-profile', autocomplete: 'off'});
+ $profile.addEventListener('change', e => {
+ ControllerShortcut.#switchProfile($profile.value);
+ });
+
+ $container.appendChild($profile);
+
+ const onActionChanged = (e: Event) => {
+ const $target = e.target as HTMLSelectElement;
+
+ const profile = $profile.value;
+ const button: unknown = $target.dataset.button;
+ const action = $target.value as ShortcutAction;
+
+ ControllerShortcut.#updateAction(profile, button as GamepadKey, action);
+ };
+
+ const $remap = CE('div', {'class': 'bx-gone'});
+ let button: keyof typeof buttons;
+ // @ts-ignore
+ for (button in buttons) {
+ const $row = CE('div', {'class': 'bx-shortcut-row'});
+
+ const prompt = buttons[button];
+ const $label = CE('label', {'class': 'bx-prompt'}, `${PrompFont.HOME} + ${prompt}`);
+
+ const $select = $baseSelect.cloneNode(true) as HTMLSelectElement;
+ $select.dataset.button = button.toString();
+ $select.addEventListener('change', onActionChanged);
+
+ ControllerShortcut.#$selectActions[button] = $select;
+
+ $row.appendChild($label);
+ $row.appendChild($select);
+
+ $remap.appendChild($row);
+ }
+
+ $container.appendChild($remap);
+
+ ControllerShortcut.#$selectProfile = $profile;
+ ControllerShortcut.#$remap = $remap;
+
+ // Detect when gamepad connected/disconnect
+ window.addEventListener('gamepadconnected', ControllerShortcut.#updateProfileList);
+ window.addEventListener('gamepaddisconnected', ControllerShortcut.#updateProfileList);
+
+ ControllerShortcut.#updateProfileList();
+
+ return $container;
+ }
+}
diff --git a/src/modules/game-bar/action-microphone.ts b/src/modules/game-bar/action-microphone.ts
index b3fe35e..4ebbfd7 100644
--- a/src/modules/game-bar/action-microphone.ts
+++ b/src/modules/game-bar/action-microphone.ts
@@ -3,6 +3,7 @@ import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle, CE } from "@utils/html";
import { t } from "@utils/translation";
import { BaseGameBarAction } from "./action-base";
+import { MicrophoneShortcut } from "../shortcuts/shortcut-microphone";
enum MicrophoneState {
REQUESTED = 'Requested',
@@ -22,15 +23,9 @@ export class MicrophoneAction extends BaseGameBarAction {
const onClick = (e: Event) => {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
- const state = this.$content.getAttribute('data-enabled');
- const enableMic = state === 'true' ? false : true;
- try {
- window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic);
- this.$content.setAttribute('data-enabled', enableMic.toString());
- } catch (e) {
- console.log(e);
- }
+ const enabled = MicrophoneShortcut.toggle(false);
+ this.$content.setAttribute('data-enabled', enabled.toString());
};
const $btnDefault = createButton({
diff --git a/src/modules/mkb/definitions.ts b/src/modules/mkb/definitions.ts
index d4bf87a..181f4f6 100644
--- a/src/modules/mkb/definitions.ts
+++ b/src/modules/mkb/definitions.ts
@@ -1,4 +1,5 @@
import type { GamepadKeyNameType } from "@/types/mkb";
+import { PrompFont } from "@/utils/prompt-font";
export enum GamepadKey {
A = 0,
@@ -18,6 +19,7 @@ export enum GamepadKey {
LEFT = 14,
RIGHT = 15,
HOME = 16,
+ SHARE = 17,
LS_UP = 100,
LS_DOWN = 101,
@@ -32,36 +34,36 @@ export enum GamepadKey {
export const GamepadKeyName: GamepadKeyNameType = {
- [GamepadKey.A]: ['A', '⇓'],
- [GamepadKey.B]: ['B', '⇒'],
- [GamepadKey.X]: ['X', '⇐'],
- [GamepadKey.Y]: ['Y', '⇑'],
+ [GamepadKey.A]: ['A', PrompFont.A],
+ [GamepadKey.B]: ['B', PrompFont.B],
+ [GamepadKey.X]: ['X', PrompFont.X],
+ [GamepadKey.Y]: ['Y', PrompFont.Y],
- [GamepadKey.LB]: ['LB', '↘'],
- [GamepadKey.RB]: ['RB', '↙'],
- [GamepadKey.LT]: ['LT', '↖'],
- [GamepadKey.RT]: ['RT', '↗'],
+ [GamepadKey.LB]: ['LB', PrompFont.LB],
+ [GamepadKey.RB]: ['RB', PrompFont.RB],
+ [GamepadKey.LT]: ['LT', PrompFont.LT],
+ [GamepadKey.RT]: ['RT', PrompFont.RT],
- [GamepadKey.SELECT]: ['Select', '⇺'],
- [GamepadKey.START]: ['Start', '⇻'],
- [GamepadKey.HOME]: ['Home', ''],
+ [GamepadKey.SELECT]: ['Select', PrompFont.SELECT],
+ [GamepadKey.START]: ['Start', PrompFont.START],
+ [GamepadKey.HOME]: ['Home', PrompFont.HOME],
- [GamepadKey.UP]: ['D-Pad Up', '≻'],
- [GamepadKey.DOWN]: ['D-Pad Down', '≽'],
- [GamepadKey.LEFT]: ['D-Pad Left', '≺'],
- [GamepadKey.RIGHT]: ['D-Pad Right', '≼'],
+ [GamepadKey.UP]: ['D-Pad Up', PrompFont.UP],
+ [GamepadKey.DOWN]: ['D-Pad Down', PrompFont.DOWN],
+ [GamepadKey.LEFT]: ['D-Pad Left', PrompFont.LEFT],
+ [GamepadKey.RIGHT]: ['D-Pad Right', PrompFont.RIGHT],
- [GamepadKey.L3]: ['L3', '↺'],
- [GamepadKey.LS_UP]: ['Left Stick Up', '↾'],
- [GamepadKey.LS_DOWN]: ['Left Stick Down', '⇂'],
- [GamepadKey.LS_LEFT]: ['Left Stick Left', '↼'],
- [GamepadKey.LS_RIGHT]: ['Left Stick Right', '⇀'],
+ [GamepadKey.L3]: ['L3', PrompFont.L3],
+ [GamepadKey.LS_UP]: ['Left Stick Up', PrompFont.LS_UP],
+ [GamepadKey.LS_DOWN]: ['Left Stick Down', PrompFont.LS_DOWN],
+ [GamepadKey.LS_LEFT]: ['Left Stick Left', PrompFont.LS_LEFT],
+ [GamepadKey.LS_RIGHT]: ['Left Stick Right', PrompFont.LS_RIGHT],
- [GamepadKey.R3]: ['R3', '↻'],
- [GamepadKey.RS_UP]: ['Right Stick Up', '↿'],
- [GamepadKey.RS_DOWN]: ['Right Stick Down', '⇃'],
- [GamepadKey.RS_LEFT]: ['Right Stick Left', '↽'],
- [GamepadKey.RS_RIGHT]: ['Right Stick Right', '⇁'],
+ [GamepadKey.R3]: ['R3', PrompFont.R3],
+ [GamepadKey.RS_UP]: ['Right Stick Up', PrompFont.RS_UP],
+ [GamepadKey.RS_DOWN]: ['Right Stick Down', PrompFont.RS_DOWN],
+ [GamepadKey.RS_LEFT]: ['Right Stick Left', PrompFont.RS_LEFT],
+ [GamepadKey.RS_RIGHT]: ['Right Stick Right', PrompFont.RS_RIGHT],
};
diff --git a/src/modules/patcher.ts b/src/modules/patcher.ts
index d50eb2e..0dc46e8 100644
--- a/src/modules/patcher.ts
+++ b/src/modules/patcher.ts
@@ -384,13 +384,19 @@ if (!!window.BX_REMOTE_PLAY_CONFIG) {
return false;
}
- // Restore the "..." button
- str = str.replace(text, 'e.guideUI = null;' + text);
+ let newCode = `
+// Expose onShowStreamMenu
+window.BX_EXPOSED.showStreamMenu = e.onShowStreamMenu;
+// Restore the "..." button
+e.guideUI = null;
+`;
// Remove the TAK Edit button when the touch controller is disabled
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off') {
- str = str.replace(text, 'e.canShowTakHUD = false;' + text);
+ newCode += 'e.canShowTakHUD = false;';
}
+
+ str = str.replace(text, newCode + text);
return str;
},
diff --git a/src/modules/patches/controller-shortcuts.js b/src/modules/patches/controller-shortcuts.js
index c8da989..f030e0a 100644
--- a/src/modules/patches/controller-shortcuts.js
+++ b/src/modules/patches/controller-shortcuts.js
@@ -1,5 +1,87 @@
const currentGamepad = ${gamepadVar};
+// Share button on XS controller
if (currentGamepad.buttons[17] && currentGamepad.buttons[17].value === 1) {
window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));
}
+
+const btnHome = currentGamepad.buttons[16];
+if (btnHome) {
+ if (!this.bxHomeStates) {
+ this.bxHomeStates = {};
+ }
+
+ if (btnHome.pressed) {
+ this.gamepadIsIdle.set(currentGamepad.index, false);
+
+ if (this.bxHomeStates[currentGamepad.index]) {
+ const lastTimestamp = this.bxHomeStates[currentGamepad.index].timestamp;
+
+ if (currentGamepad.timestamp !== lastTimestamp) {
+ this.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp;
+
+ const handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad);
+ if (handled) {
+ this.bxHomeStates[currentGamepad.index].shortcutPressed += 1;
+ }
+ }
+ } else {
+ // First time pressing > save current timestamp
+ window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index);
+ this.bxHomeStates[currentGamepad.index] = {
+ shortcutPressed: 0,
+ timestamp: currentGamepad.timestamp,
+ };
+ }
+
+ // Listen to next button press
+ const intervalMs = 50;
+ this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs);
+
+ // Hijack this button
+ return;
+ } else if (this.bxHomeStates[currentGamepad.index]) {
+ const info = structuredClone(this.bxHomeStates[currentGamepad.index]);
+
+ // Home button released
+ this.bxHomeStates[currentGamepad.index] = null;
+
+ if (info.shortcutPressed === 0) {
+ const fakeGamepadMappings = [{
+ GamepadIndex: 0,
+ A: 0,
+ B: 0,
+ X: 0,
+ Y: 0,
+ LeftShoulder: 0,
+ RightShoulder: 0,
+ LeftTrigger: 0,
+ RightTrigger: 0,
+ View: 0,
+ Menu: 0,
+ LeftThumb: 0,
+ RightThumb: 0,
+ DPadUp: 0,
+ DPadDown: 0,
+ DPadLeft: 0,
+ DPadRight: 0,
+ Nexus: 1,
+ LeftThumbXAxis: 0,
+ LeftThumbYAxis: 0,
+ RightThumbXAxis: 0,
+ RightThumbYAxis: 0,
+ PhysicalPhysicality: 0,
+ VirtualPhysicality: 0,
+ Dirty: true,
+ Virtual: false,
+ }];
+
+ const isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500;
+ const intervalMs = isLongPress ? 500 : 100;
+
+ this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings);
+ this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs);
+ return;
+ }
+ }
+}
diff --git a/src/modules/shortcuts/shortcut-microphone.ts b/src/modules/shortcuts/shortcut-microphone.ts
new file mode 100644
index 0000000..af31ab9
--- /dev/null
+++ b/src/modules/shortcuts/shortcut-microphone.ts
@@ -0,0 +1,24 @@
+import { t } from "@utils/translation";
+import { Toast } from "@utils/toast";
+
+export class MicrophoneShortcut {
+ static toggle(showToast: boolean = true): boolean {
+ if (!window.BX_EXPOSED.streamSession) {
+ return false;
+ }
+
+ const state = window.BX_EXPOSED.streamSession._microphoneState;
+ const enableMic = state === 'Enabled' ? false : true;
+
+ try {
+ window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic);
+ showToast && Toast.show(t('microphone'), t(enableMic ? 'unmuted': 'muted'), {instant: true});
+
+ return enableMic;
+ } catch (e) {
+ console.log(e);
+ }
+
+ return false;
+ }
+}
diff --git a/src/modules/shortcuts/shortcut-stream-ui.ts b/src/modules/shortcuts/shortcut-stream-ui.ts
new file mode 100644
index 0000000..181a556
--- /dev/null
+++ b/src/modules/shortcuts/shortcut-stream-ui.ts
@@ -0,0 +1,6 @@
+export class StreamUiShortcut {
+ static showHideStreamMenu() {
+ // Show menu
+ window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu();
+ }
+}
diff --git a/src/modules/stream/stream-ui.ts b/src/modules/stream/stream-ui.ts
index 6ded232..5c71a9a 100644
--- a/src/modules/stream/stream-ui.ts
+++ b/src/modules/stream/stream-ui.ts
@@ -210,7 +210,7 @@ export function injectStreamMenuButtons() {
// Create Stream Settings button
if (!$btnStreamSettings) {
- $btnStreamSettings = cloneStreamHudButton($orgButton, t('menu-stream-settings'), BxIcon.STREAM_SETTINGS);
+ $btnStreamSettings = cloneStreamHudButton($orgButton, t('stream-settings'), BxIcon.STREAM_SETTINGS);
$btnStreamSettings.addEventListener('click', e => {
hideGripHandle();
e.preventDefault();
@@ -228,7 +228,7 @@ export function injectStreamMenuButtons() {
// Create Stream Stats button
if (!$btnStreamStats) {
- $btnStreamStats = cloneStreamHudButton($orgButton, t('menu-stream-stats'), BxIcon.STREAM_STATS);
+ $btnStreamStats = cloneStreamHudButton($orgButton, t('stream-stats'), BxIcon.STREAM_STATS);
$btnStreamStats.addEventListener('click', e => {
hideGripHandle();
e.preventDefault();
diff --git a/src/modules/ui/ui.ts b/src/modules/ui/ui.ts
index 0a02724..d1a6dfa 100644
--- a/src/modules/ui/ui.ts
+++ b/src/modules/ui/ui.ts
@@ -10,6 +10,7 @@ import { TouchController } from "@modules/touch-controller";
import { t } from "@utils/translation";
import { VibrationManager } from "@modules/vibration-manager";
import { Screenshot } from "@/utils/screenshot";
+import { ControllerShortcut } from "../controller-shortcut";
export function localRedirect(path: string) {
@@ -239,13 +240,25 @@ function setupQuickSettingsBar() {
],
},
+ {
+ icon: BxIcon.COMMAND,
+ group: 'shortcuts',
+ items: [
+ {
+ group: 'shortcuts_controller',
+ label: t('controller-shortcuts'),
+ content: ControllerShortcut.renderSettings(),
+ },
+ ],
+ },
+
{
icon: BxIcon.STREAM_STATS,
group: 'stats',
items: [
{
group: 'stats',
- label: t('menu-stream-stats'),
+ label: t('stream-stats'),
help_url: 'https://better-xcloud.github.io/stream-stats/',
items: [
{
@@ -490,9 +503,24 @@ function resizeVideoPlayer() {
}
+function preloadFonts() {
+ const $link = CE('link', {
+ rel: 'preload',
+ href: 'https://redphx.github.io/better-xcloud/fonts/promptfont.otf',
+ as: 'font',
+ type: 'font/otf',
+ crossorigin: '',
+ });
+
+ document.querySelector('head')?.appendChild($link);
+}
+
+
export function setupStreamUi() {
// Prevent initializing multiple times
if (!document.querySelector('.bx-quick-settings-bar')) {
+ preloadFonts();
+
window.addEventListener('resize', updateVideoPlayerCss);
setupQuickSettingsBar();
StreamStats.render();
diff --git a/src/types/index.d.ts b/src/types/index.d.ts
index 2409859..f533aea 100644
--- a/src/types/index.d.ts
+++ b/src/types/index.d.ts
@@ -38,8 +38,6 @@ type BxStates = {
titleInfo: XcloudTitleInfo;
$video: HTMLVideoElement | null;
- $screenshotCanvas: HTMLCanvasElement | null;
- screenshotCanvasContext: CanvasRenderingContext2D | null;
peerConnection: RTCPeerConnection;
audioContext: AudioContext | null;
diff --git a/src/utils/bx-exposed.ts b/src/utils/bx-exposed.ts
index 6c96149..f15649b 100644
--- a/src/utils/bx-exposed.ts
+++ b/src/utils/bx-exposed.ts
@@ -1,3 +1,4 @@
+import { ControllerShortcut } from "@/modules/controller-shortcut";
import { GameBar } from "@modules/game-bar/game-bar";
import { BxEvent } from "@utils/bx-event";
import { STATES } from "@utils/global";
@@ -111,5 +112,8 @@ export const BxExposed = {
const gainNode = audioCtx.createGain(); // call monkey-patched createGain() in BxAudioContext
source.connect(gainNode).connect(audioCtx.destination);
- }
+ },
+
+ handleControllerShortcut: ControllerShortcut.handle,
+ resetControllerShortcut: ControllerShortcut.reset,
};
diff --git a/src/utils/bx-icon.ts b/src/utils/bx-icon.ts
index e4341f8..a6e83d3 100644
--- a/src/utils/bx-icon.ts
+++ b/src/utils/bx-icon.ts
@@ -1,3 +1,4 @@
+import iconCommand from "@assets/svg/command.svg" with { type: "text" };
import iconController from "@assets/svg/controller.svg" with { type: "text" };
import iconCopy from "@assets/svg/copy.svg" with { type: "text" };
import iconCursorText from "@assets/svg/cursor-text.svg" with { type: "text" };
@@ -24,6 +25,7 @@ import iconMicrophoneMuted from "@assets/svg/microphone-slash.svg" with { type:
export const BxIcon = {
STREAM_SETTINGS: iconStreamSettings,
STREAM_STATS: iconStreamStats,
+ COMMAND: iconCommand,
CONTROLLER: iconController,
DISPLAY: iconDisplay,
MOUSE: iconMouse,
diff --git a/src/utils/prompt-font.ts b/src/utils/prompt-font.ts
new file mode 100644
index 0000000..a64f45b
--- /dev/null
+++ b/src/utils/prompt-font.ts
@@ -0,0 +1,32 @@
+export enum PrompFont {
+ A = '⇓',
+ B = '⇒',
+ X = '⇐',
+ Y = '⇑',
+
+ LB = '↘',
+ RB = '↙',
+ LT = '↖',
+ RT = '↗',
+
+ SELECT = '⇺',
+ START = '⇻',
+ HOME = '',
+
+ UP = '≻',
+ DOWN = '≽',
+ LEFT = '≺',
+ RIGHT = '≼',
+
+ L3 = '↺',
+ LS_UP = '↾',
+ LS_DOWN = '⇂',
+ LS_LEFT = '↼',
+ LS_RIGHT = '⇀',
+
+ R3 = '↻',
+ RS_UP = '↿',
+ RS_DOWN = '⇃',
+ RS_LEFT = '↽',
+ RS_RIGHT = '⇁',
+}
diff --git a/src/utils/screenshot.ts b/src/utils/screenshot.ts
index dd298bc..88a77ee 100644
--- a/src/utils/screenshot.ts
+++ b/src/utils/screenshot.ts
@@ -3,21 +3,24 @@ import { CE } from "./html";
export class Screenshot {
- static setup() {
- const currentStream = STATES.currentStream;
- if (!currentStream.$screenshotCanvas) {
- currentStream.$screenshotCanvas = CE('canvas', {'class': 'bx-gone'});
+ static #$canvas: HTMLCanvasElement;
+ static #canvasContext: CanvasRenderingContext2D;
- currentStream.screenshotCanvasContext = currentStream.$screenshotCanvas.getContext('2d', {
- alpha: false,
- willReadFrequently: false,
- });
+ static setup() {
+ if (Screenshot.#$canvas) {
+ return;
}
- // document.documentElement.appendChild(currentStream.$screenshotCanvas!);
+
+ Screenshot.#$canvas = CE('canvas', {'class': 'bx-gone'});
+
+ Screenshot.#canvasContext = Screenshot.#$canvas.getContext('2d', {
+ alpha: false,
+ willReadFrequently: false,
+ })!;
}
static updateCanvasSize(width: number, height: number) {
- const $canvas = STATES.currentStream.$screenshotCanvas;
+ const $canvas = Screenshot.#$canvas;
if ($canvas) {
$canvas.width = width;
$canvas.height = height;
@@ -25,7 +28,7 @@ export class Screenshot {
}
static updateCanvasFilters(filters: string) {
- STATES.currentStream.screenshotCanvasContext && (STATES.currentStream.screenshotCanvasContext.filter = filters);
+ Screenshot.#canvasContext.filter = filters;
}
private static onAnimationEnd(e: Event) {
@@ -35,7 +38,7 @@ export class Screenshot {
static takeScreenshot(callback?: any) {
const currentStream = STATES.currentStream;
const $video = currentStream.$video;
- const $canvas = currentStream.$screenshotCanvas;
+ const $canvas = Screenshot.#$canvas;
if (!$video || !$canvas) {
return;
}
@@ -43,7 +46,7 @@ export class Screenshot {
$video.parentElement?.addEventListener('animationend', this.onAnimationEnd);
$video.parentElement?.classList.add('bx-taking-screenshot');
- const canvasContext = currentStream.screenshotCanvasContext!;
+ const canvasContext = Screenshot.#canvasContext;
canvasContext.drawImage($video, 0, 0, $canvas.width, $canvas.height);
// Get data URL and pass to parent app
diff --git a/src/utils/translation.ts b/src/utils/translation.ts
index 2a79feb..23b5d1d 100644
--- a/src/utils/translation.ts
+++ b/src/utils/translation.ts
@@ -13,6 +13,7 @@ export const SUPPORTED_LANGUAGES = {
'pl-PL': 'polski',
'pt-BR': 'português (Brasil)',
'ru-RU': 'русский',
+ 'th-TH': 'ภาษาไทย',
'tr-TR': 'Türkçe',
'uk-UA': 'українська',
'vi-VN': 'Tiếng Việt',
@@ -62,8 +63,10 @@ const Texts = {
"copy": "Copy",
"custom": "Custom",
"deadzone-counterweight": "Deadzone counterweight",
+ "decrease": "Decrease",
"default": "Default",
"delete": "Delete",
+ "device": "Device",
"device-unsupported-touch": "Your device doesn't have touch support",
"device-vibration": "Device vibration",
"device-vibration-not-using-gamepad": "On when not using gamepad",
@@ -93,12 +96,14 @@ const Texts = {
"game-bar": "Game Bar",
"getting-consoles-list": "Getting the list of consoles...",
"help": "Help",
+ "hide": "Hide",
"hide-idle-cursor": "Hide mouse cursor on idle",
"hide-scrollbar": "Hide web page's scrollbar",
"hide-system-menu-icon": "Hide System menu's icon",
"hide-touch-controller": "Hide touch controller",
"horizontal-sensitivity": "Horizontal sensitivity",
"import": "Import",
+ "increase": "Increase",
"install-android": "Install Better xCloud app for Android",
"keyboard-shortcuts": "Keyboard shortcuts",
"language": "Language",
@@ -109,13 +114,13 @@ const Texts = {
"local-co-op": "Local co-op",
"map-mouse-to": "Map mouse to",
"may-not-work-properly": "May not work properly!",
- "menu-stream-settings": "Stream settings",
- "menu-stream-stats": "Stream stats",
+ "menu": "Menu",
"microphone": "Microphone",
"mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings",
"mkb-click-to-activate": "Click to activate",
"mkb-disclaimer": "Using this feature when playing online could be viewed as cheating",
"mouse-and-keyboard": "Mouse & Keyboard",
+ "mute-unmute-sound": "Mute/unmute sound",
"muted": "Muted",
"name": "Name",
"new": "New",
@@ -146,6 +151,7 @@ const Texts = {
(e: any) => `Naciśnij ${e.key}, aby przełączyć funkcję myszy i klawiatury`,
(e: any) => `Pressione ${e.key} para ativar/desativar a função de Mouse e Teclado`,
(e: any) => `Нажмите ${e.key} для переключения функции мыши и клавиатуры`,
+ ,
(e: any) => `Klavye ve fare özelliğini açmak için ${e.key} tuşuna basın`,
(e: any) => `Натисніть ${e.key}, щоб увімкнути або вимкнути функцію миші та клавіатури`,
(e: any) => `Nhấn ${e.key} để bật/tắt tính năng Chuột và Bàn phím`,
@@ -167,6 +173,7 @@ const Texts = {
"safari-failed-message": "Failed to run Better xCloud. Retrying, please wait...",
"saturation": "Saturation",
"save": "Save",
+ "screen": "Screen",
"screenshot-apply-filters": "Applies video filters to screenshots",
"separate-touch-controller": "Separate Touch controller & Controller #1",
"separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",
@@ -174,7 +181,9 @@ const Texts = {
"settings-reload": "Reload page to reflect changes",
"settings-reloading": "Reloading...",
"shortcut-keys": "Shortcut keys",
+ "show": "Show",
"show-game-art": "Show game art",
+ "show-hide": "Show/hide",
"show-stats-on-startup": "Show stats when starting the game",
"show-touch-controller": "Show touch controller",
"show-wait-time": "Show the estimated wait time",
@@ -195,6 +204,8 @@ const Texts = {
"stick-decay-minimum": "Stick decay minimum",
"stick-decay-strength": "Stick decay strength",
"stream": "Stream",
+ "stream-settings": "Stream settings",
+ "stream-stats": "Stream stats",
"stretch": "Stretch",
"stretch-note": "Don't use with native touch games",
"support-better-xcloud": "Support Better xCloud",
@@ -210,6 +221,9 @@ const Texts = {
"tc-muted-colors": "Muted colors",
"tc-standard-layout-style": "Standard layout's button style",
"text-size": "Text size",
+ "toggle": "Toggle",
+ "toggle-microphone": "Toggle microphone",
+ "toggle-stream-stats": "Toggle stream stats",
"top-center": "Top-center",
"top-left": "Top-left",
"top-right": "Top-right",
@@ -226,6 +240,7 @@ const Texts = {
(e: any) => `Układ sterowania dotykowego stworzony przez ${e.name}`,
(e: any) => `Disposição de controle por toque feito por ${e.name}`,
(e: any) => `Сенсорная раскладка по ${e.name}`,
+ ,
(e: any) => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,
(e: any) => `Розташування сенсорного керування від ${e.name}`,
(e: any) => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,
@@ -292,7 +307,7 @@ export class Translations {
static get(key: keyof typeof Texts, values?: any): T {
let text = null;
- if (Translations.#selectedLocale !== Translations.#EN_US) {
+ if (Translations.#foreignTranslations && Translations.#selectedLocale !== Translations.#EN_US) {
text = Translations.#foreignTranslations[key];
}