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
This commit is contained in:
redphx 2024-05-24 18:10:38 +07:00
parent 22e29e1d92
commit 07c1757237
17 changed files with 606 additions and 58 deletions

View File

@ -124,3 +124,29 @@
font-style: italic; font-style: italic;
padding-top: 16px; 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;
}
}
}
}

View File

@ -0,0 +1,4 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 32 32'>
<path d="M25.425 1.5c2.784 0 5.075 2.291 5.075 5.075s-2.291 5.075-5.075 5.075H20.35V6.575c0-2.784 2.291-5.075 5.075-5.075zM11.65 11.65H6.575C3.791 11.65 1.5 9.359 1.5 6.575S3.791 1.5 6.575 1.5s5.075 2.291 5.075 5.075v5.075zm8.7 8.7h5.075c2.784 0 5.075 2.291 5.075 5.075S28.209 30.5 25.425 30.5s-5.075-2.291-5.075-5.075V20.35zM6.575 30.5c-2.784 0-5.075-2.291-5.075-5.075s2.291-5.075 5.075-5.075h5.075v5.075c0 2.784-2.291 5.075-5.075 5.075z"/>
<path d="M11.65 11.65h8.7v8.7h-8.7z"/>
</svg>

After

Width:  |  Height:  |  Size: 667 B

View File

@ -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<HTMLOptionElement>('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<HTMLOptionElement>('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<HTMLSelectElement>('select', {autocomplete: 'off'}, CE('option', {value: ''}, '---'));
for (const groupLabel in actions) {
const items = actions[groupLabel];
if (!items) {
continue;
}
const $optGroup = CE<HTMLOptGroupElement>('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<HTMLOptionElement>('option', {value: action}, label);
$optGroup.appendChild($option);
}
$baseSelect.appendChild($optGroup);
}
const $container = CE('div', {});
const $profile = CE<HTMLSelectElement>('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;
}
}

View File

@ -3,6 +3,7 @@ import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle, CE } from "@utils/html"; import { createButton, ButtonStyle, CE } from "@utils/html";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { BaseGameBarAction } from "./action-base"; import { BaseGameBarAction } from "./action-base";
import { MicrophoneShortcut } from "../shortcuts/shortcut-microphone";
enum MicrophoneState { enum MicrophoneState {
REQUESTED = 'Requested', REQUESTED = 'Requested',
@ -22,15 +23,9 @@ export class MicrophoneAction extends BaseGameBarAction {
const onClick = (e: Event) => { const onClick = (e: Event) => {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
const state = this.$content.getAttribute('data-enabled');
const enableMic = state === 'true' ? false : true;
try { const enabled = MicrophoneShortcut.toggle(false);
window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic); this.$content.setAttribute('data-enabled', enabled.toString());
this.$content.setAttribute('data-enabled', enableMic.toString());
} catch (e) {
console.log(e);
}
}; };
const $btnDefault = createButton({ const $btnDefault = createButton({

View File

@ -1,4 +1,5 @@
import type { GamepadKeyNameType } from "@/types/mkb"; import type { GamepadKeyNameType } from "@/types/mkb";
import { PrompFont } from "@/utils/prompt-font";
export enum GamepadKey { export enum GamepadKey {
A = 0, A = 0,
@ -18,6 +19,7 @@ export enum GamepadKey {
LEFT = 14, LEFT = 14,
RIGHT = 15, RIGHT = 15,
HOME = 16, HOME = 16,
SHARE = 17,
LS_UP = 100, LS_UP = 100,
LS_DOWN = 101, LS_DOWN = 101,
@ -32,36 +34,36 @@ export enum GamepadKey {
export const GamepadKeyName: GamepadKeyNameType = { export const GamepadKeyName: GamepadKeyNameType = {
[GamepadKey.A]: ['A', '⇓'], [GamepadKey.A]: ['A', PrompFont.A],
[GamepadKey.B]: ['B', '⇒'], [GamepadKey.B]: ['B', PrompFont.B],
[GamepadKey.X]: ['X', '⇐'], [GamepadKey.X]: ['X', PrompFont.X],
[GamepadKey.Y]: ['Y', '⇑'], [GamepadKey.Y]: ['Y', PrompFont.Y],
[GamepadKey.LB]: ['LB', '↘'], [GamepadKey.LB]: ['LB', PrompFont.LB],
[GamepadKey.RB]: ['RB', '↙'], [GamepadKey.RB]: ['RB', PrompFont.RB],
[GamepadKey.LT]: ['LT', '↖'], [GamepadKey.LT]: ['LT', PrompFont.LT],
[GamepadKey.RT]: ['RT', '↗'], [GamepadKey.RT]: ['RT', PrompFont.RT],
[GamepadKey.SELECT]: ['Select', '⇺'], [GamepadKey.SELECT]: ['Select', PrompFont.SELECT],
[GamepadKey.START]: ['Start', '⇻'], [GamepadKey.START]: ['Start', PrompFont.START],
[GamepadKey.HOME]: ['Home', ''], [GamepadKey.HOME]: ['Home', PrompFont.HOME],
[GamepadKey.UP]: ['D-Pad Up', '≻'], [GamepadKey.UP]: ['D-Pad Up', PrompFont.UP],
[GamepadKey.DOWN]: ['D-Pad Down', '≽'], [GamepadKey.DOWN]: ['D-Pad Down', PrompFont.DOWN],
[GamepadKey.LEFT]: ['D-Pad Left', '≺'], [GamepadKey.LEFT]: ['D-Pad Left', PrompFont.LEFT],
[GamepadKey.RIGHT]: ['D-Pad Right', '≼'], [GamepadKey.RIGHT]: ['D-Pad Right', PrompFont.RIGHT],
[GamepadKey.L3]: ['L3', '↺'], [GamepadKey.L3]: ['L3', PrompFont.L3],
[GamepadKey.LS_UP]: ['Left Stick Up', '↾'], [GamepadKey.LS_UP]: ['Left Stick Up', PrompFont.LS_UP],
[GamepadKey.LS_DOWN]: ['Left Stick Down', '⇂'], [GamepadKey.LS_DOWN]: ['Left Stick Down', PrompFont.LS_DOWN],
[GamepadKey.LS_LEFT]: ['Left Stick Left', '↼'], [GamepadKey.LS_LEFT]: ['Left Stick Left', PrompFont.LS_LEFT],
[GamepadKey.LS_RIGHT]: ['Left Stick Right', '⇀'], [GamepadKey.LS_RIGHT]: ['Left Stick Right', PrompFont.LS_RIGHT],
[GamepadKey.R3]: ['R3', '↻'], [GamepadKey.R3]: ['R3', PrompFont.R3],
[GamepadKey.RS_UP]: ['Right Stick Up', '↿'], [GamepadKey.RS_UP]: ['Right Stick Up', PrompFont.RS_UP],
[GamepadKey.RS_DOWN]: ['Right Stick Down', '⇃'], [GamepadKey.RS_DOWN]: ['Right Stick Down', PrompFont.RS_DOWN],
[GamepadKey.RS_LEFT]: ['Right Stick Left', '↽'], [GamepadKey.RS_LEFT]: ['Right Stick Left', PrompFont.RS_LEFT],
[GamepadKey.RS_RIGHT]: ['Right Stick Right', '⇁'], [GamepadKey.RS_RIGHT]: ['Right Stick Right', PrompFont.RS_RIGHT],
}; };

View File

@ -384,13 +384,19 @@ if (!!window.BX_REMOTE_PLAY_CONFIG) {
return false; return false;
} }
// Restore the "..." button let newCode = `
str = str.replace(text, 'e.guideUI = null;' + text); // 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 // Remove the TAK Edit button when the touch controller is disabled
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off') { 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; return str;
}, },

View File

@ -1,5 +1,87 @@
const currentGamepad = ${gamepadVar}; const currentGamepad = ${gamepadVar};
// Share button on XS controller
if (currentGamepad.buttons[17] && currentGamepad.buttons[17].value === 1) { if (currentGamepad.buttons[17] && currentGamepad.buttons[17].value === 1) {
window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT)); 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;
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
export class StreamUiShortcut {
static showHideStreamMenu() {
// Show menu
window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu();
}
}

View File

@ -210,7 +210,7 @@ export function injectStreamMenuButtons() {
// Create Stream Settings button // Create Stream Settings button
if (!$btnStreamSettings) { 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 => { $btnStreamSettings.addEventListener('click', e => {
hideGripHandle(); hideGripHandle();
e.preventDefault(); e.preventDefault();
@ -228,7 +228,7 @@ export function injectStreamMenuButtons() {
// Create Stream Stats button // Create Stream Stats button
if (!$btnStreamStats) { 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 => { $btnStreamStats.addEventListener('click', e => {
hideGripHandle(); hideGripHandle();
e.preventDefault(); e.preventDefault();

View File

@ -10,6 +10,7 @@ import { TouchController } from "@modules/touch-controller";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { VibrationManager } from "@modules/vibration-manager"; import { VibrationManager } from "@modules/vibration-manager";
import { Screenshot } from "@/utils/screenshot"; import { Screenshot } from "@/utils/screenshot";
import { ControllerShortcut } from "../controller-shortcut";
export function localRedirect(path: string) { 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, icon: BxIcon.STREAM_STATS,
group: 'stats', group: 'stats',
items: [ items: [
{ {
group: 'stats', group: 'stats',
label: t('menu-stream-stats'), label: t('stream-stats'),
help_url: 'https://better-xcloud.github.io/stream-stats/', help_url: 'https://better-xcloud.github.io/stream-stats/',
items: [ items: [
{ {
@ -490,9 +503,24 @@ function resizeVideoPlayer() {
} }
function preloadFonts() {
const $link = CE<HTMLLinkElement>('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() { export function setupStreamUi() {
// Prevent initializing multiple times // Prevent initializing multiple times
if (!document.querySelector('.bx-quick-settings-bar')) { if (!document.querySelector('.bx-quick-settings-bar')) {
preloadFonts();
window.addEventListener('resize', updateVideoPlayerCss); window.addEventListener('resize', updateVideoPlayerCss);
setupQuickSettingsBar(); setupQuickSettingsBar();
StreamStats.render(); StreamStats.render();

View File

@ -38,8 +38,6 @@ type BxStates = {
titleInfo: XcloudTitleInfo; titleInfo: XcloudTitleInfo;
$video: HTMLVideoElement | null; $video: HTMLVideoElement | null;
$screenshotCanvas: HTMLCanvasElement | null;
screenshotCanvasContext: CanvasRenderingContext2D | null;
peerConnection: RTCPeerConnection; peerConnection: RTCPeerConnection;
audioContext: AudioContext | null; audioContext: AudioContext | null;

View File

@ -1,3 +1,4 @@
import { ControllerShortcut } from "@/modules/controller-shortcut";
import { GameBar } from "@modules/game-bar/game-bar"; import { GameBar } from "@modules/game-bar/game-bar";
import { BxEvent } from "@utils/bx-event"; import { BxEvent } from "@utils/bx-event";
import { STATES } from "@utils/global"; import { STATES } from "@utils/global";
@ -111,5 +112,8 @@ export const BxExposed = {
const gainNode = audioCtx.createGain(); // call monkey-patched createGain() in BxAudioContext const gainNode = audioCtx.createGain(); // call monkey-patched createGain() in BxAudioContext
source.connect(gainNode).connect(audioCtx.destination); source.connect(gainNode).connect(audioCtx.destination);
} },
handleControllerShortcut: ControllerShortcut.handle,
resetControllerShortcut: ControllerShortcut.reset,
}; };

View File

@ -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 iconController from "@assets/svg/controller.svg" with { type: "text" };
import iconCopy from "@assets/svg/copy.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" }; 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 = { export const BxIcon = {
STREAM_SETTINGS: iconStreamSettings, STREAM_SETTINGS: iconStreamSettings,
STREAM_STATS: iconStreamStats, STREAM_STATS: iconStreamStats,
COMMAND: iconCommand,
CONTROLLER: iconController, CONTROLLER: iconController,
DISPLAY: iconDisplay, DISPLAY: iconDisplay,
MOUSE: iconMouse, MOUSE: iconMouse,

32
src/utils/prompt-font.ts Normal file
View File

@ -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 = '⇁',
}

View File

@ -3,21 +3,24 @@ import { CE } from "./html";
export class Screenshot { export class Screenshot {
static setup() { static #$canvas: HTMLCanvasElement;
const currentStream = STATES.currentStream; static #canvasContext: CanvasRenderingContext2D;
if (!currentStream.$screenshotCanvas) {
currentStream.$screenshotCanvas = CE<HTMLCanvasElement>('canvas', {'class': 'bx-gone'});
currentStream.screenshotCanvasContext = currentStream.$screenshotCanvas.getContext('2d', { static setup() {
alpha: false, if (Screenshot.#$canvas) {
willReadFrequently: false, return;
});
} }
// document.documentElement.appendChild(currentStream.$screenshotCanvas!);
Screenshot.#$canvas = CE<HTMLCanvasElement>('canvas', {'class': 'bx-gone'});
Screenshot.#canvasContext = Screenshot.#$canvas.getContext('2d', {
alpha: false,
willReadFrequently: false,
})!;
} }
static updateCanvasSize(width: number, height: number) { static updateCanvasSize(width: number, height: number) {
const $canvas = STATES.currentStream.$screenshotCanvas; const $canvas = Screenshot.#$canvas;
if ($canvas) { if ($canvas) {
$canvas.width = width; $canvas.width = width;
$canvas.height = height; $canvas.height = height;
@ -25,7 +28,7 @@ export class Screenshot {
} }
static updateCanvasFilters(filters: string) { static updateCanvasFilters(filters: string) {
STATES.currentStream.screenshotCanvasContext && (STATES.currentStream.screenshotCanvasContext.filter = filters); Screenshot.#canvasContext.filter = filters;
} }
private static onAnimationEnd(e: Event) { private static onAnimationEnd(e: Event) {
@ -35,7 +38,7 @@ export class Screenshot {
static takeScreenshot(callback?: any) { static takeScreenshot(callback?: any) {
const currentStream = STATES.currentStream; const currentStream = STATES.currentStream;
const $video = currentStream.$video; const $video = currentStream.$video;
const $canvas = currentStream.$screenshotCanvas; const $canvas = Screenshot.#$canvas;
if (!$video || !$canvas) { if (!$video || !$canvas) {
return; return;
} }
@ -43,7 +46,7 @@ export class Screenshot {
$video.parentElement?.addEventListener('animationend', this.onAnimationEnd); $video.parentElement?.addEventListener('animationend', this.onAnimationEnd);
$video.parentElement?.classList.add('bx-taking-screenshot'); $video.parentElement?.classList.add('bx-taking-screenshot');
const canvasContext = currentStream.screenshotCanvasContext!; const canvasContext = Screenshot.#canvasContext;
canvasContext.drawImage($video, 0, 0, $canvas.width, $canvas.height); canvasContext.drawImage($video, 0, 0, $canvas.width, $canvas.height);
// Get data URL and pass to parent app // Get data URL and pass to parent app

View File

@ -13,6 +13,7 @@ export const SUPPORTED_LANGUAGES = {
'pl-PL': 'polski', 'pl-PL': 'polski',
'pt-BR': 'português (Brasil)', 'pt-BR': 'português (Brasil)',
'ru-RU': 'русский', 'ru-RU': 'русский',
'th-TH': 'ภาษาไทย',
'tr-TR': 'Türkçe', 'tr-TR': 'Türkçe',
'uk-UA': 'українська', 'uk-UA': 'українська',
'vi-VN': 'Tiếng Việt', 'vi-VN': 'Tiếng Việt',
@ -62,8 +63,10 @@ const Texts = {
"copy": "Copy", "copy": "Copy",
"custom": "Custom", "custom": "Custom",
"deadzone-counterweight": "Deadzone counterweight", "deadzone-counterweight": "Deadzone counterweight",
"decrease": "Decrease",
"default": "Default", "default": "Default",
"delete": "Delete", "delete": "Delete",
"device": "Device",
"device-unsupported-touch": "Your device doesn't have touch support", "device-unsupported-touch": "Your device doesn't have touch support",
"device-vibration": "Device vibration", "device-vibration": "Device vibration",
"device-vibration-not-using-gamepad": "On when not using gamepad", "device-vibration-not-using-gamepad": "On when not using gamepad",
@ -93,12 +96,14 @@ const Texts = {
"game-bar": "Game Bar", "game-bar": "Game Bar",
"getting-consoles-list": "Getting the list of consoles...", "getting-consoles-list": "Getting the list of consoles...",
"help": "Help", "help": "Help",
"hide": "Hide",
"hide-idle-cursor": "Hide mouse cursor on idle", "hide-idle-cursor": "Hide mouse cursor on idle",
"hide-scrollbar": "Hide web page's scrollbar", "hide-scrollbar": "Hide web page's scrollbar",
"hide-system-menu-icon": "Hide System menu's icon", "hide-system-menu-icon": "Hide System menu's icon",
"hide-touch-controller": "Hide touch controller", "hide-touch-controller": "Hide touch controller",
"horizontal-sensitivity": "Horizontal sensitivity", "horizontal-sensitivity": "Horizontal sensitivity",
"import": "Import", "import": "Import",
"increase": "Increase",
"install-android": "Install Better xCloud app for Android", "install-android": "Install Better xCloud app for Android",
"keyboard-shortcuts": "Keyboard shortcuts", "keyboard-shortcuts": "Keyboard shortcuts",
"language": "Language", "language": "Language",
@ -109,13 +114,13 @@ const Texts = {
"local-co-op": "Local co-op", "local-co-op": "Local co-op",
"map-mouse-to": "Map mouse to", "map-mouse-to": "Map mouse to",
"may-not-work-properly": "May not work properly!", "may-not-work-properly": "May not work properly!",
"menu-stream-settings": "Stream settings", "menu": "Menu",
"menu-stream-stats": "Stream stats",
"microphone": "Microphone", "microphone": "Microphone",
"mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings", "mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings",
"mkb-click-to-activate": "Click to activate", "mkb-click-to-activate": "Click to activate",
"mkb-disclaimer": "Using this feature when playing online could be viewed as cheating", "mkb-disclaimer": "Using this feature when playing online could be viewed as cheating",
"mouse-and-keyboard": "Mouse & Keyboard", "mouse-and-keyboard": "Mouse & Keyboard",
"mute-unmute-sound": "Mute/unmute sound",
"muted": "Muted", "muted": "Muted",
"name": "Name", "name": "Name",
"new": "New", "new": "New",
@ -146,6 +151,7 @@ const Texts = {
(e: any) => `Naciśnij ${e.key}, aby przełączyć funkcję myszy i klawiatury`, (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) => `Pressione ${e.key} para ativar/desativar a função de Mouse e Teclado`,
(e: any) => `Нажмите ${e.key} для переключения функции мыши и клавиатуры`, (e: any) => `Нажмите ${e.key} для переключения функции мыши и клавиатуры`,
,
(e: any) => `Klavye ve fare özelliğini açmak için ${e.key} tuşuna basın`, (e: any) => `Klavye ve fare özelliğini açmak için ${e.key} tuşuna basın`,
(e: any) => `Натисніть ${e.key}, щоб увімкнути або вимкнути функцію миші та клавіатури`, (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`, (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...", "safari-failed-message": "Failed to run Better xCloud. Retrying, please wait...",
"saturation": "Saturation", "saturation": "Saturation",
"save": "Save", "save": "Save",
"screen": "Screen",
"screenshot-apply-filters": "Applies video filters to screenshots", "screenshot-apply-filters": "Applies video filters to screenshots",
"separate-touch-controller": "Separate Touch controller & Controller #1", "separate-touch-controller": "Separate Touch controller & Controller #1",
"separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2", "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-reload": "Reload page to reflect changes",
"settings-reloading": "Reloading...", "settings-reloading": "Reloading...",
"shortcut-keys": "Shortcut keys", "shortcut-keys": "Shortcut keys",
"show": "Show",
"show-game-art": "Show game art", "show-game-art": "Show game art",
"show-hide": "Show/hide",
"show-stats-on-startup": "Show stats when starting the game", "show-stats-on-startup": "Show stats when starting the game",
"show-touch-controller": "Show touch controller", "show-touch-controller": "Show touch controller",
"show-wait-time": "Show the estimated wait time", "show-wait-time": "Show the estimated wait time",
@ -195,6 +204,8 @@ const Texts = {
"stick-decay-minimum": "Stick decay minimum", "stick-decay-minimum": "Stick decay minimum",
"stick-decay-strength": "Stick decay strength", "stick-decay-strength": "Stick decay strength",
"stream": "Stream", "stream": "Stream",
"stream-settings": "Stream settings",
"stream-stats": "Stream stats",
"stretch": "Stretch", "stretch": "Stretch",
"stretch-note": "Don't use with native touch games", "stretch-note": "Don't use with native touch games",
"support-better-xcloud": "Support Better xCloud", "support-better-xcloud": "Support Better xCloud",
@ -210,6 +221,9 @@ const Texts = {
"tc-muted-colors": "Muted colors", "tc-muted-colors": "Muted colors",
"tc-standard-layout-style": "Standard layout's button style", "tc-standard-layout-style": "Standard layout's button style",
"text-size": "Text size", "text-size": "Text size",
"toggle": "Toggle",
"toggle-microphone": "Toggle microphone",
"toggle-stream-stats": "Toggle stream stats",
"top-center": "Top-center", "top-center": "Top-center",
"top-left": "Top-left", "top-left": "Top-left",
"top-right": "Top-right", "top-right": "Top-right",
@ -226,6 +240,7 @@ const Texts = {
(e: any) => `Układ sterowania dotykowego stworzony przez ${e.name}`, (e: any) => `Układ sterowania dotykowego stworzony przez ${e.name}`,
(e: any) => `Disposição de controle por toque feito por ${e.name}`, (e: any) => `Disposição de controle por toque feito por ${e.name}`,
(e: any) => `Сенсорная раскладка по ${e.name}`, (e: any) => `Сенсорная раскладка по ${e.name}`,
,
(e: any) => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`, (e: any) => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,
(e: any) => `Розташування сенсорного керування від ${e.name}`, (e: any) => `Розташування сенсорного керування від ${e.name}`,
(e: any) => `Bố cục điều khiển cảm ứng tạo bởi ${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<T=string>(key: keyof typeof Texts, values?: any): T { static get<T=string>(key: keyof typeof Texts, values?: any): T {
let text = null; let text = null;
if (Translations.#selectedLocale !== Translations.#EN_US) { if (Translations.#foreignTranslations && Translations.#selectedLocale !== Translations.#EN_US) {
text = Translations.#foreignTranslations[key]; text = Translations.#foreignTranslations[key];
} }