mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-06 23:57:19 +02:00
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:
parent
22e29e1d92
commit
07c1757237
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
4
src/assets/svg/command.svg
Normal file
4
src/assets/svg/command.svg
Normal 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 |
321
src/modules/controller-shortcut.ts
Normal file
321
src/modules/controller-shortcut.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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({
|
||||
|
@ -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],
|
||||
};
|
||||
|
||||
|
||||
|
@ -384,13 +384,19 @@ if (!!window.BX_REMOTE_PLAY_CONFIG) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let newCode = `
|
||||
// Expose onShowStreamMenu
|
||||
window.BX_EXPOSED.showStreamMenu = e.onShowStreamMenu;
|
||||
// Restore the "..." button
|
||||
str = str.replace(text, 'e.guideUI = null;' + text);
|
||||
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;
|
||||
},
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
src/modules/shortcuts/shortcut-microphone.ts
Normal file
24
src/modules/shortcuts/shortcut-microphone.ts
Normal 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;
|
||||
}
|
||||
}
|
6
src/modules/shortcuts/shortcut-stream-ui.ts
Normal file
6
src/modules/shortcuts/shortcut-stream-ui.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export class StreamUiShortcut {
|
||||
static showHideStreamMenu() {
|
||||
// Show menu
|
||||
window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu();
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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<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() {
|
||||
// Prevent initializing multiple times
|
||||
if (!document.querySelector('.bx-quick-settings-bar')) {
|
||||
preloadFonts();
|
||||
|
||||
window.addEventListener('resize', updateVideoPlayerCss);
|
||||
setupQuickSettingsBar();
|
||||
StreamStats.render();
|
||||
|
2
src/types/index.d.ts
vendored
2
src/types/index.d.ts
vendored
@ -38,8 +38,6 @@ type BxStates = {
|
||||
titleInfo: XcloudTitleInfo;
|
||||
|
||||
$video: HTMLVideoElement | null;
|
||||
$screenshotCanvas: HTMLCanvasElement | null;
|
||||
screenshotCanvasContext: CanvasRenderingContext2D | null;
|
||||
|
||||
peerConnection: RTCPeerConnection;
|
||||
audioContext: AudioContext | null;
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
32
src/utils/prompt-font.ts
Normal file
32
src/utils/prompt-font.ts
Normal 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 = '⇁',
|
||||
}
|
@ -3,21 +3,24 @@ import { CE } from "./html";
|
||||
|
||||
|
||||
export class Screenshot {
|
||||
static setup() {
|
||||
const currentStream = STATES.currentStream;
|
||||
if (!currentStream.$screenshotCanvas) {
|
||||
currentStream.$screenshotCanvas = CE<HTMLCanvasElement>('canvas', {'class': 'bx-gone'});
|
||||
static #$canvas: HTMLCanvasElement;
|
||||
static #canvasContext: CanvasRenderingContext2D;
|
||||
|
||||
currentStream.screenshotCanvasContext = currentStream.$screenshotCanvas.getContext('2d', {
|
||||
static setup() {
|
||||
if (Screenshot.#$canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
Screenshot.#$canvas = CE<HTMLCanvasElement>('canvas', {'class': 'bx-gone'});
|
||||
|
||||
Screenshot.#canvasContext = Screenshot.#$canvas.getContext('2d', {
|
||||
alpha: false,
|
||||
willReadFrequently: false,
|
||||
});
|
||||
}
|
||||
// document.documentElement.appendChild(currentStream.$screenshotCanvas!);
|
||||
})!;
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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<T=string>(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];
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user