Support navigating Stream settings using left stick

This commit is contained in:
redphx 2024-07-17 17:47:23 +07:00
parent 0fb83de0ff
commit e73b4dfe78

View File

@ -14,9 +14,10 @@ import { StreamStats } from "./stream-stats";
import { BxSelectElement } from "@/web-components/bx-select"; import { BxSelectElement } from "@/web-components/bx-select";
import { onChangeVideoPlayerType, updateVideoPlayer } from "./stream-settings-utils"; import { onChangeVideoPlayerType, updateVideoPlayer } from "./stream-settings-utils";
import { GamepadKey } from "@/enums/mkb"; import { GamepadKey } from "@/enums/mkb";
import { EmulatedMkbHandler } from "../mkb/mkb-handler";
enum FocusDirection { enum NavigationDirection {
UP, UP = 1,
RIGHT, RIGHT,
DOWN, DOWN,
LEFT, LEFT,
@ -39,6 +40,8 @@ export class StreamSettings {
return StreamSettings.instance; return StreamSettings.instance;
} }
static readonly MAIN_CLASS = 'bx-stream-settings-dialog';
private static readonly GAMEPAD_POLLING_INTERVAL = 50; private static readonly GAMEPAD_POLLING_INTERVAL = 50;
private static readonly GAMEPAD_KEYS = [ private static readonly GAMEPAD_KEYS = [
GamepadKey.UP, GamepadKey.UP,
@ -51,6 +54,18 @@ export class StreamSettings {
GamepadKey.RB, GamepadKey.RB,
]; ];
private static readonly GAMEPAD_DIRECTION_MAP = {
[GamepadKey.UP]: NavigationDirection.UP,
[GamepadKey.DOWN]: NavigationDirection.DOWN,
[GamepadKey.LEFT]: NavigationDirection.LEFT,
[GamepadKey.RIGHT]: NavigationDirection.RIGHT,
[GamepadKey.LS_UP]: NavigationDirection.UP,
[GamepadKey.LS_DOWN]: NavigationDirection.DOWN,
[GamepadKey.LS_LEFT]: NavigationDirection.LEFT,
[GamepadKey.LS_RIGHT]: NavigationDirection.RIGHT,
};
private gamepadPollingIntervalId: number | null = null; private gamepadPollingIntervalId: number | null = null;
private gamepadLastButtons: Array<GamepadKey | null> = []; private gamepadLastButtons: Array<GamepadKey | null> = [];
@ -275,6 +290,10 @@ export class StreamSettings {
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, e => this.hide()); window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, e => this.hide());
} }
isShowing() {
return this.$container && !this.$container.classList.contains('bx-gone');
}
show(tabId?: string) { show(tabId?: string) {
const $container = this.$container!; const $container = this.$container!;
// Select tab // Select tab
@ -294,7 +313,7 @@ export class StreamSettings {
if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) { if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
// Focus the first visible setting // Focus the first visible setting
this.#focusDirection(FocusDirection.DOWN); this.#focusDirection(NavigationDirection.DOWN);
// Add event listeners // Add event listeners
$container.addEventListener('keydown', this); $container.addEventListener('keydown', this);
@ -329,61 +348,99 @@ export class StreamSettings {
BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_DISMISSED); BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_DISMISSED);
} }
#focusCurrentTab() {
const $currentTab = this.$tabs!.querySelector('.bx-active') as HTMLElement;
$currentTab && $currentTab.focus();
}
#pollGamepad() { #pollGamepad() {
const gamepads = window.navigator.getGamepads(); const gamepads = window.navigator.getGamepads();
let direction: FocusDirection | null = null; let direction: NavigationDirection | null = null;
for (const gamepad of gamepads) { for (const gamepad of gamepads) {
if (!gamepad || !gamepad.connected) { if (!gamepad || !gamepad.connected) {
continue; continue;
} }
// Ignore virtual controller
if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) {
continue;
}
const axes = gamepad.axes;
const buttons = gamepad.buttons; const buttons = gamepad.buttons;
let lastButton = this.gamepadLastButtons[gamepad.index]; let lastButton = this.gamepadLastButtons[gamepad.index];
let pressedButton: GamepadKey | undefined = undefined; let pressedButton: GamepadKey | null = null;
let holdingButton: GamepadKey | null = null;
for (const key of StreamSettings.GAMEPAD_KEYS) { for (const key of StreamSettings.GAMEPAD_KEYS) {
if (typeof lastButton === 'number') { if (typeof lastButton === 'number') {
// Key pressed // Key released
if (lastButton === key && !buttons[key].pressed) { if (lastButton === key && !buttons[key].pressed) {
pressedButton = key; pressedButton = key;
break; break;
} }
} else if (buttons[key].pressed) { } else if (buttons[key].pressed) {
this.gamepadLastButtons[gamepad.index] = key; // Key pressed
holdingButton = key;
break; break;
} }
} }
if (typeof pressedButton !== 'undefined') { if (holdingButton === null && pressedButton === null && axes && axes.length >= 2) {
// Check sticks
// LEFT left-right, LEFT up-down
if (typeof lastButton === 'number') {
const releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastButton === GamepadKey.LS_LEFT || lastButton === GamepadKey.LS_RIGHT);
const releasedVertical = Math.abs(axes[1]) < 0.1 && (lastButton === GamepadKey.LS_UP || lastButton === GamepadKey.LS_DOWN);
if (releasedHorizontal || releasedVertical) {
pressedButton = lastButton;
}
} else {
if (axes[0] < -0.5) {
holdingButton = GamepadKey.LS_LEFT;
} else if (axes[0] > 0.5) {
holdingButton = GamepadKey.LS_RIGHT;
} else if (axes[1] < -0.5) {
holdingButton = GamepadKey.LS_UP;
} else if (axes[1] > 0.5) {
holdingButton = GamepadKey.LS_DOWN;
}
}
}
if (holdingButton !== null) {
this.gamepadLastButtons[gamepad.index] = holdingButton;
}
if (pressedButton === null) {
continue;
}
this.gamepadLastButtons[gamepad.index] = null; this.gamepadLastButtons[gamepad.index] = null;
if (pressedButton === GamepadKey.A) { if (pressedButton === GamepadKey.A) {
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click')); document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click'));
return;
} else if (pressedButton === GamepadKey.B) { } else if (pressedButton === GamepadKey.B) {
this.hide(); this.hide();
return;
} else if (pressedButton === GamepadKey.LB || pressedButton === GamepadKey.RB) { } else if (pressedButton === GamepadKey.LB || pressedButton === GamepadKey.RB) {
// Focus setting tabs // Focus setting tabs
const $currentTab = this.$tabs!.querySelector('.bx-active') as HTMLElement; this.#focusCurrentTab();
$currentTab && $currentTab.focus(); return;
} }
if (pressedButton === GamepadKey.UP) { direction = StreamSettings.GAMEPAD_DIRECTION_MAP[pressedButton as keyof typeof StreamSettings.GAMEPAD_DIRECTION_MAP];
direction = FocusDirection.UP; if (direction) {
} else if (pressedButton === GamepadKey.DOWN) {
direction = FocusDirection.DOWN;
} else if (pressedButton === GamepadKey.LEFT) {
direction = FocusDirection.LEFT;
} else if (pressedButton === GamepadKey.RIGHT) {
direction = FocusDirection.RIGHT;
}
if (direction !== null) {
let handled = false; let handled = false;
if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === 'range') { if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === 'range') {
const $range = document.activeElement; const $range = document.activeElement;
if (direction === FocusDirection.LEFT || direction === FocusDirection.RIGHT) { if (direction === NavigationDirection.LEFT || direction === NavigationDirection.RIGHT) {
$range.value = (parseInt($range.value) + parseInt($range.step) * (direction === FocusDirection.LEFT ? -1 : 1)).toString(); $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === NavigationDirection.LEFT ? -1 : 1)).toString();
$range.dispatchEvent(new InputEvent('input')); $range.dispatchEvent(new InputEvent('input'));
handled = true; handled = true;
} }
@ -397,7 +454,6 @@ export class StreamSettings {
return; return;
} }
} }
}
#startGamepadPolling() { #startGamepadPolling() {
this.#stopGamepadPolling(); this.#stopGamepadPolling();
@ -412,22 +468,22 @@ export class StreamSettings {
this.gamepadPollingIntervalId = null; this.gamepadPollingIntervalId = null;
} }
#handleTabsNavigation($focusing: HTMLElement, direction: FocusDirection) { #handleTabsNavigation($focusing: HTMLElement, direction: NavigationDirection) {
if (direction === FocusDirection.UP || direction === FocusDirection.DOWN) { if (direction === NavigationDirection.UP || direction === NavigationDirection.DOWN) {
let $sibling = $focusing; let $sibling = $focusing;
const siblingProperty = direction === FocusDirection.UP ? 'previousElementSibling' : 'nextElementSibling'; const siblingProperty = direction === NavigationDirection.UP ? 'previousElementSibling' : 'nextElementSibling';
while ($sibling[siblingProperty]) { while ($sibling[siblingProperty]) {
$sibling = $sibling[siblingProperty] as HTMLElement; $sibling = $sibling[siblingProperty] as HTMLElement;
$sibling && $sibling.focus(); $sibling && $sibling.focus();
return; return;
} }
} else if (direction === FocusDirection.RIGHT) { } else if (direction === NavigationDirection.RIGHT) {
this.#focusFirstVisibleSetting(); this.#focusFirstVisibleSetting();
} }
} }
#handleSettingsNavigation($focusing: HTMLElement, direction: FocusDirection) { #handleSettingsNavigation($focusing: HTMLElement, direction: NavigationDirection) {
// If current element's tabIndex property is not 0 // If current element's tabIndex property is not 0
if ($focusing.tabIndex !== 0) { if ($focusing.tabIndex !== 0) {
// Find first visible setting // Find first visible setting
@ -448,8 +504,8 @@ export class StreamSettings {
// Find sibling setting // Find sibling setting
let $sibling = $parent; let $sibling = $parent;
if (direction === FocusDirection.UP || direction === FocusDirection.DOWN) { if (direction === NavigationDirection.UP || direction === NavigationDirection.DOWN) {
const siblingProperty = direction === FocusDirection.UP ? 'previousElementSibling' : 'nextElementSibling'; const siblingProperty = direction === NavigationDirection.UP ? 'previousElementSibling' : 'nextElementSibling';
while ($sibling[siblingProperty]) { while ($sibling[siblingProperty]) {
$sibling = $sibling[siblingProperty]; $sibling = $sibling[siblingProperty];
@ -459,12 +515,12 @@ export class StreamSettings {
return; return;
} }
} }
} else if (direction === FocusDirection.LEFT || direction === FocusDirection.RIGHT) { } else if (direction === NavigationDirection.LEFT || direction === NavigationDirection.RIGHT) {
// Find all child elements with tabindex // Find all child elements with tabindex
const children = Array.from($parent.querySelectorAll('[tabindex="0"]')); const children = Array.from($parent.querySelectorAll('[tabindex="0"]'));
const index = children.indexOf($focusing); const index = children.indexOf($focusing);
let nextIndex; let nextIndex;
if (direction === FocusDirection.LEFT) { if (direction === NavigationDirection.LEFT) {
nextIndex = index - 1; nextIndex = index - 1;
} else { } else {
nextIndex = index + 1; nextIndex = index + 1;
@ -497,7 +553,7 @@ export class StreamSettings {
} }
} }
#focusDirection(direction: FocusDirection) { #focusDirection(direction: NavigationDirection) {
const $tabs = this.$tabs!; const $tabs = this.$tabs!;
const $settings = this.$settings!; const $settings = this.$settings!;
@ -531,28 +587,34 @@ export class StreamSettings {
const keyboardEvent = event as KeyboardEvent; const keyboardEvent = event as KeyboardEvent;
const keyCode = keyboardEvent.code || keyboardEvent.key; const keyCode = keyboardEvent.code || keyboardEvent.key;
if (keyCode === 'ArrowUp' || keyCode === 'ArrowDown') { let handled = false;
event.preventDefault();
event.stopPropagation();
this.#focusDirection(keyCode === 'ArrowUp' ? FocusDirection.UP : FocusDirection.DOWN); if (keyCode === 'ArrowUp' || keyCode === 'ArrowDown') {
handled = true;
this.#focusDirection(keyCode === 'ArrowUp' ? NavigationDirection.UP : NavigationDirection.DOWN);
} else if (keyCode === 'ArrowLeft' || keyCode === 'ArrowRight') { } else if (keyCode === 'ArrowLeft' || keyCode === 'ArrowRight') {
if (($target as any).type !== 'range') { if (($target as any).type !== 'range') {
event.preventDefault(); handled = true;
event.stopPropagation(); this.#focusDirection(keyCode === 'ArrowLeft' ? NavigationDirection.LEFT : NavigationDirection.RIGHT);
this.#focusDirection(keyCode === 'ArrowLeft' ? FocusDirection.LEFT : FocusDirection.RIGHT);
} }
} else if (keyCode === 'Enter' || keyCode === 'Space') { } else if (keyCode === 'Enter' || keyCode === 'Space') {
if ($target instanceof SVGElement) { if ($target instanceof SVGElement) {
event.preventDefault(); handled = true;
event.stopPropagation();
$target.dispatchEvent(new Event('click')); $target.dispatchEvent(new Event('click'));
} }
} else if (keyCode === 'Tab') {
handled = true;
this.#focusCurrentTab();
} else if (keyCode === 'Escape') { } else if (keyCode === 'Escape') {
handled = true;
this.hide(); this.hide();
} }
if (handled) {
event.preventDefault();
event.stopPropagation();
}
break; break;
} }
} }
@ -564,7 +626,7 @@ export class StreamSettings {
const $overlay = CE('div', {class: 'bx-stream-settings-overlay bx-gone'}); const $overlay = CE('div', {class: 'bx-stream-settings-overlay bx-gone'});
this.$overlay = $overlay; this.$overlay = $overlay;
const $container = CE('div', {class: 'bx-stream-settings-dialog bx-gone'}, const $container = CE('div', {class: StreamSettings.MAIN_CLASS + ' bx-gone'},
$tabs = CE('div', {class: 'bx-stream-settings-tabs'}), $tabs = CE('div', {class: 'bx-stream-settings-tabs'}),
$settings = CE('div', { $settings = CE('div', {
class: 'bx-stream-settings-tab-contents', class: 'bx-stream-settings-tab-contents',