Controller shortcuts (#157)

* Basic code for GamepadHandler

* Take screenshot using Home + RB

* Enable shortcuts using setting

* Only poll gamepad when playing

* Toogle stats using Home + Select

* Add basic Toast class

* Update translations
This commit is contained in:
redphx 2023-11-19 16:09:41 +07:00 committed by GitHub
parent d73f91ed5f
commit 9b955aabb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -375,6 +375,13 @@ const Translations = {
"vi-VN": "Độ tương phản",
"zh-CN": "对比度",
},
"controller": {
"en-US": "Controller",
"ja-JP": "コントローラー",
"pl-PL": "Kontroler",
"pt-BR": "Controle",
"vi-VN": "Bộ điều khiển",
},
"custom": {
"de-DE": "Benutzerdefiniert",
"en-US": "Custom",
@ -484,6 +491,13 @@ const Translations = {
"vi-VN": "Khóa phân tích thông tin của xCloud",
"zh-CN": "关闭 xCloud 遥测数据统计",
},
"enable-controller-shortcuts": {
"en-US": "Enable controller shortcuts",
"ja-JP": "コントローラーショートカットを有効化",
"pl-PL": "Włącz skróty kontrolera",
"pt-BR": "Ativar atalhos do controle",
"vi-VN": "Bật tính năng phím tắt cho bộ điều khiển",
},
"enable-mic-on-startup": {
"de-DE": "Mikrofon bei Spielstart aktivieren",
"en-US": "Enable microphone on game launch",
@ -623,6 +637,20 @@ const Translations = {
"vi-VN": "Thông số stream",
"zh-CN": "串流统计数据",
},
"microphone": {
"en-US": "Microphone",
"ja-JP": "マイク",
"pl-PL": "Mikrofon",
"pt-BR": "Microfone",
"vi-VN": "Mic",
},
"muted": {
"en-US": "Muted",
"ja-JP": "ミュート",
"pl-PL": "Wyciszony",
"pt-BR": "Mutado",
"vi-VN": "Đã tắt âm",
},
"normal": {
"de-DE": "Mittel",
"en-US": "Normal",
@ -651,6 +679,13 @@ const Translations = {
"vi-VN": "Tắt",
"zh-CN": "关",
},
"on": {
"en-US": "On",
"ja-JP": "オン",
"pl-PL": "Włącz",
"pt-BR": "Ativado",
"vi-VN": "Bật",
},
"opacity": {
"de-DE": "Deckkraft",
"en-US": "Opacity",
@ -987,6 +1022,13 @@ const Translations = {
"vi-VN": "Nhỏ",
"zh-CN": "小",
},
"sound": {
"en-US": "Sound",
"ja-JP": "サウンド",
"pl-PL": "Dźwięk",
"pt-BR": "Som",
"vi-VN": "Âm thanh",
},
"stat-bitrate": {
"de-DE": "Bitrate",
"en-US": "Bitrate",
@ -1323,6 +1365,13 @@ const Translations = {
"vi-VN": "Giao diện",
"zh-CN": "UI",
},
"unmuted": {
"en-US": "Unmuted",
"ja-JP": "ミュート解除",
"pl-PL": "Wyciszenie wyłączone",
"pt-BR": "Desmutado",
"vi-VN": "Đã mở âm",
},
"user-agent-profile": {
"de-DE": "User-Agent Profil",
"en-US": "User-Agent profile",
@ -1500,6 +1549,7 @@ window.addEventListener('load', e => {
const SERVER_REGIONS = {};
var IS_PLAYING = false;
var STREAM_WEBRTC;
var STREAM_AUDIO_CONTEXT;
var STREAM_AUDIO_GAIN_NODE;
@ -1893,6 +1943,164 @@ class TouchController {
}
}
class Toast {
static #$wrapper;
static #$msg;
static #$status;
static setup() {
Toast.#$wrapper = createElement('div', {'class': 'bx-toast bx-gone'},
Toast.#$msg = createElement('span', {'class': 'bx-toast-msg'}),
Toast.#$status = createElement('span', {'class': 'bx-toast-status'}));
document.documentElement.appendChild(Toast.#$wrapper);
}
}
class GamepadHandler {
static #BUTTON_A = 0;
static #BUTTON_B = 1;
static #BUTTON_X = 2;
static #BUTTON_Y = 3;
static #BUTTON_UP = 12;
static #BUTTON_DOWN = 13;
static #BUTTON_LEFT = 14;
static #BUTTON_RIGHT = 15;
static #BUTTON_LB = 4;
static #BUTTON_LT = 6;
static #BUTTON_RB = 5;
static #BUTTON_RT = 7;
static #BUTTON_SELECT = 8;
static #BUTTON_START = 9;
static #BUTTON_HOME = 16;
static #isPolling = false;
static #pollingInterval;
static #isHoldingHome = false;
static #buttonsCache = [];
static #buttonsStatus = [];
static #emulatedGamepads = [null, null, null, null];
static #nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator);
static #cloneGamepad(gamepad) {
const buttons = Array(gamepad.buttons.length).fill({pressed: false, value: 0});
buttons[GamepadHandler.#BUTTON_HOME] = {
pressed: true,
value: 0,
};
return {
timestamp: gamepad.timestamp,
id: gamepad.id,
index: gamepad.index,
connected: gamepad.connected,
mapping: gamepad.mapping,
axes: [0, 0, 0, 0],
buttons: buttons,
};
}
static #customGetGamepads() {
return GamepadHandler.#emulatedGamepads;
}
static #isPressed(buttonIndex) {
return !GamepadHandler.#buttonsCache[buttonIndex] && GamepadHandler.#buttonsStatus[buttonIndex];
}
static #poll() {
// Move the buttons status from the previous frame to the cache
GamepadHandler.#buttonsCache = GamepadHandler.#buttonsStatus.slice(0);
// Clear the buttons status
GamepadHandler.#buttonsStatus = [];
const pressed = [];
const timestamps = [0, 0, 0, 0];
GamepadHandler.#nativeGetGamepads().forEach(gamepad => {
if (!gamepad || gamepad.mapping !== 'standard' || !gamepad.buttons) {
return;
}
gamepad.buttons.forEach((button, index) => {
// Only add the newly pressed button to the array (holding doesn't count)
if (button.pressed) {
timestamps[index] = gamepad.timestamp;
pressed[index] = true;
}
});
});
GamepadHandler.#buttonsStatus = pressed;
GamepadHandler.#isHoldingHome = !!pressed[GamepadHandler.#BUTTON_HOME];
if (GamepadHandler.#isHoldingHome) {
// Update timestamps
GamepadHandler.#emulatedGamepads.forEach(gamepad => {
gamepad && (gamepad.timestamp = timestamps[gamepad.index]);
});
// Patch getGamepads()
window.navigator.getGamepads = GamepadHandler.#customGetGamepads;
// Check pressed button
if (GamepadHandler.#isPressed(GamepadHandler.#BUTTON_RB)) {
takeScreenshot();
} else if (GamepadHandler.#isPressed(GamepadHandler.#BUTTON_SELECT)) {
StreamStats.toggle();
}
} else {
// Restore to native getGamepads()
window.navigator.getGamepads = GamepadHandler.#nativeGetGamepads;
}
}
static initialSetup() {
window.addEventListener('gamepadconnected', e => {
const gamepad = e.gamepad;
console.log('Gamepad connected', gamepad);
GamepadHandler.#emulatedGamepads[gamepad.index] = GamepadHandler.#cloneGamepad(gamepad);
if (IS_PLAYING) {
GamepadHandler.startPolling();
}
});
window.addEventListener('gamepaddisconnected', e => {
console.log('Gamepad disconnected', e.gamepad);
GamepadHandler.#emulatedGamepads[e.gamepad.index] = null;
// No gamepads left
const noGamepads = GamepadHandler.#nativeGetGamepads().every(gamepad => gamepad === null);
if (noGamepads) {
GamepadHandler.stopPolling();
}
});
}
static startPolling() {
if (GamepadHandler.#isPolling) {
return;
}
GamepadHandler.stopPolling();
GamepadHandler.#isPolling = true;
GamepadHandler.#pollingInterval = setInterval(GamepadHandler.#poll, 50);
}
static stopPolling() {
GamepadHandler.#isPolling = false;
GamepadHandler.#isHoldingHome = false;
GamepadHandler.#pollingInterval && clearInterval(GamepadHandler.#pollingInterval);
GamepadHandler.#pollingInterval = null;
}
}
class MouseCursorHider {
static #timeout;
static #cursorVisible = true;
@ -2544,6 +2752,8 @@ class Preferences {
static get STREAM_DISABLE_FEEDBACK_DIALOG() { return 'stream_disable_feedback_dialog'; }
static get CONTROLLER_ENABLE_SHORTCUTS() { return 'controller_enable_shortcuts'; }
static get SCREENSHOT_BUTTON_POSITION() { return 'screenshot_button_position'; }
static get BLOCK_TRACKING() { return 'block_tracking'; }
static get BLOCK_SOCIAL_FEATURES() { return 'block_social_features'; }
@ -2761,6 +2971,9 @@ class Preferences {
[Preferences.STREAM_DISABLE_FEEDBACK_DIALOG]: {
'default': false,
},
[Preferences.CONTROLLER_ENABLE_SHORTCUTS]: {
'default': false,
},
[Preferences.REDUCE_ANIMATIONS]: {
'default': false,
},
@ -3797,6 +4010,35 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
font-family: var(--bx-monospaced-font);
}
.bx-toast {
position: fixed;
left: 50%;
top: 24px;
transform: translate(-50%, 0);
background: #000000cc;
border-radius: 40px;
padding: 8px 18px;
color: white;
z-index: 999;
font-family: var(--bx-normal-font);
border: 1px solid #fff;
}
.bx-toast-msg {
font-size: 12px;
display: inline-block;
vertical-align: middle;
}
.bx-toast-status {
font-weight: bold;
font-size: 18px;
text-transform: uppercase;
display: inline-block;
vertical-align: middle;
margin-left: 10px;
}
@media (hover: hover) {
.bx-quick-settings-bar button:hover {
background-color: #414141;
@ -4432,6 +4674,9 @@ function injectSettingsButton($parent) {
[Preferences.STREAM_HIDE_IDLE_CURSOR]: __('hide-idle-cursor'),
[Preferences.STREAM_DISABLE_FEEDBACK_DIALOG]: __('disable-post-stream-feedback-dialog'),
},
[__('controller')]: {
[Preferences.CONTROLLER_ENABLE_SHORTCUTS]: __('enable-controller-shortcuts'),
},
[__('touch-controller')]: {
[Preferences.STREAM_TOUCH_CONTROLLER]: __('tc-availability'),
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: __('tc-standard-layout-style'),
@ -4968,12 +5213,32 @@ function setupVideoSettingsBar() {
}
function takeScreenshot(callback) {
const $canvasContext = $SCREENSHOT_CANVAS.getContext('2d');
$canvasContext.drawImage($STREAM_VIDEO, 0, 0, $SCREENSHOT_CANVAS.width, $SCREENSHOT_CANVAS.height);
$SCREENSHOT_CANVAS.toBlob(blob => {
// Download screenshot
const now = +new Date;
const $anchor = createElement('a', {
'download': `${GAME_TITLE_ID}-${now}.png`,
'href': URL.createObjectURL(blob),
});
$anchor.click();
// Free screenshot from memory
URL.revokeObjectURL($anchor.href);
$canvasContext.clearRect(0, 0, $SCREENSHOT_CANVAS.width, $SCREENSHOT_CANVAS.height);
callback && callback();
}, 'image/png');
}
function setupScreenshotButton() {
$SCREENSHOT_CANVAS = createElement('canvas', {'class': 'bx-screenshot-canvas'});
document.documentElement.appendChild($SCREENSHOT_CANVAS);
const $canvasContext = $SCREENSHOT_CANVAS.getContext('2d');
const delay = 2000;
const $btn = createElement('div', {'class': 'bx-screenshot-button', 'data-showing': false});
@ -4990,20 +5255,7 @@ function setupScreenshotButton() {
timeout = null;
$btn.setAttribute('data-capturing', 'true');
$canvasContext.drawImage($STREAM_VIDEO, 0, 0, $SCREENSHOT_CANVAS.width, $SCREENSHOT_CANVAS.height);
$SCREENSHOT_CANVAS.toBlob(blob => {
// Download screenshot
const now = +new Date;
const $anchor = createElement('a', {
'download': `${GAME_TITLE_ID}-${now}.png`,
'href': URL.createObjectURL(blob),
});
$anchor.click();
// Free screenshot from memory
URL.revokeObjectURL($anchor.href);
$canvasContext.clearRect(0, 0, $SCREENSHOT_CANVAS.width, $SCREENSHOT_CANVAS.height);
takeScreenshot(() => {
// Hide button
$btn.setAttribute('data-showing', 'false');
setTimeout(() => {
@ -5011,7 +5263,7 @@ function setupScreenshotButton() {
$btn.setAttribute('data-capturing', 'false');
}
}, 100);
}, 'image/png');
});
return;
}
@ -5033,7 +5285,6 @@ function setupScreenshotButton() {
$btn.addEventListener('mousedown', detectDbClick);
document.documentElement.appendChild($btn);
}
@ -5050,6 +5301,8 @@ function patchHistoryMethod(type) {
function onHistoryChanged() {
IS_PLAYING = false;
const $settings = document.querySelector('.better_xcloud_settings');
if ($settings) {
$settings.classList.add('bx-gone');
@ -5070,11 +5323,15 @@ function onHistoryChanged() {
LoadingScreen.reset();
GamepadHandler.stopPolling();
setTimeout(checkHeader, 2000);
}
function onStreamStarted($video) {
IS_PLAYING = true;
// Get title ID for screenshot's name
GAME_TITLE_ID = /\/launch\/([^/]+)/.exec(window.location.pathname)[1];
@ -5082,6 +5339,10 @@ function onStreamStarted($video) {
TouchController.enableBar();
}
if (PREFS.get(Preferences.CONTROLLER_ENABLE_SHORTCUTS)) {
GamepadHandler.startPolling();
}
const PREF_SCREENSHOT_BUTTON_POSITION = PREFS.get(Preferences.SCREENSHOT_BUTTON_POSITION);
const PREF_STATS_QUICK_GLANCE = PREFS.get(Preferences.STATS_QUICK_GLANCE);
@ -5275,7 +5536,6 @@ window.RTCPeerConnection = function() {
return peer;
}
patchRtcCodecs();
interceptHttpRequests();
patchVideoApi();
@ -5284,9 +5544,14 @@ patchVideoApi();
addCss();
updateVideoPlayerCss();
window.addEventListener('resize', updateVideoPlayerCss);
Toast.setup();
setupVideoSettingsBar();
setupScreenshotButton();
StreamStats.render();
disablePwa();
if (PREFS.get(Preferences.CONTROLLER_ENABLE_SHORTCUTS)) {
GamepadHandler.initialSetup();
}