mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-06 13:18:27 +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:
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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user