mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-10 23:27:46 +02:00
6.0
This commit is contained in:
396
src/modules/controller-shortcut.ts
Normal file → Executable file
396
src/modules/controller-shortcut.ts
Normal file → Executable file
@@ -1,70 +1,29 @@
|
||||
import { ScreenshotManager } from "@/utils/screenshot-manager";
|
||||
import { GamepadKey } from "@enums/mkb";
|
||||
import { PrompFont } from "@enums/prompt-font";
|
||||
import { CE, removeChildElements } from "@utils/html";
|
||||
import { t } from "@utils/translation";
|
||||
import { StreamStats } from "./stream/stream-stats";
|
||||
import { MicrophoneShortcut } from "./shortcuts/shortcut-microphone";
|
||||
import { StreamUiShortcut } from "./shortcuts/shortcut-stream-ui";
|
||||
import { SoundShortcut } from "./shortcuts/shortcut-sound";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { AppInterface } from "@/utils/global";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import { setNearby } from "@/utils/navigation-utils";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { SettingsNavigationDialog } from "./ui/dialog/settings-dialog";
|
||||
import { VIRTUAL_GAMEPAD_ID } from "./mkb/mkb-handler";
|
||||
import { GamepadKey } from "@enums/gamepad";
|
||||
import { ShortcutHandler } from "@/utils/shortcut-handler";
|
||||
|
||||
const enum ShortcutAction {
|
||||
BETTER_XCLOUD_SETTINGS_SHOW = 'bx-settings-show',
|
||||
|
||||
STREAM_SCREENSHOT_CAPTURE = 'stream-screenshot-capture',
|
||||
|
||||
STREAM_MENU_SHOW = 'stream-menu-show',
|
||||
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_SOUND_TOGGLE = 'device-sound-toggle',
|
||||
DEVICE_VOLUME_INC = 'device-volume-inc',
|
||||
DEVICE_VOLUME_DEC = 'device-volume-dec',
|
||||
|
||||
DEVICE_BRIGHTNESS_INC = 'device-brightness-inc',
|
||||
DEVICE_BRIGHTNESS_DEC = 'device-brightness-dec',
|
||||
}
|
||||
|
||||
export class ControllerShortcut {
|
||||
private static readonly STORAGE_KEY = 'better_xcloud_controller_shortcuts';
|
||||
|
||||
private static buttonsCache: {[key: string]: boolean[]} = {};
|
||||
private static buttonsStatus: {[key: string]: boolean[]} = {};
|
||||
|
||||
private static $selectProfile: HTMLSelectElement;
|
||||
private static $selectActions: Partial<{[key in GamepadKey]: HTMLSelectElement}> = {};
|
||||
private static $container: HTMLElement;
|
||||
|
||||
private static ACTIONS: {[key: string]: (ShortcutAction | null)[]} | null = null;
|
||||
|
||||
static reset(index: number) {
|
||||
ControllerShortcut.buttonsCache[index] = [];
|
||||
ControllerShortcut.buttonsStatus[index] = [];
|
||||
}
|
||||
|
||||
static handle(gamepad: Gamepad): boolean {
|
||||
if (!ControllerShortcut.ACTIONS) {
|
||||
ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage();
|
||||
const controllerSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id];
|
||||
if (!controllerSettings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const gamepadIndex = gamepad.index;
|
||||
const actions = ControllerShortcut.ACTIONS![gamepad.id];
|
||||
const actions = controllerSettings.shortcuts;
|
||||
if (!actions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const gamepadIndex = gamepad.index;
|
||||
|
||||
// Move the buttons status from the previous frame to the cache
|
||||
ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0);
|
||||
// Clear the buttons status
|
||||
@@ -74,7 +33,9 @@ export class ControllerShortcut {
|
||||
let otherButtonPressed = false;
|
||||
|
||||
const entries = gamepad.buttons.entries();
|
||||
for (const [index, button] of entries) {
|
||||
let index: GamepadKey;
|
||||
let button: GamepadButton;
|
||||
for ([index, button] of entries) {
|
||||
// Only add the newly pressed button to the array (holding doesn't count)
|
||||
if (button.pressed && index !== GamepadKey.HOME) {
|
||||
otherButtonPressed = true;
|
||||
@@ -82,7 +43,8 @@ export class ControllerShortcut {
|
||||
|
||||
// If this is newly pressed button -> run action
|
||||
if (actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) {
|
||||
setTimeout(() => ControllerShortcut.runAction(actions[index]!), 0);
|
||||
const idx = index;
|
||||
setTimeout(() => ShortcutHandler.runAction(actions[idx]!), 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -90,336 +52,4 @@ export class ControllerShortcut {
|
||||
ControllerShortcut.buttonsStatus[gamepadIndex] = pressed;
|
||||
return otherButtonPressed;
|
||||
}
|
||||
|
||||
private static runAction(action: ShortcutAction) {
|
||||
switch (action) {
|
||||
case ShortcutAction.BETTER_XCLOUD_SETTINGS_SHOW:
|
||||
SettingsNavigationDialog.getInstance().show();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_SCREENSHOT_CAPTURE:
|
||||
ScreenshotManager.getInstance().takeScreenshot();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_STATS_TOGGLE:
|
||||
StreamStats.getInstance().toggle();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_MICROPHONE_TOGGLE:
|
||||
MicrophoneShortcut.toggle();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_MENU_SHOW:
|
||||
StreamUiShortcut.showHideStreamMenu();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_SOUND_TOGGLE:
|
||||
SoundShortcut.muteUnmute();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_VOLUME_INC:
|
||||
SoundShortcut.adjustGainNodeVolume(10);
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_VOLUME_DEC:
|
||||
SoundShortcut.adjustGainNodeVolume(-10);
|
||||
break;
|
||||
|
||||
case ShortcutAction.DEVICE_BRIGHTNESS_INC:
|
||||
case ShortcutAction.DEVICE_BRIGHTNESS_DEC:
|
||||
case ShortcutAction.DEVICE_SOUND_TOGGLE:
|
||||
case ShortcutAction.DEVICE_VOLUME_INC:
|
||||
case ShortcutAction.DEVICE_VOLUME_DEC:
|
||||
AppInterface && AppInterface.runShortcut && AppInterface.runShortcut(action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static updateAction(profile: string, button: GamepadKey, action: ShortcutAction | null) {
|
||||
const actions = ControllerShortcut.ACTIONS!;
|
||||
if (!(profile in actions)) {
|
||||
actions[profile] = [];
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
action = null;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
private static updateProfileList(e?: GamepadEvent) {
|
||||
const $select = ControllerShortcut.$selectProfile;
|
||||
const $container = ControllerShortcut.$container;
|
||||
|
||||
const $fragment = document.createDocumentFragment();
|
||||
|
||||
// Remove old profiles
|
||||
removeChildElements($select);
|
||||
|
||||
const gamepads = navigator.getGamepads();
|
||||
let hasGamepad = false;
|
||||
|
||||
for (const gamepad of gamepads) {
|
||||
if (!gamepad || !gamepad.connected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore emulated gamepad
|
||||
if (gamepad.id === VIRTUAL_GAMEPAD_ID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
hasGamepad = true;
|
||||
|
||||
const $option = CE<HTMLOptionElement>('option', {value: gamepad.id}, gamepad.id);
|
||||
$fragment.appendChild($option);
|
||||
}
|
||||
|
||||
$container.dataset.hasGamepad = hasGamepad.toString();
|
||||
if (hasGamepad) {
|
||||
$select.appendChild($fragment);
|
||||
|
||||
$select.selectedIndex = 0;
|
||||
$select.dispatchEvent(new Event('input'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private 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] || '';
|
||||
|
||||
BxEvent.dispatch($select, 'input', {
|
||||
ignoreOnChange: true,
|
||||
manualTrigger: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static getActionsFromStorage() {
|
||||
return JSON.parse(window.localStorage.getItem(ControllerShortcut.STORAGE_KEY) || '{}');
|
||||
}
|
||||
|
||||
static renderSettings() {
|
||||
const PREF_CONTROLLER_FRIENDLY_UI = getPref(PrefKey.UI_CONTROLLER_FRIENDLY);
|
||||
|
||||
// Read actions from localStorage
|
||||
ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage();
|
||||
|
||||
const buttons: Map<GamepadKey, PrompFont> = new Map();
|
||||
buttons.set(GamepadKey.Y, PrompFont.Y);
|
||||
buttons.set(GamepadKey.A, PrompFont.A);
|
||||
buttons.set(GamepadKey.B, PrompFont.B);
|
||||
buttons.set(GamepadKey.X, PrompFont.X);
|
||||
|
||||
buttons.set(GamepadKey.UP, PrompFont.UP);
|
||||
buttons.set(GamepadKey.DOWN, PrompFont.DOWN);
|
||||
buttons.set(GamepadKey.LEFT, PrompFont.LEFT);
|
||||
buttons.set(GamepadKey.RIGHT, PrompFont.RIGHT);
|
||||
|
||||
buttons.set(GamepadKey.SELECT, PrompFont.SELECT);
|
||||
buttons.set(GamepadKey.START, PrompFont.START);
|
||||
|
||||
buttons.set(GamepadKey.LB, PrompFont.LB);
|
||||
buttons.set(GamepadKey.RB, PrompFont.RB);
|
||||
|
||||
buttons.set(GamepadKey.LT, PrompFont.LT);
|
||||
buttons.set(GamepadKey.RT, PrompFont.RT);
|
||||
|
||||
buttons.set(GamepadKey.L3, PrompFont.L3);
|
||||
buttons.set(GamepadKey.R3, PrompFont.R3);
|
||||
|
||||
const actions: {[key: string]: Partial<{[key in ShortcutAction]: string | string[]}>} = {
|
||||
[t('better-xcloud')]: {
|
||||
[ShortcutAction.BETTER_XCLOUD_SETTINGS_SHOW]: [t('settings'), t('show')],
|
||||
},
|
||||
|
||||
[t('device')]: AppInterface && {
|
||||
[ShortcutAction.DEVICE_SOUND_TOGGLE]: [t('sound'), t('toggle')],
|
||||
[ShortcutAction.DEVICE_VOLUME_INC]: [t('volume'), t('increase')],
|
||||
[ShortcutAction.DEVICE_VOLUME_DEC]: [t('volume'), t('decrease')],
|
||||
|
||||
[ShortcutAction.DEVICE_BRIGHTNESS_INC]: [t('brightness'), t('increase')],
|
||||
[ShortcutAction.DEVICE_BRIGHTNESS_DEC]: [t('brightness'), t('decrease')],
|
||||
},
|
||||
|
||||
[t('stream')]: {
|
||||
[ShortcutAction.STREAM_SCREENSHOT_CAPTURE]: t('take-screenshot'),
|
||||
|
||||
[ShortcutAction.STREAM_SOUND_TOGGLE]: [t('sound'), t('toggle')],
|
||||
[ShortcutAction.STREAM_VOLUME_INC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('volume'), t('increase')],
|
||||
[ShortcutAction.STREAM_VOLUME_DEC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('volume'), t('decrease')],
|
||||
|
||||
[ShortcutAction.STREAM_MENU_SHOW]: [t('menu'), t('show')],
|
||||
[ShortcutAction.STREAM_STATS_TOGGLE]: [t('stats'), t('show-hide')],
|
||||
[ShortcutAction.STREAM_MICROPHONE_TOGGLE]: [t('microphone'), t('toggle')],
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
let $remap: HTMLElement;
|
||||
const $selectProfile = CE<HTMLSelectElement>('select', {class: 'bx-shortcut-profile', autocomplete: 'off'});
|
||||
|
||||
const $profile = PREF_CONTROLLER_FRIENDLY_UI ? BxSelectElement.wrap($selectProfile) : $selectProfile;
|
||||
$profile.classList.add('bx-full-width');
|
||||
|
||||
const $container = CE('div', {
|
||||
'data-has-gamepad': 'false',
|
||||
_nearby: {
|
||||
focus: $profile,
|
||||
},
|
||||
},
|
||||
CE('div', {},
|
||||
CE('p', {class: 'bx-shortcut-note'}, t('controller-shortcuts-connect-note')),
|
||||
),
|
||||
|
||||
$remap = CE('div', {},
|
||||
CE('div', {
|
||||
_nearby: {
|
||||
focus: $profile,
|
||||
},
|
||||
}, $profile),
|
||||
CE('p', {class: 'bx-shortcut-note'},
|
||||
CE('span', {class: 'bx-prompt'}, PrompFont.HOME),
|
||||
': ' + t('controller-shortcuts-xbox-note'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$selectProfile.addEventListener('input', e => {
|
||||
ControllerShortcut.switchProfile($selectProfile.value);
|
||||
});
|
||||
|
||||
const onActionChanged = (e: Event) => {
|
||||
const $target = e.target as HTMLSelectElement;
|
||||
|
||||
const profile = $selectProfile.value;
|
||||
const button: unknown = $target.dataset.button;
|
||||
const action = $target.value as ShortcutAction;
|
||||
|
||||
if (!PREF_CONTROLLER_FRIENDLY_UI) {
|
||||
const $fakeSelect = $target.previousElementSibling! as HTMLSelectElement;
|
||||
let fakeText = '---';
|
||||
if (action) {
|
||||
const $selectedOption = $target.options[$target.selectedIndex];
|
||||
const $optGroup = $selectedOption.parentElement as HTMLOptGroupElement;
|
||||
fakeText = $optGroup.label + ' ❯ ' + $selectedOption.text;
|
||||
}
|
||||
($fakeSelect.firstElementChild as HTMLOptionElement).text = fakeText;
|
||||
}
|
||||
|
||||
!(e as any).ignoreOnChange && ControllerShortcut.updateAction(profile, button as GamepadKey, action);
|
||||
};
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
for (const [button, prompt] of buttons) {
|
||||
const $row = CE('div', {
|
||||
class: 'bx-shortcut-row',
|
||||
});
|
||||
|
||||
const $label = CE('label', {class: 'bx-prompt'}, `${PrompFont.HOME} + ${prompt}`);
|
||||
|
||||
const $div = CE('div', {class: 'bx-shortcut-actions'});
|
||||
|
||||
if (!PREF_CONTROLLER_FRIENDLY_UI) {
|
||||
const $fakeSelect = CE<HTMLSelectElement>('select', {autocomplete: 'off'},
|
||||
CE('option', {}, '---'),
|
||||
);
|
||||
|
||||
$div.appendChild($fakeSelect);
|
||||
}
|
||||
|
||||
const $select = $baseSelect.cloneNode(true) as HTMLSelectElement;
|
||||
$select.dataset.button = button.toString();
|
||||
$select.addEventListener('input', onActionChanged);
|
||||
|
||||
ControllerShortcut.$selectActions[button] = $select;
|
||||
|
||||
if (PREF_CONTROLLER_FRIENDLY_UI) {
|
||||
const $bxSelect = BxSelectElement.wrap($select);
|
||||
$bxSelect.classList.add('bx-full-width');
|
||||
|
||||
$div.appendChild($bxSelect);
|
||||
setNearby($row, {
|
||||
focus: $bxSelect,
|
||||
});
|
||||
} else {
|
||||
$div.appendChild($select);
|
||||
setNearby($row, {
|
||||
focus: $select,
|
||||
});
|
||||
}
|
||||
|
||||
$row.appendChild($label);
|
||||
$row.appendChild($div);
|
||||
|
||||
$remap.appendChild($row);
|
||||
}
|
||||
|
||||
$container.appendChild($remap);
|
||||
|
||||
ControllerShortcut.$selectProfile = $selectProfile;
|
||||
ControllerShortcut.$container = $container;
|
||||
|
||||
// Detect when gamepad connected/disconnect
|
||||
window.addEventListener('gamepadconnected', ControllerShortcut.updateProfileList);
|
||||
window.addEventListener('gamepaddisconnected', ControllerShortcut.updateProfileList);
|
||||
|
||||
ControllerShortcut.updateProfileList();
|
||||
|
||||
return $container;
|
||||
}
|
||||
}
|
||||
|
145
src/modules/device-vibration-manager.ts
Executable file
145
src/modules/device-vibration-manager.ts
Executable file
@@ -0,0 +1,145 @@
|
||||
import { AppInterface, STATES } from "@utils/global";
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
|
||||
const VIBRATION_DATA_MAP = {
|
||||
gamepadIndex: 8,
|
||||
leftMotorPercent: 8,
|
||||
rightMotorPercent: 8,
|
||||
leftTriggerMotorPercent: 8,
|
||||
rightTriggerMotorPercent: 8,
|
||||
durationMs: 16,
|
||||
// delayMs: 16,
|
||||
// repeat: 8,
|
||||
};
|
||||
|
||||
type VibrationData = {
|
||||
[key in keyof typeof VIBRATION_DATA_MAP]?: number;
|
||||
}
|
||||
|
||||
export class DeviceVibrationManager {
|
||||
private static instance: DeviceVibrationManager | null | undefined;
|
||||
public static getInstance(): typeof DeviceVibrationManager['instance'] {
|
||||
if (typeof DeviceVibrationManager.instance === 'undefined') {
|
||||
if (STATES.browser.capabilities.deviceVibration) {
|
||||
DeviceVibrationManager.instance = new DeviceVibrationManager();
|
||||
} else {
|
||||
DeviceVibrationManager.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
return DeviceVibrationManager.instance;
|
||||
}
|
||||
|
||||
private dataChannel: RTCDataChannel | null = null;
|
||||
private boundOnMessage: (e: MessageEvent) => void;
|
||||
|
||||
constructor() {
|
||||
this.boundOnMessage = this.onMessage.bind(this);
|
||||
|
||||
window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => {
|
||||
const dataChannel = (e as any).dataChannel as RTCDataChannel;
|
||||
if (dataChannel?.label === 'input') {
|
||||
this.reset();
|
||||
|
||||
this.dataChannel = dataChannel;
|
||||
this.setupDataChannel();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener(BxEvent.DEVICE_VIBRATION_CHANGED, e => {
|
||||
this.setupDataChannel();
|
||||
});
|
||||
}
|
||||
|
||||
private setupDataChannel() {
|
||||
if (!this.dataChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeEventListeners();
|
||||
|
||||
if (window.BX_STREAM_SETTINGS.deviceVibrationIntensity > 0) {
|
||||
this.dataChannel.addEventListener('message', this.boundOnMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private playVibration(data: Required<VibrationData>) {
|
||||
const vibrationIntensity = StreamSettings.settings.deviceVibrationIntensity;
|
||||
if (AppInterface) {
|
||||
AppInterface.vibrate(JSON.stringify(data), vibrationIntensity);
|
||||
return;
|
||||
}
|
||||
|
||||
const realIntensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * vibrationIntensity;
|
||||
if (realIntensity === 0 || realIntensity === 100) {
|
||||
// Stop vibration
|
||||
window.navigator.vibrate(realIntensity ? data.durationMs : 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const pulseDuration = 200;
|
||||
const onDuration = Math.floor(pulseDuration * realIntensity / 100);
|
||||
const offDuration = pulseDuration - onDuration;
|
||||
|
||||
const repeats = Math.ceil(data.durationMs / pulseDuration);
|
||||
const pulses = Array(repeats).fill([onDuration, offDuration]).flat();
|
||||
|
||||
window.navigator.vibrate(pulses);
|
||||
}
|
||||
|
||||
onMessage(e: MessageEvent) {
|
||||
if (typeof e !== 'object' || !(e.data instanceof ArrayBuffer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataView = new DataView(e.data);
|
||||
let offset = 0;
|
||||
|
||||
let messageType;
|
||||
if (dataView.byteLength === 13) { // version >= 8
|
||||
messageType = dataView.getUint16(offset, true);
|
||||
offset += Uint16Array.BYTES_PER_ELEMENT;
|
||||
} else {
|
||||
messageType = dataView.getUint8(offset);
|
||||
offset += Uint8Array.BYTES_PER_ELEMENT;
|
||||
}
|
||||
|
||||
if (!(messageType & 128)) { // Vibration
|
||||
return;
|
||||
}
|
||||
|
||||
const vibrationType = dataView.getUint8(offset);
|
||||
offset += Uint8Array.BYTES_PER_ELEMENT;
|
||||
|
||||
if (vibrationType !== 0) { // FourMotorRumble
|
||||
return;
|
||||
}
|
||||
|
||||
const data: VibrationData = {};
|
||||
let key: keyof typeof VIBRATION_DATA_MAP;
|
||||
for (key in VIBRATION_DATA_MAP) {
|
||||
if (VIBRATION_DATA_MAP[key] === 16) {
|
||||
data[key] = dataView.getUint16(offset, true);
|
||||
offset += Uint16Array.BYTES_PER_ELEMENT;
|
||||
} else {
|
||||
data[key] = dataView.getUint8(offset);
|
||||
offset += Uint8Array.BYTES_PER_ELEMENT;
|
||||
}
|
||||
}
|
||||
|
||||
this.playVibration(data as Required<VibrationData>);
|
||||
}
|
||||
|
||||
private removeEventListeners() {
|
||||
// Clear event listeners in previous DataChannel
|
||||
try {
|
||||
this.dataChannel?.removeEventListener('message', this.boundOnMessage);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.removeEventListeners();
|
||||
this.dataChannel = null;
|
||||
}
|
||||
}
|
@@ -1,102 +0,0 @@
|
||||
import { t } from "@utils/translation";
|
||||
import { CE, createButton, ButtonStyle } from "@utils/html";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
|
||||
type DialogOptions = Partial<{
|
||||
title: string;
|
||||
className: string;
|
||||
content: string | HTMLElement;
|
||||
hideCloseButton: boolean;
|
||||
onClose: string;
|
||||
helpUrl: string;
|
||||
}>;
|
||||
|
||||
export class Dialog {
|
||||
$dialog: HTMLElement;
|
||||
$title: HTMLElement;
|
||||
$content: HTMLElement;
|
||||
$overlay: HTMLElement;
|
||||
|
||||
onClose: any;
|
||||
|
||||
constructor(options: DialogOptions) {
|
||||
const {
|
||||
title,
|
||||
className,
|
||||
content,
|
||||
hideCloseButton,
|
||||
onClose,
|
||||
helpUrl,
|
||||
} = options;
|
||||
|
||||
// Create dialog overlay
|
||||
const $overlay = document.querySelector<HTMLElement>('.bx-dialog-overlay');
|
||||
|
||||
if (!$overlay) {
|
||||
this.$overlay = CE('div', {'class': 'bx-dialog-overlay bx-gone'});
|
||||
|
||||
// Disable right click
|
||||
this.$overlay.addEventListener('contextmenu', e => e.preventDefault());
|
||||
|
||||
document.documentElement.appendChild(this.$overlay);
|
||||
} else {
|
||||
this.$overlay = $overlay;
|
||||
}
|
||||
|
||||
let $close;
|
||||
this.onClose = onClose;
|
||||
this.$dialog = CE('div', {'class': `bx-dialog ${className || ''} bx-gone`},
|
||||
this.$title = CE('h2', {}, CE('b', {}, title),
|
||||
helpUrl && createButton({
|
||||
icon: BxIcon.QUESTION,
|
||||
style: ButtonStyle.GHOST,
|
||||
title: t('help'),
|
||||
url: helpUrl,
|
||||
}),
|
||||
),
|
||||
this.$content = CE('div', {'class': 'bx-dialog-content'}, content),
|
||||
!hideCloseButton && ($close = CE('button', {type: 'button'}, t('close'))),
|
||||
);
|
||||
|
||||
$close && $close.addEventListener('click', e => {
|
||||
this.hide(e);
|
||||
});
|
||||
|
||||
!title && this.$title.classList.add('bx-gone');
|
||||
!content && this.$content.classList.add('bx-gone');
|
||||
|
||||
// Disable right click
|
||||
this.$dialog.addEventListener('contextmenu', e => e.preventDefault());
|
||||
|
||||
document.documentElement.appendChild(this.$dialog);
|
||||
}
|
||||
|
||||
show(newOptions: DialogOptions) {
|
||||
// Clear focus
|
||||
document.activeElement && (document.activeElement as HTMLElement).blur();
|
||||
|
||||
if (newOptions && newOptions.title) {
|
||||
this.$title.querySelector('b')!.textContent = newOptions.title;
|
||||
this.$title.classList.remove('bx-gone');
|
||||
}
|
||||
|
||||
this.$dialog.classList.remove('bx-gone');
|
||||
this.$overlay.classList.remove('bx-gone');
|
||||
|
||||
document.body.classList.add('bx-no-scroll');
|
||||
}
|
||||
|
||||
hide(e?: any) {
|
||||
this.$dialog.classList.add('bx-gone');
|
||||
this.$overlay.classList.add('bx-gone');
|
||||
|
||||
document.body.classList.remove('bx-no-scroll');
|
||||
|
||||
this.onClose && this.onClose(e);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.$dialog.classList.toggle('bx-gone');
|
||||
this.$overlay.classList.toggle('bx-gone');
|
||||
}
|
||||
}
|
0
src/modules/game-bar/action-base.ts → src/modules/game-bar/base-action.ts
Normal file → Executable file
0
src/modules/game-bar/action-base.ts → src/modules/game-bar/base-action.ts
Normal file → Executable file
53
src/modules/game-bar/game-bar.ts
Normal file → Executable file
53
src/modules/game-bar/game-bar.ts
Normal file → Executable file
@@ -1,22 +1,34 @@
|
||||
import { CE, createSvgIcon } from "@utils/html";
|
||||
import { ScreenshotAction } from "./action-screenshot";
|
||||
import { TouchControlAction } from "./action-touch-control";
|
||||
import { ScreenshotAction } from "./screenshot-action";
|
||||
import { TouchControlAction } from "./touch-control-action";
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import type { BaseGameBarAction } from "./action-base";
|
||||
import type { BaseGameBarAction } from "./base-action";
|
||||
import { STATES } from "@utils/global";
|
||||
import { MicrophoneAction } from "./action-microphone";
|
||||
import { MicrophoneAction } from "./microphone-action";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, StreamTouchController, type GameBarPosition } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { TrueAchievementsAction } from "./action-true-achievements";
|
||||
import { SpeakerAction } from "./action-speaker";
|
||||
import { RendererAction } from "./action-renderer";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { TrueAchievementsAction } from "./true-achievements-action";
|
||||
import { SpeakerAction } from "./speaker-action";
|
||||
import { RendererAction } from "./renderer-action";
|
||||
import { BxLogger } from "@/utils/bx-logger";
|
||||
import { GameBarPosition, TouchControllerMode } from "@/enums/pref-values";
|
||||
|
||||
|
||||
export class GameBar {
|
||||
private static instance: GameBar;
|
||||
public static getInstance = () => GameBar.instance ?? (GameBar.instance = new GameBar());
|
||||
private static instance: GameBar | null | undefined;
|
||||
public static getInstance(): typeof GameBar['instance'] {
|
||||
if (typeof GameBar.instance === 'undefined') {
|
||||
if (getPref<GameBarPosition>(PrefKey.GAME_BAR_POSITION) !== GameBarPosition.OFF) {
|
||||
GameBar.instance = new GameBar();
|
||||
} else {
|
||||
GameBar.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
return GameBar.instance;
|
||||
}
|
||||
|
||||
private readonly LOG_TAG = 'GameBar';
|
||||
|
||||
private static readonly VISIBLE_DURATION = 2000;
|
||||
@@ -33,7 +45,7 @@ export class GameBar {
|
||||
|
||||
let $container;
|
||||
|
||||
const position = getPref(PrefKey.GAME_BAR_POSITION) as GameBarPosition;
|
||||
const position = getPref<GameBarPosition>(PrefKey.GAME_BAR_POSITION);
|
||||
|
||||
const $gameBar = CE('div', {id: 'bx-game-bar', class: 'bx-gone', 'data-position': position},
|
||||
$container = CE('div', {class: 'bx-game-bar-container bx-offscreen'}),
|
||||
@@ -42,7 +54,7 @@ export class GameBar {
|
||||
|
||||
this.actions = [
|
||||
new ScreenshotAction(),
|
||||
...(STATES.userAgent.capabilities.touch && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== StreamTouchController.OFF) ? [new TouchControlAction()] : []),
|
||||
...(STATES.userAgent.capabilities.touch && (getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) !== TouchControllerMode.OFF) ? [new TouchControlAction()] : []),
|
||||
new SpeakerAction(),
|
||||
new RendererAction(),
|
||||
new MicrophoneAction(),
|
||||
@@ -69,10 +81,10 @@ export class GameBar {
|
||||
});
|
||||
|
||||
// Hide game bar after clicking on an action
|
||||
window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar.bind(this));
|
||||
window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar);
|
||||
|
||||
$container.addEventListener('pointerover', this.clearHideTimeout.bind(this));
|
||||
$container.addEventListener('pointerout', this.beginHideTimeout.bind(this));
|
||||
$container.addEventListener('pointerover', this.clearHideTimeout);
|
||||
$container.addEventListener('pointerout', this.beginHideTimeout);
|
||||
|
||||
// Add animation when hiding game bar
|
||||
$container.addEventListener('transitionend', e => {
|
||||
@@ -84,16 +96,15 @@ export class GameBar {
|
||||
this.$container = $container;
|
||||
|
||||
// Enable/disable Game Bar when playing/pausing
|
||||
getPref(PrefKey.GAME_BAR_POSITION) !== 'off' && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e: Event) => {
|
||||
position !== GameBarPosition.OFF && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e: Event) => {
|
||||
// Toggle Game bar
|
||||
if (STATES.isPlaying) {
|
||||
const mode = (e as any).mode;
|
||||
mode !== 'none' ? this.disable() : this.enable();
|
||||
window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none' ? this.disable() : this.enable();
|
||||
}
|
||||
}).bind(this));
|
||||
}
|
||||
|
||||
private beginHideTimeout() {
|
||||
private beginHideTimeout = () => {
|
||||
this.clearHideTimeout();
|
||||
|
||||
this.timeoutId = window.setTimeout(() => {
|
||||
@@ -102,7 +113,7 @@ export class GameBar {
|
||||
}, GameBar.VISIBLE_DURATION);
|
||||
}
|
||||
|
||||
private clearHideTimeout() {
|
||||
private clearHideTimeout = () => {
|
||||
this.timeoutId && clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
}
|
||||
@@ -123,7 +134,7 @@ export class GameBar {
|
||||
this.beginHideTimeout();
|
||||
}
|
||||
|
||||
hideBar() {
|
||||
hideBar = () => {
|
||||
this.clearHideTimeout();
|
||||
this.$container.classList.replace('bx-show', 'bx-hide');
|
||||
}
|
||||
|
10
src/modules/game-bar/action-microphone.ts → src/modules/game-bar/microphone-action.ts
Normal file → Executable file
10
src/modules/game-bar/action-microphone.ts → src/modules/game-bar/microphone-action.ts
Normal file → Executable file
@@ -1,8 +1,8 @@
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import { createButton, ButtonStyle, CE } from "@utils/html";
|
||||
import { BaseGameBarAction } from "./action-base";
|
||||
import { MicrophoneShortcut, MicrophoneState } from "../shortcuts/shortcut-microphone";
|
||||
import { BaseGameBarAction } from "./base-action";
|
||||
import { MicrophoneShortcut, MicrophoneState } from "../shortcuts/microphone-shortcut";
|
||||
|
||||
|
||||
export class MicrophoneAction extends BaseGameBarAction {
|
||||
@@ -14,14 +14,14 @@ export class MicrophoneAction extends BaseGameBarAction {
|
||||
const $btnDefault = createButton({
|
||||
style: ButtonStyle.GHOST,
|
||||
icon: BxIcon.MICROPHONE,
|
||||
onClick: this.onClick.bind(this),
|
||||
onClick: this.onClick,
|
||||
classes: ['bx-activated'],
|
||||
});
|
||||
|
||||
const $btnMuted = createButton({
|
||||
style: ButtonStyle.GHOST,
|
||||
icon: BxIcon.MICROPHONE_MUTED,
|
||||
onClick: this.onClick.bind(this),
|
||||
onClick: this.onClick,
|
||||
});
|
||||
|
||||
this.$content = CE('div', {}, $btnMuted, $btnDefault);
|
||||
@@ -36,7 +36,7 @@ export class MicrophoneAction extends BaseGameBarAction {
|
||||
});
|
||||
}
|
||||
|
||||
onClick(e: Event) {
|
||||
onClick = (e: Event) => {
|
||||
super.onClick(e);
|
||||
const enabled = MicrophoneShortcut.toggle(false);
|
||||
this.$content.dataset.activated = enabled.toString();
|
19
src/modules/game-bar/action-renderer.ts → src/modules/game-bar/renderer-action.ts
Normal file → Executable file
19
src/modules/game-bar/action-renderer.ts → src/modules/game-bar/renderer-action.ts
Normal file → Executable file
@@ -1,7 +1,8 @@
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import { createButton, ButtonStyle, CE } from "@utils/html";
|
||||
import { BaseGameBarAction } from "./action-base";
|
||||
import { RendererShortcut } from "../shortcuts/shortcut-renderer";
|
||||
import { BaseGameBarAction } from "./base-action";
|
||||
import { RendererShortcut } from "../shortcuts/renderer-shortcut";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
|
||||
|
||||
export class RendererAction extends BaseGameBarAction {
|
||||
@@ -13,23 +14,27 @@ export class RendererAction extends BaseGameBarAction {
|
||||
const $btnDefault = createButton({
|
||||
style: ButtonStyle.GHOST,
|
||||
icon: BxIcon.EYE,
|
||||
onClick: this.onClick.bind(this),
|
||||
onClick: this.onClick,
|
||||
});
|
||||
|
||||
const $btnActivated = createButton({
|
||||
style: ButtonStyle.GHOST,
|
||||
icon: BxIcon.EYE_SLASH,
|
||||
onClick: this.onClick.bind(this),
|
||||
onClick: this.onClick,
|
||||
classes: ['bx-activated'],
|
||||
});
|
||||
|
||||
this.$content = CE('div', {}, $btnDefault, $btnActivated);
|
||||
|
||||
window.addEventListener(BxEvent.VIDEO_VISIBILITY_CHANGED, e => {
|
||||
const isShowing = (e as any).isShowing;
|
||||
this.$content.dataset.activated = (!isShowing).toString();
|
||||
});
|
||||
}
|
||||
|
||||
onClick(e: Event) {
|
||||
onClick = (e: Event) => {
|
||||
super.onClick(e);
|
||||
const isVisible = RendererShortcut.toggleVisibility();
|
||||
this.$content.dataset.activated = (!isVisible).toString();
|
||||
RendererShortcut.toggleVisibility();
|
||||
}
|
||||
|
||||
reset(): void {
|
6
src/modules/game-bar/action-screenshot.ts → src/modules/game-bar/screenshot-action.ts
Normal file → Executable file
6
src/modules/game-bar/action-screenshot.ts → src/modules/game-bar/screenshot-action.ts
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import { createButton, ButtonStyle } from "@utils/html";
|
||||
import { BaseGameBarAction } from "./action-base";
|
||||
import { BaseGameBarAction } from "./base-action";
|
||||
import { t } from "@utils/translation";
|
||||
import { ScreenshotManager } from "@/utils/screenshot-manager";
|
||||
|
||||
@@ -14,11 +14,11 @@ export class ScreenshotAction extends BaseGameBarAction {
|
||||
style: ButtonStyle.GHOST,
|
||||
icon: BxIcon.SCREENSHOT,
|
||||
title: t('take-screenshot'),
|
||||
onClick: this.onClick.bind(this),
|
||||
onClick: this.onClick,
|
||||
});
|
||||
}
|
||||
|
||||
onClick(e: Event): void {
|
||||
onClick = (e: Event) => {
|
||||
super.onClick(e);
|
||||
ScreenshotManager.getInstance().takeScreenshot();
|
||||
}
|
10
src/modules/game-bar/action-speaker.ts → src/modules/game-bar/speaker-action.ts
Normal file → Executable file
10
src/modules/game-bar/action-speaker.ts → src/modules/game-bar/speaker-action.ts
Normal file → Executable file
@@ -1,8 +1,8 @@
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import { createButton, ButtonStyle, CE } from "@utils/html";
|
||||
import { BaseGameBarAction } from "./action-base";
|
||||
import { SoundShortcut, SpeakerState } from "../shortcuts/shortcut-sound";
|
||||
import { BaseGameBarAction } from "./base-action";
|
||||
import { SoundShortcut, SpeakerState } from "../shortcuts/sound-shortcut";
|
||||
|
||||
|
||||
export class SpeakerAction extends BaseGameBarAction {
|
||||
@@ -14,13 +14,13 @@ export class SpeakerAction extends BaseGameBarAction {
|
||||
const $btnEnable = createButton({
|
||||
style: ButtonStyle.GHOST,
|
||||
icon: BxIcon.AUDIO,
|
||||
onClick: this.onClick.bind(this),
|
||||
onClick: this.onClick,
|
||||
});
|
||||
|
||||
const $btnMuted = createButton({
|
||||
style: ButtonStyle.GHOST,
|
||||
icon: BxIcon.SPEAKER_MUTED,
|
||||
onClick: this.onClick.bind(this),
|
||||
onClick: this.onClick,
|
||||
classes: ['bx-activated'],
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ export class SpeakerAction extends BaseGameBarAction {
|
||||
});
|
||||
}
|
||||
|
||||
onClick(e: Event) {
|
||||
onClick = (e: Event) => {
|
||||
super.onClick(e);
|
||||
SoundShortcut.muteUnmute();
|
||||
}
|
8
src/modules/game-bar/action-touch-control.ts → src/modules/game-bar/touch-control-action.ts
Normal file → Executable file
8
src/modules/game-bar/action-touch-control.ts → src/modules/game-bar/touch-control-action.ts
Normal file → Executable file
@@ -1,7 +1,7 @@
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import { createButton, ButtonStyle, CE } from "@utils/html";
|
||||
import { TouchController } from "@modules/touch-controller";
|
||||
import { BaseGameBarAction } from "./action-base";
|
||||
import { BaseGameBarAction } from "./base-action";
|
||||
import { t } from "@utils/translation";
|
||||
|
||||
export class TouchControlAction extends BaseGameBarAction {
|
||||
@@ -14,21 +14,21 @@ export class TouchControlAction extends BaseGameBarAction {
|
||||
style: ButtonStyle.GHOST,
|
||||
icon: BxIcon.TOUCH_CONTROL_ENABLE,
|
||||
title: t('show-touch-controller'),
|
||||
onClick: this.onClick.bind(this),
|
||||
onClick: this.onClick,
|
||||
});
|
||||
|
||||
const $btnDisable = createButton({
|
||||
style: ButtonStyle.GHOST,
|
||||
icon: BxIcon.TOUCH_CONTROL_DISABLE,
|
||||
title: t('hide-touch-controller'),
|
||||
onClick: this.onClick.bind(this),
|
||||
onClick: this.onClick,
|
||||
classes: ['bx-activated'],
|
||||
});
|
||||
|
||||
this.$content = CE('div', {}, $btnEnable, $btnDisable);
|
||||
}
|
||||
|
||||
onClick(e: Event) {
|
||||
onClick = (e: Event) => {
|
||||
super.onClick(e);
|
||||
const isVisible = TouchController.toggleVisibility();
|
||||
this.$content.dataset.activated = (!isVisible).toString();
|
6
src/modules/game-bar/action-true-achievements.ts → src/modules/game-bar/true-achievements-action.ts
Normal file → Executable file
6
src/modules/game-bar/action-true-achievements.ts → src/modules/game-bar/true-achievements-action.ts
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
import { BxIcon } from "@/utils/bx-icon";
|
||||
import { createButton, ButtonStyle } from "@/utils/html";
|
||||
import { BaseGameBarAction } from "./action-base";
|
||||
import { BaseGameBarAction } from "./base-action";
|
||||
import { TrueAchievements } from "@/utils/true-achievements";
|
||||
|
||||
export class TrueAchievementsAction extends BaseGameBarAction {
|
||||
@@ -12,11 +12,11 @@ export class TrueAchievementsAction extends BaseGameBarAction {
|
||||
this.$content = createButton({
|
||||
style: ButtonStyle.GHOST,
|
||||
icon: BxIcon.TRUE_ACHIEVEMENTS,
|
||||
onClick: this.onClick.bind(this),
|
||||
onClick: this.onClick,
|
||||
});
|
||||
}
|
||||
|
||||
onClick(e: Event) {
|
||||
onClick = (e: Event) => {
|
||||
super.onClick(e);
|
||||
TrueAchievements.getInstance().open(false);
|
||||
}
|
23
src/modules/loading-screen.ts
Normal file → Executable file
23
src/modules/loading-screen.ts
Normal file → Executable file
@@ -5,6 +5,7 @@ import { STATES } from "@utils/global";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { compressCss } from "@macros/build" with {type: "macro"};
|
||||
import { LoadingScreenRocket } from "@/enums/pref-values";
|
||||
|
||||
export class LoadingScreen {
|
||||
private static $bgStyle: HTMLElement;
|
||||
@@ -36,7 +37,7 @@ export class LoadingScreen {
|
||||
|
||||
LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl);
|
||||
|
||||
if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === 'hide') {
|
||||
if (getPref<LoadingScreenRocket>(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE) {
|
||||
LoadingScreen.hideRocket();
|
||||
}
|
||||
}
|
||||
@@ -88,7 +89,7 @@ export class LoadingScreen {
|
||||
|
||||
static setupWaitTime(waitTime: number) {
|
||||
// Hide rocket when queing
|
||||
if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === 'hide-queue') {
|
||||
if (getPref<LoadingScreenRocket>(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE_QUEUE) {
|
||||
LoadingScreen.hideRocket();
|
||||
}
|
||||
|
||||
@@ -108,14 +109,14 @@ export class LoadingScreen {
|
||||
|
||||
let $waitTimeBox = LoadingScreen.$waitTimeBox;
|
||||
if (!$waitTimeBox) {
|
||||
$waitTimeBox = CE('div', {'class': 'bx-wait-time-box'},
|
||||
CE('label', {}, t('server')),
|
||||
CE('span', {}, getPreferredServerRegion()),
|
||||
CE('label', {}, t('wait-time-estimated')),
|
||||
$estimated = CE('span', {}),
|
||||
CE('label', {}, t('wait-time-countdown')),
|
||||
$countDown = CE('span', {}),
|
||||
);
|
||||
$waitTimeBox = CE('div', { class: 'bx-wait-time-box' },
|
||||
CE('label', {}, t('server')),
|
||||
CE('span', {}, getPreferredServerRegion()),
|
||||
CE('label', {}, t('wait-time-estimated')),
|
||||
$estimated = CE('span', {}),
|
||||
CE('label', {}, t('wait-time-countdown')),
|
||||
$countDown = CE('span', {}),
|
||||
);
|
||||
|
||||
document.documentElement.appendChild($waitTimeBox);
|
||||
LoadingScreen.$waitTimeBox = $waitTimeBox;
|
||||
@@ -145,7 +146,7 @@ export class LoadingScreen {
|
||||
LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle);
|
||||
LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add('bx-gone');
|
||||
|
||||
if (getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.$bgStyle) {
|
||||
if (getPref(PrefKey.LOADING_SCREEN_GAME_ART) && LoadingScreen.$bgStyle) {
|
||||
const $rocketBg = document.querySelector('#game-stream rect[width="800"]');
|
||||
$rocketBg && $rocketBg.addEventListener('transitionend', e => {
|
||||
LoadingScreen.$bgStyle.textContent += compressCss(`
|
||||
|
6
src/modules/mkb/base-mkb-handler.ts
Normal file → Executable file
6
src/modules/mkb/base-mkb-handler.ts
Normal file → Executable file
@@ -4,10 +4,11 @@ export abstract class MouseDataProvider {
|
||||
this.mkbHandler = handler;
|
||||
}
|
||||
|
||||
abstract init(): void;
|
||||
init() {};
|
||||
destroy() {};
|
||||
|
||||
abstract start(): void;
|
||||
abstract stop(): void;
|
||||
abstract destroy(): void;
|
||||
}
|
||||
|
||||
export abstract class MkbHandler {
|
||||
@@ -15,6 +16,7 @@ export abstract class MkbHandler {
|
||||
abstract start(): void;
|
||||
abstract stop(): void;
|
||||
abstract destroy(): void;
|
||||
abstract toggle(force: boolean): void;
|
||||
abstract handleMouseMove(data: MkbMouseMove): void;
|
||||
abstract handleMouseClick(data: MkbMouseClick): void;
|
||||
abstract handleMouseWheel(data: MkbMouseWheel): boolean;
|
||||
|
113
src/modules/mkb/key-helper.ts
Normal file → Executable file
113
src/modules/mkb/key-helper.ts
Normal file → Executable file
@@ -1,8 +1,36 @@
|
||||
import { MouseButtonCode, WheelCode } from "@enums/mkb";
|
||||
import { MouseButtonCode, WheelCode, type KeyCode } from "@/enums/mkb";
|
||||
|
||||
export const enum KeyModifier {
|
||||
CTRL = 1,
|
||||
SHIFT = 2,
|
||||
ALT = 4,
|
||||
};
|
||||
|
||||
export type KeyEventInfo = {
|
||||
code: KeyCode | MouseButtonCode | WheelCode;
|
||||
modifiers?: number;
|
||||
};
|
||||
|
||||
export class KeyHelper {
|
||||
static #NON_PRINTABLE_KEYS = {
|
||||
'Backquote': '`',
|
||||
private static readonly NON_PRINTABLE_KEYS = {
|
||||
Backquote: '`',
|
||||
Minus: '-',
|
||||
Equal: '=',
|
||||
BracketLeft: '[',
|
||||
BracketRight: ']',
|
||||
Backslash: '\\',
|
||||
Semicolon: ';',
|
||||
Quote: '\'',
|
||||
Comma: ',',
|
||||
Period: '.',
|
||||
Slash: '/',
|
||||
|
||||
NumpadMultiply: 'Numpad *',
|
||||
NumpadAdd: 'Numpad +',
|
||||
NumpadSubtract: 'Numpad -',
|
||||
NumpadDecimal: 'Numpad .',
|
||||
NumpadDivide: 'Numpad /',
|
||||
NumpadEqual: 'Numpad =',
|
||||
|
||||
// Mouse buttons
|
||||
[MouseButtonCode.LEFT_CLICK]: 'Left Click',
|
||||
@@ -15,12 +43,19 @@ export class KeyHelper {
|
||||
[WheelCode.SCROLL_RIGHT]: 'Scroll Right',
|
||||
};
|
||||
|
||||
static getKeyFromEvent(e: Event) {
|
||||
let code;
|
||||
let name;
|
||||
static getKeyFromEvent(e: Event): KeyEventInfo | null {
|
||||
let code: KeyEventInfo['code'] | null = null;
|
||||
let modifiers;
|
||||
|
||||
if (e instanceof KeyboardEvent) {
|
||||
code = e.code || e.key;
|
||||
code = (e.code || e.key) as KeyCode;
|
||||
|
||||
// Modifiers
|
||||
modifiers = 0;
|
||||
modifiers ^= e.ctrlKey ? KeyModifier.CTRL : 0;
|
||||
modifiers ^= e.shiftKey ? KeyModifier.SHIFT : 0;
|
||||
modifiers ^= e.altKey ? KeyModifier.ALT : 0;
|
||||
|
||||
} else if (e instanceof WheelEvent) {
|
||||
if (e.deltaY < 0) {
|
||||
code = WheelCode.SCROLL_UP;
|
||||
@@ -32,20 +67,47 @@ export class KeyHelper {
|
||||
code = WheelCode.SCROLL_RIGHT;
|
||||
}
|
||||
} else if (e instanceof MouseEvent) {
|
||||
code = 'Mouse' + e.button;
|
||||
code = 'Mouse' + e.button as MouseButtonCode;
|
||||
}
|
||||
|
||||
if (code) {
|
||||
name = KeyHelper.codeToKeyName(code);
|
||||
const results: KeyEventInfo = { code };
|
||||
if (modifiers) {
|
||||
results.modifiers = modifiers;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
return code ? {code, name} : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
static codeToKeyName(code: string) {
|
||||
return (
|
||||
// @ts-ignore
|
||||
KeyHelper.#NON_PRINTABLE_KEYS[code]
|
||||
static getFullKeyCodeFromEvent(e: KeyboardEvent): string {
|
||||
const key = KeyHelper.getKeyFromEvent(e);
|
||||
return key ? `${key.code}:${key.modifiers || 0}` : '';
|
||||
}
|
||||
|
||||
static parseFullKeyCode(str: string | undefined | null): KeyEventInfo | null {
|
||||
if (!str) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tmp = str.split(':');
|
||||
|
||||
const code = tmp[0] as KeyEventInfo['code'];
|
||||
const modifiers = parseInt(tmp[1]);
|
||||
|
||||
return {
|
||||
code,
|
||||
modifiers,
|
||||
} as KeyEventInfo;
|
||||
}
|
||||
|
||||
static codeToKeyName(key: KeyEventInfo): string {
|
||||
const { code, modifiers } = key;
|
||||
|
||||
const text = [(
|
||||
KeyHelper.NON_PRINTABLE_KEYS[code as keyof typeof KeyHelper.NON_PRINTABLE_KEYS]
|
||||
||
|
||||
(code.startsWith('Key') && code.substring(3))
|
||||
||
|
||||
@@ -62,6 +124,27 @@ export class KeyHelper {
|
||||
(code.endsWith('Right') && ('Right ' + code.replace('Right', '')))
|
||||
||
|
||||
code
|
||||
);
|
||||
)];
|
||||
|
||||
if (modifiers && modifiers !== 0) {
|
||||
if (!code.startsWith('Control') && !code.startsWith('Shift') && !code.startsWith('Alt')) {
|
||||
// Shift
|
||||
if (modifiers & KeyModifier.SHIFT) {
|
||||
text.unshift('Shift');
|
||||
}
|
||||
|
||||
// Alt
|
||||
if (modifiers & KeyModifier.ALT) {
|
||||
text.unshift('Alt');
|
||||
}
|
||||
|
||||
// Ctrl
|
||||
if (modifiers & KeyModifier.CTRL) {
|
||||
text.unshift('Ctrl');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text.join(' + ');
|
||||
}
|
||||
}
|
||||
|
40
src/modules/mkb/keyboard-shortcut-handler.ts
Executable file
40
src/modules/mkb/keyboard-shortcut-handler.ts
Executable file
@@ -0,0 +1,40 @@
|
||||
import { ShortcutHandler } from "@/utils/shortcut-handler";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
|
||||
export class KeyboardShortcutHandler {
|
||||
private static instance: KeyboardShortcutHandler;
|
||||
public static getInstance = () => KeyboardShortcutHandler.instance ?? (KeyboardShortcutHandler.instance = new KeyboardShortcutHandler());
|
||||
|
||||
start() {
|
||||
window.addEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
stop() {
|
||||
window.removeEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
onKeyDown = (e: KeyboardEvent) => {
|
||||
// Don't run when the stream is not being focused
|
||||
if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't activate repeated key
|
||||
if (e.repeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check unknown key
|
||||
const fullKeyCode = KeyHelper.getFullKeyCodeFromEvent(e);
|
||||
if (!fullKeyCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = window.BX_STREAM_SETTINGS.keyboardShortcuts?.[fullKeyCode];
|
||||
if (action) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
ShortcutHandler.runAction(action);
|
||||
}
|
||||
}
|
||||
}
|
551
src/modules/mkb/mkb-handler.ts
Normal file → Executable file
551
src/modules/mkb/mkb-handler.ts
Normal file → Executable file
@@ -1,24 +1,21 @@
|
||||
import { isFullVersion } from "@macros/build" with {type: "macro"};
|
||||
|
||||
import { MkbPreset } from "./mkb-preset";
|
||||
import { GamepadKey, MkbPresetKey, GamepadStick, MouseMapTo, WheelCode } from "@enums/mkb";
|
||||
import { createButton, ButtonStyle, CE } from "@utils/html";
|
||||
import { MkbPresetKey, MouseConstant, MouseMapTo, WheelCode } from "@/enums/mkb";
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { Toast } from "@utils/toast";
|
||||
import { t } from "@utils/translation";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
import type { MkbStoredPreset } from "@/types/mkb";
|
||||
import { AppInterface, STATES } from "@utils/global";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
import { PointerClient } from "./pointer-client";
|
||||
import { NativeMkbHandler } from "./native-mkb-handler";
|
||||
import { MkbHandler, MouseDataProvider } from "./base-mkb-handler";
|
||||
import { SettingsNavigationDialog } from "../ui/dialog/settings-dialog";
|
||||
import { NavigationDialogManager } from "../ui/dialog/navigation-dialog";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { MkbPresetsDb } from "@/utils/local-db/mkb-presets-db";
|
||||
import { GamepadKey, GamepadStick } from "@/enums/gamepad";
|
||||
import { MkbPopup } from "./mkb-popup";
|
||||
import type { MkbConvertedPresetData } from "@/types/presets";
|
||||
|
||||
const PointerToMouseButton = {
|
||||
1: 0,
|
||||
@@ -26,79 +23,74 @@ const PointerToMouseButton = {
|
||||
4: 1,
|
||||
}
|
||||
|
||||
export const VIRTUAL_GAMEPAD_ID = 'Xbox 360 Controller';
|
||||
export const VIRTUAL_GAMEPAD_ID = 'Better xCloud Virtual Controller';
|
||||
|
||||
class WebSocketMouseDataProvider extends MouseDataProvider {
|
||||
#pointerClient: PointerClient | undefined
|
||||
#connected = false
|
||||
private pointerClient: PointerClient | undefined
|
||||
private isConnected = false
|
||||
|
||||
init(): void {
|
||||
this.#pointerClient = PointerClient.getInstance();
|
||||
this.#connected = false;
|
||||
this.pointerClient = PointerClient.getInstance();
|
||||
this.isConnected = false;
|
||||
try {
|
||||
this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler);
|
||||
this.#connected = true;
|
||||
this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler);
|
||||
this.isConnected = true;
|
||||
} catch (e) {
|
||||
Toast.show('Cannot enable Mouse & Keyboard feature');
|
||||
}
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.#connected && AppInterface.requestPointerCapture();
|
||||
this.isConnected && AppInterface.requestPointerCapture();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.#connected && AppInterface.releasePointerCapture();
|
||||
this.isConnected && AppInterface.releasePointerCapture();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.#connected && this.#pointerClient?.stop();
|
||||
this.isConnected && this.pointerClient?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
class PointerLockMouseDataProvider extends MouseDataProvider {
|
||||
init(): void {}
|
||||
|
||||
start(): void {
|
||||
window.addEventListener('mousemove', this.#onMouseMoveEvent);
|
||||
window.addEventListener('mousedown', this.#onMouseEvent);
|
||||
window.addEventListener('mouseup', this.#onMouseEvent);
|
||||
window.addEventListener('wheel', this.#onWheelEvent, {passive: false});
|
||||
window.addEventListener('contextmenu', this.#disableContextMenu);
|
||||
start() {
|
||||
window.addEventListener('mousemove', this.onMouseMoveEvent);
|
||||
window.addEventListener('mousedown', this.onMouseEvent);
|
||||
window.addEventListener('mouseup', this.onMouseEvent);
|
||||
window.addEventListener('wheel', this.onWheelEvent, { passive: false });
|
||||
window.addEventListener('contextmenu', this.disableContextMenu);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
stop() {
|
||||
document.pointerLockElement && document.exitPointerLock();
|
||||
|
||||
window.removeEventListener('mousemove', this.#onMouseMoveEvent);
|
||||
window.removeEventListener('mousedown', this.#onMouseEvent);
|
||||
window.removeEventListener('mouseup', this.#onMouseEvent);
|
||||
window.removeEventListener('wheel', this.#onWheelEvent);
|
||||
window.removeEventListener('contextmenu', this.#disableContextMenu);
|
||||
window.removeEventListener('mousemove', this.onMouseMoveEvent);
|
||||
window.removeEventListener('mousedown', this.onMouseEvent);
|
||||
window.removeEventListener('mouseup', this.onMouseEvent);
|
||||
window.removeEventListener('wheel', this.onWheelEvent);
|
||||
window.removeEventListener('contextmenu', this.disableContextMenu);
|
||||
}
|
||||
|
||||
destroy(): void {}
|
||||
|
||||
#onMouseMoveEvent = (e: MouseEvent) => {
|
||||
private onMouseMoveEvent = (e: MouseEvent) => {
|
||||
this.mkbHandler.handleMouseMove({
|
||||
movementX: e.movementX,
|
||||
movementY: e.movementY,
|
||||
});
|
||||
}
|
||||
|
||||
#onMouseEvent = (e: MouseEvent) => {
|
||||
private onMouseEvent = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const isMouseDown = e.type === 'mousedown';
|
||||
const data: MkbMouseClick = {
|
||||
mouseButton: e.button,
|
||||
pressed: isMouseDown,
|
||||
pressed: e.type === 'mousedown',
|
||||
};
|
||||
|
||||
this.mkbHandler.handleMouseClick(data);
|
||||
}
|
||||
|
||||
#onWheelEvent = (e: WheelEvent) => {
|
||||
private onWheelEvent = (e: WheelEvent) => {
|
||||
const key = KeyHelper.getKeyFromEvent(e);
|
||||
if (!key) {
|
||||
return;
|
||||
@@ -114,7 +106,7 @@ class PointerLockMouseDataProvider extends MouseDataProvider {
|
||||
}
|
||||
}
|
||||
|
||||
#disableContextMenu = (e: Event) => e.preventDefault();
|
||||
private disableContextMenu = (e: Event) => e.preventDefault();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -122,80 +114,95 @@ This class uses some code from Yuzu emulator to handle mouse's movements
|
||||
Source: https://github.com/yuzu-emu/yuzu-mainline/blob/master/src/input_common/drivers/mouse.cpp
|
||||
*/
|
||||
export class EmulatedMkbHandler extends MkbHandler {
|
||||
private static instance: EmulatedMkbHandler;
|
||||
public static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler());
|
||||
private static instance: EmulatedMkbHandler | null | undefined;
|
||||
public static getInstance(): typeof EmulatedMkbHandler['instance'] {
|
||||
if (typeof EmulatedMkbHandler.instance === 'undefined') {
|
||||
if (EmulatedMkbHandler.isAllowed()) {
|
||||
EmulatedMkbHandler.instance = new EmulatedMkbHandler();
|
||||
} else {
|
||||
EmulatedMkbHandler.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
return EmulatedMkbHandler.instance;
|
||||
}
|
||||
|
||||
private static readonly LOG_TAG = 'EmulatedMkbHandler';
|
||||
|
||||
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
|
||||
static isAllowed() {
|
||||
return getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile());
|
||||
}
|
||||
|
||||
static readonly DEFAULT_PANNING_SENSITIVITY = 0.0010;
|
||||
static readonly DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01;
|
||||
static readonly MAXIMUM_STICK_RANGE = 1.1;
|
||||
private PRESET!: MkbConvertedPresetData | null;
|
||||
private VIRTUAL_GAMEPAD = {
|
||||
id: VIRTUAL_GAMEPAD_ID,
|
||||
index: 0,
|
||||
connected: false,
|
||||
hapticActuators: null,
|
||||
mapping: 'standard',
|
||||
|
||||
#VIRTUAL_GAMEPAD = {
|
||||
id: VIRTUAL_GAMEPAD_ID,
|
||||
index: 3,
|
||||
connected: false,
|
||||
hapticActuators: null,
|
||||
mapping: 'standard',
|
||||
axes: [0, 0, 0, 0],
|
||||
buttons: new Array(17).fill(null).map(() => ({pressed: false, value: 0})),
|
||||
timestamp: performance.now(),
|
||||
|
||||
axes: [0, 0, 0, 0],
|
||||
buttons: new Array(17).fill(null).map(() => ({pressed: false, value: 0})),
|
||||
timestamp: performance.now(),
|
||||
vibrationActuator: null,
|
||||
};
|
||||
private nativeGetGamepads: Navigator['getGamepads'];
|
||||
|
||||
vibrationActuator: null,
|
||||
};
|
||||
#nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator);
|
||||
private initialized = false;
|
||||
private enabled = false;
|
||||
private mouseDataProvider: MouseDataProvider | undefined;
|
||||
private isPolling = false;
|
||||
|
||||
#enabled = false;
|
||||
#mouseDataProvider: MouseDataProvider | undefined;
|
||||
#isPolling = false;
|
||||
private prevWheelCode = null;
|
||||
private wheelStoppedTimeoutId: number | null = null;
|
||||
|
||||
#prevWheelCode = null;
|
||||
#wheelStoppedTimeout?: number | null;
|
||||
private detectMouseStoppedTimeoutId: number | null = null;
|
||||
|
||||
#detectMouseStoppedTimeout?: number | null;
|
||||
private escKeyDownTime: number = -1;
|
||||
|
||||
#$message?: HTMLElement;
|
||||
private LEFT_STICK_X: GamepadKey[] = [];
|
||||
private LEFT_STICK_Y: GamepadKey[] = [];
|
||||
private RIGHT_STICK_X: GamepadKey[] = [];
|
||||
private RIGHT_STICK_Y: GamepadKey[] = [];
|
||||
|
||||
#escKeyDownTime: number = -1;
|
||||
private popup: MkbPopup;
|
||||
|
||||
#STICK_MAP: {[key in GamepadKey]?: [GamepadKey[], number, number]};
|
||||
#LEFT_STICK_X: GamepadKey[] = [];
|
||||
#LEFT_STICK_Y: GamepadKey[] = [];
|
||||
#RIGHT_STICK_X: GamepadKey[] = [];
|
||||
#RIGHT_STICK_Y: GamepadKey[] = [];
|
||||
private STICK_MAP: {[key in GamepadKey]?: [GamepadKey[], number, number]} = {
|
||||
[GamepadKey.LS_LEFT]: [this.LEFT_STICK_X, 0, -1],
|
||||
[GamepadKey.LS_RIGHT]: [this.LEFT_STICK_X, 0, 1],
|
||||
[GamepadKey.LS_UP]: [this.LEFT_STICK_Y, 1, -1],
|
||||
[GamepadKey.LS_DOWN]: [this.LEFT_STICK_Y, 1, 1],
|
||||
|
||||
[GamepadKey.RS_LEFT]: [this.RIGHT_STICK_X, 2, -1],
|
||||
[GamepadKey.RS_RIGHT]: [this.RIGHT_STICK_X, 2, 1],
|
||||
[GamepadKey.RS_UP]: [this.RIGHT_STICK_Y, 3, -1],
|
||||
[GamepadKey.RS_DOWN]: [this.RIGHT_STICK_Y, 3, 1],
|
||||
};
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
BxLogger.info(EmulatedMkbHandler.LOG_TAG, 'constructor()');
|
||||
|
||||
this.#STICK_MAP = {
|
||||
[GamepadKey.LS_LEFT]: [this.#LEFT_STICK_X, 0, -1],
|
||||
[GamepadKey.LS_RIGHT]: [this.#LEFT_STICK_X, 0, 1],
|
||||
[GamepadKey.LS_UP]: [this.#LEFT_STICK_Y, 1, -1],
|
||||
[GamepadKey.LS_DOWN]: [this.#LEFT_STICK_Y, 1, 1],
|
||||
this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator);
|
||||
|
||||
[GamepadKey.RS_LEFT]: [this.#RIGHT_STICK_X, 2, -1],
|
||||
[GamepadKey.RS_RIGHT]: [this.#RIGHT_STICK_X, 2, 1],
|
||||
[GamepadKey.RS_UP]: [this.#RIGHT_STICK_Y, 3, -1],
|
||||
[GamepadKey.RS_DOWN]: [this.#RIGHT_STICK_Y, 3, 1],
|
||||
};
|
||||
this.popup = MkbPopup.getInstance();
|
||||
this.popup.attachMkbHandler(this);
|
||||
}
|
||||
|
||||
isEnabled = () => this.#enabled;
|
||||
isEnabled = () => this.enabled;
|
||||
|
||||
#patchedGetGamepads = () => {
|
||||
const gamepads = this.#nativeGetGamepads() || [];
|
||||
(gamepads as any)[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD;
|
||||
private patchedGetGamepads = () => {
|
||||
const gamepads = (this.nativeGetGamepads() || []) as any;
|
||||
|
||||
gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD;
|
||||
return gamepads;
|
||||
}
|
||||
|
||||
#getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD;
|
||||
private getVirtualGamepad = () => this.VIRTUAL_GAMEPAD;
|
||||
|
||||
#updateStick(stick: GamepadStick, x: number, y: number) {
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
private updateStick(stick: GamepadStick, x: number, y: number) {
|
||||
const virtualGamepad = this.getVirtualGamepad();
|
||||
virtualGamepad.axes[stick * 2] = x;
|
||||
virtualGamepad.axes[stick * 2 + 1] = y;
|
||||
|
||||
@@ -212,10 +219,10 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
}
|
||||
*/
|
||||
|
||||
#vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2);
|
||||
private vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2);
|
||||
|
||||
#resetGamepad = () => {
|
||||
const gamepad = this.#getVirtualGamepad();
|
||||
private resetGamepad() {
|
||||
const gamepad = this.getVirtualGamepad();
|
||||
|
||||
// Reset axes
|
||||
gamepad.axes = [0, 0, 0, 0];
|
||||
@@ -229,11 +236,11 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
gamepad.timestamp = performance.now();
|
||||
}
|
||||
|
||||
#pressButton = (buttonIndex: GamepadKey, pressed: boolean) => {
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
private pressButton(buttonIndex: GamepadKey, pressed: boolean) {
|
||||
const virtualGamepad = this.getVirtualGamepad();
|
||||
|
||||
if (buttonIndex >= 100) {
|
||||
let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]!;
|
||||
let [valueArr, axisIndex] = this.STICK_MAP[buttonIndex]!;
|
||||
valueArr = valueArr as number[];
|
||||
axisIndex = axisIndex as number;
|
||||
|
||||
@@ -249,7 +256,7 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
let value;
|
||||
if (valueArr.length) {
|
||||
// Get value of the last key of the axis
|
||||
value = this.#STICK_MAP[valueArr[valueArr.length - 1]]![2] as number;
|
||||
value = this.STICK_MAP[valueArr[valueArr.length - 1]]![2] as number;
|
||||
} else {
|
||||
value = 0;
|
||||
}
|
||||
@@ -263,41 +270,35 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
virtualGamepad.timestamp = performance.now();
|
||||
}
|
||||
|
||||
#onKeyboardEvent = (e: KeyboardEvent) => {
|
||||
private onKeyboardEvent = (e: KeyboardEvent) => {
|
||||
const isKeyDown = e.type === 'keydown';
|
||||
|
||||
// Toggle MKB feature
|
||||
if (e.code === 'F8') {
|
||||
if (!isKeyDown) {
|
||||
e.preventDefault();
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Hijack the Esc button
|
||||
if (e.code === 'Escape') {
|
||||
e.preventDefault();
|
||||
|
||||
// Hold the Esc for 1 second to disable MKB
|
||||
if (this.#enabled && isKeyDown) {
|
||||
if (this.#escKeyDownTime === -1) {
|
||||
this.#escKeyDownTime = performance.now();
|
||||
} else if (performance.now() - this.#escKeyDownTime >= 1000) {
|
||||
if (this.enabled && isKeyDown) {
|
||||
if (this.escKeyDownTime === -1) {
|
||||
this.escKeyDownTime = performance.now();
|
||||
} else if (performance.now() - this.escKeyDownTime >= 1000) {
|
||||
this.stop();
|
||||
}
|
||||
} else {
|
||||
this.#escKeyDownTime = -1;
|
||||
this.escKeyDownTime = -1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#isPolling) {
|
||||
if (!this.isPolling || !this.PRESET) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]!;
|
||||
if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonIndex = this.PRESET.mapping[e.code || e.key]!;
|
||||
if (typeof buttonIndex === 'undefined') {
|
||||
return;
|
||||
}
|
||||
@@ -308,19 +309,23 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
this.#pressButton(buttonIndex, isKeyDown);
|
||||
this.pressButton(buttonIndex, isKeyDown);
|
||||
}
|
||||
|
||||
#onMouseStopped = () => {
|
||||
private onMouseStopped = () => {
|
||||
// Reset stick position
|
||||
this.#detectMouseStoppedTimeout = null;
|
||||
this.detectMouseStoppedTimeoutId = null;
|
||||
|
||||
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
|
||||
if (!this.PRESET) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mouseMapTo = this.PRESET.mouse[MkbPresetKey.MOUSE_MAP_TO];
|
||||
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
|
||||
this.#updateStick(analog, 0, 0);
|
||||
this.updateStick(analog, 0, 0);
|
||||
}
|
||||
|
||||
handleMouseClick = (data: MkbMouseClick) => {
|
||||
handleMouseClick(data: MkbMouseClick) {
|
||||
let mouseButton;
|
||||
if (typeof data.mouseButton !== 'undefined') {
|
||||
mouseButton = data.mouseButton;
|
||||
@@ -331,51 +336,54 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
const keyCode = 'Mouse' + mouseButton;
|
||||
const key = {
|
||||
code: keyCode,
|
||||
name: KeyHelper.codeToKeyName(keyCode),
|
||||
};
|
||||
|
||||
if (!key.name) {
|
||||
if (!this.PRESET) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]!;
|
||||
const buttonIndex = this.PRESET.mapping[key.code]!;
|
||||
if (typeof buttonIndex === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#pressButton(buttonIndex, data.pressed);
|
||||
this.pressButton(buttonIndex, data.pressed);
|
||||
}
|
||||
|
||||
handleMouseMove = (data: MkbMouseMove) => {
|
||||
handleMouseMove(data: MkbMouseMove) {
|
||||
const preset = this.PRESET;
|
||||
if (!preset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: optimize this
|
||||
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
|
||||
const mouseMapTo = preset.mouse[MkbPresetKey.MOUSE_MAP_TO];
|
||||
if (mouseMapTo === MouseMapTo.OFF) {
|
||||
// Ignore mouse movements
|
||||
return;
|
||||
}
|
||||
|
||||
this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout);
|
||||
this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50);
|
||||
this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId);
|
||||
this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50);
|
||||
|
||||
const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT];
|
||||
const deadzoneCounterweight = preset.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT];
|
||||
|
||||
let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X];
|
||||
let y = data.movementY * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y];
|
||||
let x = data.movementX * preset.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X];
|
||||
let y = data.movementY * preset.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y];
|
||||
|
||||
let length = this.#vectorLength(x, y);
|
||||
let length = this.vectorLength(x, y);
|
||||
if (length !== 0 && length < deadzoneCounterweight) {
|
||||
x *= deadzoneCounterweight / length;
|
||||
y *= deadzoneCounterweight / length;
|
||||
} else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) {
|
||||
x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length;
|
||||
y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length;
|
||||
} else if (length > MouseConstant.MAXIMUM_STICK_RANGE) {
|
||||
x *= MouseConstant.MAXIMUM_STICK_RANGE / length;
|
||||
y *= MouseConstant.MAXIMUM_STICK_RANGE / length;
|
||||
}
|
||||
|
||||
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
|
||||
this.#updateStick(analog, x, y);
|
||||
this.updateStick(analog, x, y);
|
||||
}
|
||||
|
||||
handleMouseWheel = (data: MkbMouseWheel): boolean => {
|
||||
handleMouseWheel(data: MkbMouseWheel): boolean {
|
||||
let code = '';
|
||||
if (data.vertical < 0) {
|
||||
code = WheelCode.SCROLL_UP;
|
||||
@@ -391,136 +399,69 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.PRESET) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const key = {
|
||||
code: code,
|
||||
name: KeyHelper.codeToKeyName(code),
|
||||
};
|
||||
|
||||
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]!;
|
||||
const buttonIndex = this.PRESET.mapping[key.code]!;
|
||||
if (typeof buttonIndex === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) {
|
||||
this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout);
|
||||
this.#pressButton(buttonIndex, true);
|
||||
if (this.prevWheelCode === null || this.prevWheelCode === key.code) {
|
||||
this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId);
|
||||
this.pressButton(buttonIndex, true);
|
||||
}
|
||||
|
||||
this.#wheelStoppedTimeout = window.setTimeout(() => {
|
||||
this.#prevWheelCode = null;
|
||||
this.#pressButton(buttonIndex, false);
|
||||
this.wheelStoppedTimeoutId = window.setTimeout(() => {
|
||||
this.prevWheelCode = null;
|
||||
this.pressButton(buttonIndex, false);
|
||||
}, 20);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
toggle = (force?: boolean) => {
|
||||
if (typeof force !== 'undefined') {
|
||||
this.#enabled = force;
|
||||
} else {
|
||||
this.#enabled = !this.#enabled;
|
||||
toggle(force?: boolean) {
|
||||
if (!this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#enabled) {
|
||||
if (typeof force !== 'undefined') {
|
||||
this.enabled = force;
|
||||
} else {
|
||||
this.enabled = !this.enabled;
|
||||
}
|
||||
|
||||
if (this.enabled) {
|
||||
document.body.requestPointerLock();
|
||||
} else {
|
||||
document.pointerLockElement && document.exitPointerLock();
|
||||
}
|
||||
}
|
||||
|
||||
#getCurrentPreset = (): Promise<MkbStoredPreset> => {
|
||||
return new Promise(resolve => {
|
||||
const presetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
|
||||
MkbPresetsDb.getInstance().getPreset(presetId).then((preset: MkbStoredPreset) => {
|
||||
resolve(preset);
|
||||
});
|
||||
});
|
||||
refreshPresetData() {
|
||||
this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset;
|
||||
this.resetGamepad();
|
||||
}
|
||||
|
||||
refreshPresetData = () => {
|
||||
this.#getCurrentPreset().then((preset: MkbStoredPreset) => {
|
||||
this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET);
|
||||
this.#resetGamepad();
|
||||
});
|
||||
waitForMouseData(showPopup: boolean) {
|
||||
this.popup.toggleVisibility(showPopup);
|
||||
}
|
||||
|
||||
waitForMouseData = (wait: boolean) => {
|
||||
this.#$message && this.#$message.classList.toggle('bx-gone', !wait);
|
||||
private onPollingModeChanged = (e: Event) => {
|
||||
const move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none';
|
||||
this.popup.moveOffscreen(move);
|
||||
}
|
||||
|
||||
#onPollingModeChanged = (e: Event) => {
|
||||
if (!this.#$message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = (e as any).mode;
|
||||
if (mode === 'none') {
|
||||
this.#$message.classList.remove('bx-offscreen');
|
||||
} else {
|
||||
this.#$message.classList.add('bx-offscreen');
|
||||
}
|
||||
}
|
||||
|
||||
#onDialogShown = () => {
|
||||
private onDialogShown = () => {
|
||||
document.pointerLockElement && document.exitPointerLock();
|
||||
}
|
||||
|
||||
#initMessage = () => {
|
||||
if (!this.#$message) {
|
||||
this.#$message = CE('div', {'class': 'bx-mkb-pointer-lock-msg bx-gone'},
|
||||
CE('div', {},
|
||||
CE('p', {}, t('virtual-controller')),
|
||||
CE('p', {}, t('press-key-to-toggle-mkb', {key: 'F8'})),
|
||||
),
|
||||
|
||||
CE('div', {'data-type': 'virtual'},
|
||||
createButton({
|
||||
style: ButtonStyle.PRIMARY | ButtonStyle.TALL | ButtonStyle.FULL_WIDTH,
|
||||
label: t('activate'),
|
||||
onClick: ((e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.toggle(true);
|
||||
}).bind(this),
|
||||
}),
|
||||
|
||||
CE('div', {},
|
||||
createButton({
|
||||
label: t('ignore'),
|
||||
style: ButtonStyle.GHOST,
|
||||
onClick: e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.toggle(false);
|
||||
this.waitForMouseData(false);
|
||||
},
|
||||
}),
|
||||
|
||||
createButton({
|
||||
label: t('edit'),
|
||||
onClick: e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Show Settings dialog & focus the MKB tab
|
||||
const dialog = SettingsNavigationDialog.getInstance();
|
||||
dialog.focusTab('mkb');
|
||||
NavigationDialogManager.getInstance().show(dialog);
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.#$message.isConnected) {
|
||||
document.documentElement.appendChild(this.#$message);
|
||||
}
|
||||
}
|
||||
|
||||
#onPointerLockChange = () => {
|
||||
private onPointerLockChange = () => {
|
||||
if (document.pointerLockElement) {
|
||||
this.start();
|
||||
} else {
|
||||
@@ -528,58 +469,64 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
}
|
||||
}
|
||||
|
||||
#onPointerLockError = (e: Event) => {
|
||||
private onPointerLockError = (e: Event) => {
|
||||
console.log(e);
|
||||
this.stop();
|
||||
}
|
||||
|
||||
#onPointerLockRequested = () => {
|
||||
private onPointerLockRequested = () => {
|
||||
this.start();
|
||||
}
|
||||
|
||||
#onPointerLockExited = () => {
|
||||
this.#mouseDataProvider?.stop();
|
||||
private onPointerLockExited = () => {
|
||||
this.mouseDataProvider?.stop();
|
||||
}
|
||||
|
||||
handleEvent(event: Event) {
|
||||
switch (event.type) {
|
||||
case BxEvent.POINTER_LOCK_REQUESTED:
|
||||
this.#onPointerLockRequested();
|
||||
this.onPointerLockRequested();
|
||||
break;
|
||||
case BxEvent.POINTER_LOCK_EXITED:
|
||||
this.#onPointerLockExited();
|
||||
this.onPointerLockExited();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
init = () => {
|
||||
init() {
|
||||
if (!STATES.browser.capabilities.mkb) {
|
||||
this.initialized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
this.refreshPresetData();
|
||||
this.#enabled = false;
|
||||
this.enabled = false;
|
||||
|
||||
if (AppInterface) {
|
||||
this.#mouseDataProvider = new WebSocketMouseDataProvider(this);
|
||||
this.mouseDataProvider = new WebSocketMouseDataProvider(this);
|
||||
} else {
|
||||
this.#mouseDataProvider = new PointerLockMouseDataProvider(this);
|
||||
this.mouseDataProvider = new PointerLockMouseDataProvider(this);
|
||||
}
|
||||
this.#mouseDataProvider.init();
|
||||
this.mouseDataProvider.init();
|
||||
|
||||
window.addEventListener('keydown', this.#onKeyboardEvent);
|
||||
window.addEventListener('keyup', this.#onKeyboardEvent);
|
||||
window.addEventListener('keydown', this.onKeyboardEvent);
|
||||
window.addEventListener('keyup', this.onKeyboardEvent);
|
||||
|
||||
window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged);
|
||||
window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown);
|
||||
window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
|
||||
window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.onDialogShown);
|
||||
|
||||
if (AppInterface) {
|
||||
// Android app doesn't support PointerLock API so we need to use a different method
|
||||
window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this);
|
||||
window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this);
|
||||
} else {
|
||||
document.addEventListener('pointerlockchange', this.#onPointerLockChange);
|
||||
document.addEventListener('pointerlockerror', this.#onPointerLockError);
|
||||
document.addEventListener('pointerlockchange', this.onPointerLockChange);
|
||||
document.addEventListener('pointerlockerror', this.onPointerLockError);
|
||||
}
|
||||
|
||||
this.#initMessage();
|
||||
this.#$message?.classList.add('bx-gone');
|
||||
MkbPopup.getInstance().reset();
|
||||
|
||||
if (AppInterface) {
|
||||
Toast.show(t('press-key-to-toggle-mkb', {key: `<b>F8</b>`}), t('virtual-controller'), {html: true});
|
||||
@@ -589,51 +536,62 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
}
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this.#isPolling = false;
|
||||
this.#enabled = false;
|
||||
destroy() {
|
||||
if (!this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialized = false;
|
||||
this.isPolling = false;
|
||||
this.enabled = false;
|
||||
this.stop();
|
||||
|
||||
this.waitForMouseData(false);
|
||||
document.pointerLockElement && document.exitPointerLock();
|
||||
|
||||
window.removeEventListener('keydown', this.#onKeyboardEvent);
|
||||
window.removeEventListener('keyup', this.#onKeyboardEvent);
|
||||
window.removeEventListener('keydown', this.onKeyboardEvent);
|
||||
window.removeEventListener('keyup', this.onKeyboardEvent);
|
||||
|
||||
if (AppInterface) {
|
||||
window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this);
|
||||
window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this);
|
||||
} else {
|
||||
document.removeEventListener('pointerlockchange', this.#onPointerLockChange);
|
||||
document.removeEventListener('pointerlockerror', this.#onPointerLockError);
|
||||
document.removeEventListener('pointerlockchange', this.onPointerLockChange);
|
||||
document.removeEventListener('pointerlockerror', this.onPointerLockError);
|
||||
}
|
||||
|
||||
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged);
|
||||
window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown);
|
||||
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
|
||||
window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.onDialogShown);
|
||||
|
||||
this.#mouseDataProvider?.destroy();
|
||||
this.mouseDataProvider?.destroy();
|
||||
|
||||
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged);
|
||||
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
|
||||
}
|
||||
|
||||
start = () => {
|
||||
if (!this.#enabled) {
|
||||
this.#enabled = true;
|
||||
Toast.show(t('virtual-controller'), t('enabled'), {instant: true});
|
||||
updateGamepadSlots() {
|
||||
// Set gamepad slot
|
||||
this.VIRTUAL_GAMEPAD.index = getPref<number>(PrefKey.MKB_P1_SLOT) - 1;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.enabled) {
|
||||
this.enabled = true;
|
||||
Toast.show(t('virtual-controller'), t('enabled'), { instant: true });
|
||||
}
|
||||
|
||||
this.#isPolling = true;
|
||||
this.#escKeyDownTime = -1;
|
||||
this.isPolling = true;
|
||||
this.escKeyDownTime = -1;
|
||||
|
||||
this.#resetGamepad();
|
||||
window.navigator.getGamepads = this.#patchedGetGamepads;
|
||||
this.resetGamepad();
|
||||
this.updateGamepadSlots();
|
||||
window.navigator.getGamepads = this.patchedGetGamepads;
|
||||
|
||||
this.waitForMouseData(false);
|
||||
|
||||
this.#mouseDataProvider?.start();
|
||||
this.mouseDataProvider?.start();
|
||||
|
||||
// Dispatch "gamepadconnected" event
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
const virtualGamepad = this.getVirtualGamepad();
|
||||
virtualGamepad.connected = true;
|
||||
virtualGamepad.timestamp = performance.now();
|
||||
|
||||
@@ -643,46 +601,51 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
|
||||
window.BX_EXPOSED.stopTakRendering = true;
|
||||
|
||||
Toast.show(t('virtual-controller'), t('enabled'), {instant: true});
|
||||
Toast.show(t('virtual-controller'), t('enabled'), { instant: true });
|
||||
}
|
||||
|
||||
stop = () => {
|
||||
this.#enabled = false;
|
||||
this.#isPolling = false;
|
||||
this.#escKeyDownTime = -1;
|
||||
stop() {
|
||||
this.enabled = false;
|
||||
this.isPolling = false;
|
||||
this.escKeyDownTime = -1;
|
||||
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
const virtualGamepad = this.getVirtualGamepad();
|
||||
if (virtualGamepad.connected) {
|
||||
// Dispatch "gamepaddisconnected" event
|
||||
this.#resetGamepad();
|
||||
this.resetGamepad();
|
||||
|
||||
virtualGamepad.connected = false;
|
||||
virtualGamepad.timestamp = performance.now();
|
||||
|
||||
BxEvent.dispatch(window, 'gamepaddisconnected', {
|
||||
gamepad: virtualGamepad,
|
||||
});
|
||||
gamepad: virtualGamepad,
|
||||
});
|
||||
|
||||
window.navigator.getGamepads = this.#nativeGetGamepads;
|
||||
window.navigator.getGamepads = this.nativeGetGamepads;
|
||||
}
|
||||
|
||||
this.waitForMouseData(true);
|
||||
this.#mouseDataProvider?.stop();
|
||||
this.mouseDataProvider?.stop();
|
||||
|
||||
// Toast.show(t('virtual-controller'), t('disabled'), {instant: true});
|
||||
}
|
||||
|
||||
static setupEvents() {
|
||||
isFullVersion() && window.addEventListener(BxEvent.STREAM_PLAYING, () => {
|
||||
if (STATES.currentStream.titleInfo?.details.hasMkbSupport) {
|
||||
// Enable native MKB in Android app
|
||||
if (AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on') {
|
||||
AppInterface && NativeMkbHandler.getInstance().init();
|
||||
if (isFullVersion()) {
|
||||
window.addEventListener(BxEvent.STREAM_PLAYING, () => {
|
||||
if (STATES.currentStream.titleInfo?.details.hasMkbSupport) {
|
||||
// Enable native MKB in Android app
|
||||
NativeMkbHandler.getInstance()?.init();
|
||||
} else {
|
||||
EmulatedMkbHandler.getInstance()?.init();
|
||||
}
|
||||
} else if (getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile())) {
|
||||
BxLogger.info(EmulatedMkbHandler.LOG_TAG, 'Emulate MKB');
|
||||
EmulatedMkbHandler.getInstance().init();
|
||||
});
|
||||
|
||||
if (EmulatedMkbHandler.isAllowed()) {
|
||||
window.addEventListener(BxEvent.MKB_UPDATED, () => {
|
||||
EmulatedMkbHandler.getInstance()?.refreshPresetData();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
110
src/modules/mkb/mkb-popup.ts
Executable file
110
src/modules/mkb/mkb-popup.ts
Executable file
@@ -0,0 +1,110 @@
|
||||
import { CE, createButton, ButtonStyle, type BxButtonOptions } from "@/utils/html";
|
||||
import { t } from "@/utils/translation";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { SettingsDialog } from "../ui/dialog/settings-dialog";
|
||||
import type { MkbHandler } from "./base-mkb-handler";
|
||||
import { NativeMkbHandler } from "./native-mkb-handler";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
|
||||
type MkbPopupType = 'virtual' | 'native';
|
||||
|
||||
export class MkbPopup {
|
||||
private static instance: MkbPopup;
|
||||
public static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup());
|
||||
|
||||
private popupType!: MkbPopupType;
|
||||
private $popup!: HTMLElement;
|
||||
private $title!: HTMLElement;
|
||||
private $btnActivate!: HTMLButtonElement;
|
||||
|
||||
private mkbHandler!: MkbHandler;
|
||||
|
||||
constructor() {
|
||||
this.render();
|
||||
|
||||
window.addEventListener(BxEvent.KEYBOARD_SHORTCUTS_UPDATED, e => {
|
||||
const $newButton = this.createActivateButton();
|
||||
this.$btnActivate.replaceWith($newButton);
|
||||
this.$btnActivate = $newButton;
|
||||
});
|
||||
}
|
||||
|
||||
attachMkbHandler(handler: MkbHandler) {
|
||||
this.mkbHandler = handler;
|
||||
|
||||
// Set popupType
|
||||
this.popupType = (handler instanceof NativeMkbHandler) ? 'native' : 'virtual';
|
||||
this.$popup.dataset.type = this.popupType;
|
||||
|
||||
// Update popup title
|
||||
this.$title.innerText = t(this.popupType === 'native' ? 'native-mkb' : 'virtual-controller');
|
||||
}
|
||||
|
||||
toggleVisibility(show: boolean) {
|
||||
this.$popup.classList.toggle('bx-gone', !show);
|
||||
show && this.moveOffscreen(false);
|
||||
}
|
||||
|
||||
moveOffscreen(doMove: boolean) {
|
||||
this.$popup.classList.toggle('bx-offscreen', doMove);
|
||||
}
|
||||
|
||||
private createActivateButton() {
|
||||
const options: BxButtonOptions = {
|
||||
style: ButtonStyle.PRIMARY | ButtonStyle.TALL | ButtonStyle.FULL_WIDTH,
|
||||
label: t('activate'),
|
||||
onClick: this.onActivate,
|
||||
};
|
||||
|
||||
// Find shortcut key
|
||||
const shortcutKey = StreamSettings.findKeyboardShortcut(ShortcutAction.MKB_TOGGLE);
|
||||
if (shortcutKey) {
|
||||
options.secondaryText = t('press-key-to-toggle-mkb', { key: KeyHelper.codeToKeyName(shortcutKey) });
|
||||
}
|
||||
|
||||
return createButton(options);
|
||||
}
|
||||
|
||||
private onActivate = (e: Event) => {
|
||||
e.preventDefault();
|
||||
this.mkbHandler.toggle(true);
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.$popup = CE('div', { class: 'bx-mkb-pointer-lock-msg bx-gone' },
|
||||
this.$title = CE('p'),
|
||||
this.$btnActivate = this.createActivateButton(),
|
||||
|
||||
CE('div', {},
|
||||
createButton({
|
||||
label: t('ignore'),
|
||||
style: ButtonStyle.GHOST,
|
||||
onClick: e => {
|
||||
e.preventDefault();
|
||||
this.mkbHandler.toggle(false);
|
||||
this.mkbHandler.waitForMouseData(false);
|
||||
},
|
||||
}),
|
||||
|
||||
createButton({
|
||||
label: t('manage'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: () => {
|
||||
const dialog = SettingsDialog.getInstance();
|
||||
dialog.focusTab('mkb');
|
||||
dialog.show();
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
document.documentElement.appendChild(this.$popup);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.toggleVisibility(true);
|
||||
this.moveOffscreen(false);
|
||||
}
|
||||
}
|
@@ -1,135 +0,0 @@
|
||||
import { t } from "@utils/translation";
|
||||
import { GamepadKey, MouseButtonCode, MouseMapTo, MkbPresetKey } from "@enums/mkb";
|
||||
import { EmulatedMkbHandler } from "./mkb-handler";
|
||||
import type { MkbPresetData, MkbConvertedPresetData } from "@/types/mkb";
|
||||
import type { PreferenceSettings } from "@/types/preferences";
|
||||
import { SettingElementType } from "@/utils/setting-element";
|
||||
|
||||
|
||||
export class MkbPreset {
|
||||
static MOUSE_SETTINGS: PreferenceSettings = {
|
||||
[MkbPresetKey.MOUSE_MAP_TO]: {
|
||||
label: t('map-mouse-to'),
|
||||
type: SettingElementType.OPTIONS,
|
||||
default: MouseMapTo[MouseMapTo.RS],
|
||||
options: {
|
||||
[MouseMapTo[MouseMapTo.RS]]: t('right-stick'),
|
||||
[MouseMapTo[MouseMapTo.LS]]: t('left-stick'),
|
||||
[MouseMapTo[MouseMapTo.OFF]]: t('off'),
|
||||
},
|
||||
},
|
||||
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: {
|
||||
label: t('horizontal-sensitivity'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 50,
|
||||
min: 1,
|
||||
max: 300,
|
||||
|
||||
params: {
|
||||
suffix: '%',
|
||||
exactTicks: 50,
|
||||
},
|
||||
},
|
||||
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_X]: {
|
||||
label: t('vertical-sensitivity'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 50,
|
||||
min: 1,
|
||||
max: 300,
|
||||
|
||||
params: {
|
||||
suffix: '%',
|
||||
exactTicks: 50,
|
||||
},
|
||||
},
|
||||
|
||||
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: {
|
||||
label: t('deadzone-counterweight'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 20,
|
||||
min: 1,
|
||||
max: 50,
|
||||
|
||||
params: {
|
||||
suffix: '%',
|
||||
exactTicks: 10,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static DEFAULT_PRESET: MkbPresetData = {
|
||||
'mapping': {
|
||||
// Use "e.code" value from https://keyjs.dev
|
||||
[GamepadKey.UP]: ['ArrowUp'],
|
||||
[GamepadKey.DOWN]: ['ArrowDown'],
|
||||
[GamepadKey.LEFT]: ['ArrowLeft'],
|
||||
[GamepadKey.RIGHT]: ['ArrowRight'],
|
||||
|
||||
[GamepadKey.LS_UP]: ['KeyW'],
|
||||
[GamepadKey.LS_DOWN]: ['KeyS'],
|
||||
[GamepadKey.LS_LEFT]: ['KeyA'],
|
||||
[GamepadKey.LS_RIGHT]: ['KeyD'],
|
||||
|
||||
[GamepadKey.RS_UP]: ['KeyI'],
|
||||
[GamepadKey.RS_DOWN]: ['KeyK'],
|
||||
[GamepadKey.RS_LEFT]: ['KeyJ'],
|
||||
[GamepadKey.RS_RIGHT]: ['KeyL'],
|
||||
|
||||
[GamepadKey.A]: ['Space', 'KeyE'],
|
||||
[GamepadKey.X]: ['KeyR'],
|
||||
[GamepadKey.B]: ['ControlLeft', 'Backspace'],
|
||||
[GamepadKey.Y]: ['KeyV'],
|
||||
|
||||
[GamepadKey.START]: ['Enter'],
|
||||
[GamepadKey.SELECT]: ['Tab'],
|
||||
|
||||
[GamepadKey.LB]: ['KeyC', 'KeyG'],
|
||||
[GamepadKey.RB]: ['KeyQ'],
|
||||
|
||||
[GamepadKey.HOME]: ['Backquote'],
|
||||
|
||||
[GamepadKey.RT]: [MouseButtonCode.LEFT_CLICK],
|
||||
[GamepadKey.LT]: [MouseButtonCode.RIGHT_CLICK],
|
||||
|
||||
[GamepadKey.L3]: ['ShiftLeft'],
|
||||
[GamepadKey.R3]: ['KeyF'],
|
||||
},
|
||||
|
||||
'mouse': {
|
||||
[MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo[MouseMapTo.RS],
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_X]: 100,
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 100,
|
||||
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20,
|
||||
},
|
||||
};
|
||||
|
||||
static convert(preset: MkbPresetData): MkbConvertedPresetData {
|
||||
const obj: MkbConvertedPresetData = {
|
||||
mapping: {},
|
||||
mouse: Object.assign({}, preset.mouse),
|
||||
};
|
||||
|
||||
for (const buttonIndex in preset.mapping) {
|
||||
for (const keyName of preset.mapping[parseInt(buttonIndex)]) {
|
||||
obj.mapping[keyName!] = parseInt(buttonIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-calculate mouse's sensitivities
|
||||
const mouse = obj.mouse;
|
||||
mouse[MkbPresetKey.MOUSE_SENSITIVITY_X] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY;
|
||||
mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY;
|
||||
mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT] *= EmulatedMkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT;
|
||||
|
||||
const mouseMapTo = MouseMapTo[mouse[MkbPresetKey.MOUSE_MAP_TO]!];
|
||||
if (typeof mouseMapTo !== 'undefined') {
|
||||
mouse[MkbPresetKey.MOUSE_MAP_TO] = mouseMapTo;
|
||||
} else {
|
||||
mouse[MkbPresetKey.MOUSE_MAP_TO] = MkbPreset.MOUSE_SETTINGS[MkbPresetKey.MOUSE_MAP_TO].default;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
@@ -1,541 +0,0 @@
|
||||
import { CE, createButton, ButtonStyle, removeChildElements } from "@utils/html";
|
||||
import { t } from "@utils/translation";
|
||||
import { Dialog } from "@modules/dialog";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
import { MkbPreset } from "./mkb-preset";
|
||||
import { EmulatedMkbHandler } from "./mkb-handler";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import type { MkbPresetData, MkbStoredPresets } from "@/types/mkb";
|
||||
import { MkbPresetKey, GamepadKey, GamepadKeyName } from "@enums/mkb";
|
||||
import { deepClone } from "@utils/global";
|
||||
import { SettingElement } from "@/utils/setting-element";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { MkbPresetsDb } from "@/utils/local-db/mkb-presets-db";
|
||||
import { BxLogger } from "@/utils/bx-logger";
|
||||
|
||||
|
||||
type MkbRemapperStates = {
|
||||
currentPresetId: number;
|
||||
presets: MkbStoredPresets;
|
||||
|
||||
editingPresetData?: MkbPresetData | null;
|
||||
isEditing: boolean;
|
||||
};
|
||||
|
||||
export class MkbRemapper {
|
||||
private readonly BUTTON_ORDERS = [
|
||||
GamepadKey.UP,
|
||||
GamepadKey.DOWN,
|
||||
GamepadKey.LEFT,
|
||||
GamepadKey.RIGHT,
|
||||
|
||||
GamepadKey.A,
|
||||
GamepadKey.B,
|
||||
GamepadKey.X,
|
||||
GamepadKey.Y,
|
||||
|
||||
GamepadKey.LB,
|
||||
GamepadKey.RB,
|
||||
GamepadKey.LT,
|
||||
GamepadKey.RT,
|
||||
|
||||
GamepadKey.SELECT,
|
||||
GamepadKey.START,
|
||||
GamepadKey.HOME,
|
||||
|
||||
GamepadKey.L3,
|
||||
GamepadKey.LS_UP,
|
||||
GamepadKey.LS_DOWN,
|
||||
GamepadKey.LS_LEFT,
|
||||
GamepadKey.LS_RIGHT,
|
||||
|
||||
GamepadKey.R3,
|
||||
GamepadKey.RS_UP,
|
||||
GamepadKey.RS_DOWN,
|
||||
GamepadKey.RS_LEFT,
|
||||
GamepadKey.RS_RIGHT,
|
||||
];
|
||||
|
||||
private static instance: MkbRemapper;
|
||||
public static getInstance = () => MkbRemapper.instance ?? (MkbRemapper.instance = new MkbRemapper());
|
||||
private readonly LOG_TAG = 'MkbRemapper';
|
||||
|
||||
private states: MkbRemapperStates = {
|
||||
currentPresetId: 0,
|
||||
presets: {},
|
||||
editingPresetData: null,
|
||||
isEditing: false,
|
||||
};
|
||||
|
||||
private $wrapper!: HTMLElement;
|
||||
private $presetsSelect!: HTMLSelectElement;
|
||||
private $activateButton!: HTMLButtonElement;
|
||||
|
||||
private $currentBindingKey!: HTMLElement;
|
||||
|
||||
private allKeyElements: HTMLElement[] = [];
|
||||
private allMouseElements: {[key in MkbPresetKey]?: HTMLElement} = {};
|
||||
|
||||
bindingDialog: Dialog;
|
||||
|
||||
private constructor() {
|
||||
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||
this.states.currentPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
|
||||
|
||||
this.bindingDialog = new Dialog({
|
||||
className: 'bx-binding-dialog',
|
||||
content: CE('div', {},
|
||||
CE('p', {}, t('press-to-bind')),
|
||||
CE('i', {}, t('press-esc-to-cancel')),
|
||||
),
|
||||
hideCloseButton: true,
|
||||
});
|
||||
}
|
||||
|
||||
private clearEventListeners = () => {
|
||||
window.removeEventListener('keydown', this.onKeyDown);
|
||||
window.removeEventListener('mousedown', this.onMouseDown);
|
||||
window.removeEventListener('wheel', this.onWheel);
|
||||
};
|
||||
|
||||
private bindKey = ($elm: HTMLElement, key: any) => {
|
||||
const buttonIndex = parseInt($elm.dataset.buttonIndex!);
|
||||
const keySlot = parseInt($elm.dataset.keySlot!);
|
||||
|
||||
// Ignore if bind the save key to the same element
|
||||
if ($elm.dataset.keyCode! === key.code) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unbind duplicated keys
|
||||
for (const $otherElm of this.allKeyElements) {
|
||||
if ($otherElm.dataset.keyCode === key.code) {
|
||||
this.unbindKey($otherElm);
|
||||
}
|
||||
}
|
||||
|
||||
this.states.editingPresetData!.mapping[buttonIndex][keySlot] = key.code;
|
||||
$elm.textContent = key.name;
|
||||
$elm.dataset.keyCode = key.code;
|
||||
}
|
||||
|
||||
private unbindKey = ($elm: HTMLElement) => {
|
||||
const buttonIndex = parseInt($elm.dataset.buttonIndex!);
|
||||
const keySlot = parseInt($elm.dataset.keySlot!);
|
||||
|
||||
// Remove key from preset
|
||||
this.states.editingPresetData!.mapping[buttonIndex][keySlot] = null;
|
||||
$elm.textContent = '';
|
||||
delete $elm.dataset.keyCode;
|
||||
}
|
||||
|
||||
private onWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
this.clearEventListeners();
|
||||
|
||||
this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e));
|
||||
window.setTimeout(() => this.bindingDialog.hide(), 200);
|
||||
};
|
||||
|
||||
private onMouseDown = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.clearEventListeners();
|
||||
|
||||
this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e));
|
||||
window.setTimeout(() => this.bindingDialog.hide(), 200);
|
||||
};
|
||||
|
||||
private onKeyDown = (e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.clearEventListeners();
|
||||
|
||||
if (e.code !== 'Escape') {
|
||||
this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e));
|
||||
}
|
||||
|
||||
window.setTimeout(() => this.bindingDialog.hide(), 200);
|
||||
};
|
||||
|
||||
private onBindingKey = (e: MouseEvent) => {
|
||||
if (!this.states.isEditing || e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(e);
|
||||
|
||||
this.$currentBindingKey = e.target as HTMLElement;
|
||||
|
||||
window.addEventListener('keydown', this.onKeyDown);
|
||||
window.addEventListener('mousedown', this.onMouseDown);
|
||||
window.addEventListener('wheel', this.onWheel);
|
||||
|
||||
this.bindingDialog.show({title: this.$currentBindingKey.dataset.prompt!});
|
||||
};
|
||||
|
||||
private onContextMenu = (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (!this.states.isEditing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unbindKey(e.target as HTMLElement);
|
||||
};
|
||||
|
||||
private getPreset = (presetId: number) => {
|
||||
return this.states.presets[presetId];
|
||||
}
|
||||
|
||||
private getCurrentPreset = () => {
|
||||
let preset = this.getPreset(this.states.currentPresetId);
|
||||
if (!preset) {
|
||||
// Get the first preset in the list
|
||||
const firstPresetId = parseInt(Object.keys(this.states.presets)[0]);
|
||||
preset = this.states.presets[firstPresetId];
|
||||
this.states.currentPresetId = firstPresetId;
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, firstPresetId);
|
||||
}
|
||||
|
||||
return preset;
|
||||
}
|
||||
|
||||
private switchPreset = (presetId: number) => {
|
||||
this.states.currentPresetId = presetId;
|
||||
const presetData = this.getCurrentPreset().data;
|
||||
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const buttonIndex = parseInt($elm.dataset.buttonIndex!);
|
||||
const keySlot = parseInt($elm.dataset.keySlot!);
|
||||
|
||||
const buttonKeys = presetData.mapping[buttonIndex];
|
||||
if (buttonKeys && buttonKeys[keySlot]) {
|
||||
$elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]!);
|
||||
$elm.dataset.keyCode = buttonKeys[keySlot]!;
|
||||
} else {
|
||||
$elm.textContent = '';
|
||||
delete $elm.dataset.keyCode;
|
||||
}
|
||||
}
|
||||
|
||||
let key: MkbPresetKey;
|
||||
for (key in this.allMouseElements) {
|
||||
const $elm = this.allMouseElements[key]!;
|
||||
let value = presetData.mouse[key];
|
||||
if (typeof value === 'undefined') {
|
||||
value = MkbPreset.MOUSE_SETTINGS[key].default;
|
||||
}
|
||||
|
||||
'setValue' in $elm && ($elm as any).setValue(value);
|
||||
}
|
||||
|
||||
// Update state of Activate button
|
||||
const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.states.currentPresetId;
|
||||
this.$activateButton.disabled = activated;
|
||||
this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
|
||||
}
|
||||
|
||||
private async refresh() {
|
||||
// Clear presets select
|
||||
removeChildElements(this.$presetsSelect);
|
||||
|
||||
const presets = await MkbPresetsDb.getInstance().getPresets();
|
||||
|
||||
this.states.presets = presets;
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
let defaultPresetId;
|
||||
if (this.states.currentPresetId === 0) {
|
||||
this.states.currentPresetId = parseInt(Object.keys(presets)[0]);
|
||||
|
||||
defaultPresetId = this.states.currentPresetId;
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId);
|
||||
EmulatedMkbHandler.getInstance().refreshPresetData();
|
||||
} else {
|
||||
defaultPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
|
||||
}
|
||||
|
||||
for (let id in presets) {
|
||||
const preset = presets[id];
|
||||
let name = preset.name;
|
||||
if (id === defaultPresetId) {
|
||||
name = `🎮 ` + name;
|
||||
}
|
||||
|
||||
const $options = CE<HTMLOptionElement>('option', {value: id}, name);
|
||||
$options.selected = parseInt(id) === this.states.currentPresetId;
|
||||
|
||||
fragment.appendChild($options);
|
||||
};
|
||||
|
||||
this.$presetsSelect.appendChild(fragment);
|
||||
|
||||
// Update state of Activate button
|
||||
const activated = defaultPresetId === this.states.currentPresetId;
|
||||
this.$activateButton.disabled = activated;
|
||||
this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
|
||||
|
||||
!this.states.isEditing && this.switchPreset(this.states.currentPresetId);
|
||||
}
|
||||
|
||||
private toggleEditing = (force?: boolean) => {
|
||||
this.states.isEditing = typeof force !== 'undefined' ? force : !this.states.isEditing;
|
||||
this.$wrapper.classList.toggle('bx-editing', this.states.isEditing);
|
||||
|
||||
if (this.states.isEditing) {
|
||||
this.states.editingPresetData = deepClone(this.getCurrentPreset().data);
|
||||
} else {
|
||||
this.states.editingPresetData = null;
|
||||
}
|
||||
|
||||
|
||||
const childElements = this.$wrapper.querySelectorAll('select, button, input');
|
||||
for (const $elm of Array.from(childElements)) {
|
||||
if ($elm.parentElement!.parentElement!.classList.contains('bx-mkb-action-buttons')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let disable = !this.states.isEditing;
|
||||
|
||||
if ($elm.parentElement!.classList.contains('bx-mkb-preset-tools')) {
|
||||
disable = !disable;
|
||||
}
|
||||
|
||||
($elm as HTMLButtonElement).disabled = disable;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.$wrapper = CE('div', {class: 'bx-mkb-settings'});
|
||||
|
||||
this.$presetsSelect = CE<HTMLSelectElement>('select', {tabindex: -1});
|
||||
this.$presetsSelect.addEventListener('change', e => {
|
||||
this.switchPreset(parseInt((e.target as HTMLSelectElement).value));
|
||||
});
|
||||
|
||||
const promptNewName = (value: string) => {
|
||||
let newName: string | null = '';
|
||||
while (!newName) {
|
||||
newName = prompt(t('prompt-preset-name'), value);
|
||||
if (newName === null) {
|
||||
return false;
|
||||
}
|
||||
newName = newName.trim();
|
||||
}
|
||||
|
||||
return newName ? newName : false;
|
||||
};
|
||||
|
||||
const $header = CE('div', {class: 'bx-mkb-preset-tools'},
|
||||
this.$presetsSelect,
|
||||
// Rename button
|
||||
createButton({
|
||||
title: t('rename'),
|
||||
icon: BxIcon.CURSOR_TEXT,
|
||||
tabIndex: -1,
|
||||
onClick: async () => {
|
||||
const preset = this.getCurrentPreset();
|
||||
|
||||
let newName = promptNewName(preset.name);
|
||||
if (!newName || newName === preset.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update preset with new name
|
||||
preset.name = newName;
|
||||
|
||||
await MkbPresetsDb.getInstance().updatePreset(preset);
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
|
||||
// New button
|
||||
createButton({
|
||||
icon: BxIcon.NEW,
|
||||
title: t('new'),
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
let newName = promptNewName('');
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new preset selected name
|
||||
MkbPresetsDb.getInstance().newPreset(newName, MkbPreset.DEFAULT_PRESET).then(id => {
|
||||
this.states.currentPresetId = id;
|
||||
this.refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
// Copy button
|
||||
createButton({
|
||||
icon: BxIcon.COPY,
|
||||
title: t('copy'),
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
const preset = this.getCurrentPreset();
|
||||
|
||||
let newName = promptNewName(`${preset.name} (2)`);
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new preset selected name
|
||||
MkbPresetsDb.getInstance().newPreset(newName, preset.data).then(id => {
|
||||
this.states.currentPresetId = id;
|
||||
this.refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
// Delete button
|
||||
createButton({
|
||||
icon: BxIcon.TRASH,
|
||||
style: ButtonStyle.DANGER,
|
||||
title: t('delete'),
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
if (!confirm(t('confirm-delete-preset'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
MkbPresetsDb.getInstance().deletePreset(this.states.currentPresetId).then(id => {
|
||||
this.states.currentPresetId = 0;
|
||||
this.refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
this.$wrapper.appendChild($header);
|
||||
|
||||
const $rows = CE('div', {class: 'bx-mkb-settings-rows'},
|
||||
CE('i', {class: 'bx-mkb-note'}, t('right-click-to-unbind')),
|
||||
);
|
||||
|
||||
// Render keys
|
||||
const keysPerButton = 2;
|
||||
for (const buttonIndex of this.BUTTON_ORDERS) {
|
||||
const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex];
|
||||
|
||||
let $elm;
|
||||
const $fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < keysPerButton; i++) {
|
||||
$elm = CE('button', {
|
||||
type: 'button',
|
||||
'data-prompt': buttonPrompt,
|
||||
'data-button-index': buttonIndex,
|
||||
'data-key-slot': i,
|
||||
}, ' ');
|
||||
|
||||
$elm.addEventListener('mouseup', this.onBindingKey);
|
||||
$elm.addEventListener('contextmenu', this.onContextMenu);
|
||||
|
||||
$fragment.appendChild($elm);
|
||||
this.allKeyElements.push($elm);
|
||||
}
|
||||
|
||||
const $keyRow = CE('div', {class: 'bx-mkb-key-row'},
|
||||
CE('label', {title: buttonName}, buttonPrompt),
|
||||
$fragment,
|
||||
);
|
||||
|
||||
$rows.appendChild($keyRow);
|
||||
}
|
||||
|
||||
$rows.appendChild(CE('i', {class: 'bx-mkb-note'}, t('mkb-adjust-ingame-settings')),);
|
||||
|
||||
// Render mouse settings
|
||||
const $mouseSettings = document.createDocumentFragment();
|
||||
|
||||
for (const key in MkbPreset.MOUSE_SETTINGS) {
|
||||
const setting = MkbPreset.MOUSE_SETTINGS[key];
|
||||
const value = setting.default;
|
||||
|
||||
let $elm;
|
||||
const onChange = (e: Event, value: any) => {
|
||||
(this.states.editingPresetData!.mouse as any)[key] = value;
|
||||
};
|
||||
const $row = CE('label', {
|
||||
class: 'bx-settings-row',
|
||||
for: `bx_setting_${key}`
|
||||
},
|
||||
CE('span', {class: 'bx-settings-label'}, setting.label),
|
||||
$elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params),
|
||||
);
|
||||
|
||||
$mouseSettings.appendChild($row);
|
||||
this.allMouseElements[key as MkbPresetKey] = $elm;
|
||||
}
|
||||
|
||||
$rows.appendChild($mouseSettings);
|
||||
this.$wrapper.appendChild($rows);
|
||||
|
||||
// Render action buttons
|
||||
const $actionButtons = CE('div', {class: 'bx-mkb-action-buttons'},
|
||||
CE('div', {},
|
||||
// Edit button
|
||||
createButton({
|
||||
label: t('edit'),
|
||||
tabIndex: -1,
|
||||
onClick: e => this.toggleEditing(true),
|
||||
}),
|
||||
|
||||
// Activate button
|
||||
this.$activateButton = createButton({
|
||||
label: t('activate'),
|
||||
style: ButtonStyle.PRIMARY,
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.states.currentPresetId);
|
||||
EmulatedMkbHandler.getInstance().refreshPresetData();
|
||||
|
||||
this.refresh();
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
||||
CE('div', {},
|
||||
// Cancel button
|
||||
createButton({
|
||||
label: t('cancel'),
|
||||
style: ButtonStyle.GHOST,
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
// Restore preset
|
||||
this.switchPreset(this.states.currentPresetId);
|
||||
this.toggleEditing(false);
|
||||
},
|
||||
}),
|
||||
|
||||
// Save button
|
||||
createButton({
|
||||
label: t('save'),
|
||||
style: ButtonStyle.PRIMARY,
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
const updatedPreset = deepClone(this.getCurrentPreset());
|
||||
updatedPreset.data = this.states.editingPresetData as MkbPresetData;
|
||||
|
||||
MkbPresetsDb.getInstance().updatePreset(updatedPreset).then(id => {
|
||||
// If this is the default preset => refresh preset data
|
||||
if (id === getPref(PrefKey.MKB_DEFAULT_PRESET_ID)) {
|
||||
EmulatedMkbHandler.getInstance().refreshPresetData();
|
||||
}
|
||||
|
||||
this.toggleEditing(false);
|
||||
this.refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
this.$wrapper.appendChild($actionButtons);
|
||||
|
||||
this.toggleEditing(false);
|
||||
this.refresh();
|
||||
return this.$wrapper;
|
||||
}
|
||||
}
|
54
src/modules/mkb/mouse-cursor-hider.ts
Normal file → Executable file
54
src/modules/mkb/mouse-cursor-hider.ts
Normal file → Executable file
@@ -1,34 +1,52 @@
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
export class MouseCursorHider {
|
||||
static #timeout: number | null;
|
||||
static #cursorVisible = true;
|
||||
private static instance: MouseCursorHider | null | undefined;
|
||||
public static getInstance(): typeof MouseCursorHider['instance'] {
|
||||
if (typeof MouseCursorHider.instance === 'undefined') {
|
||||
if (!getPref(PrefKey.MKB_ENABLED) && getPref(PrefKey.MKB_HIDE_IDLE_CURSOR)) {
|
||||
MouseCursorHider.instance = new MouseCursorHider();
|
||||
} else {
|
||||
MouseCursorHider.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
static show() {
|
||||
return MouseCursorHider.instance;
|
||||
}
|
||||
|
||||
private timeoutId!: number | null;
|
||||
private isCursorVisible = true;
|
||||
|
||||
show() {
|
||||
document.body && (document.body.style.cursor = 'unset');
|
||||
MouseCursorHider.#cursorVisible = true;
|
||||
this.isCursorVisible = true;
|
||||
}
|
||||
|
||||
static hide() {
|
||||
hide() {
|
||||
document.body && (document.body.style.cursor = 'none');
|
||||
MouseCursorHider.#timeout = null;
|
||||
MouseCursorHider.#cursorVisible = false;
|
||||
this.timeoutId = null;
|
||||
this.isCursorVisible = false;
|
||||
}
|
||||
|
||||
static onMouseMove(e: MouseEvent) {
|
||||
onMouseMove = (e: MouseEvent) => {
|
||||
// Toggle cursor
|
||||
!MouseCursorHider.#cursorVisible && MouseCursorHider.show();
|
||||
!this.isCursorVisible && this.show();
|
||||
// Setup timeout
|
||||
MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout);
|
||||
MouseCursorHider.#timeout = window.setTimeout(MouseCursorHider.hide, 3000);
|
||||
this.timeoutId && clearTimeout(this.timeoutId);
|
||||
this.timeoutId = window.setTimeout(this.hide, 3000);
|
||||
}
|
||||
|
||||
static start() {
|
||||
MouseCursorHider.show();
|
||||
document.addEventListener('mousemove', MouseCursorHider.onMouseMove);
|
||||
start() {
|
||||
this.show();
|
||||
document.addEventListener('mousemove', this.onMouseMove);
|
||||
}
|
||||
|
||||
static stop() {
|
||||
MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout);
|
||||
document.removeEventListener('mousemove', MouseCursorHider.onMouseMove);
|
||||
MouseCursorHider.show();
|
||||
stop() {
|
||||
this.timeoutId && clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
|
||||
document.removeEventListener('mousemove', this.onMouseMove);
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
232
src/modules/mkb/native-mkb-handler.ts
Normal file → Executable file
232
src/modules/mkb/native-mkb-handler.ts
Normal file → Executable file
@@ -4,10 +4,14 @@ import { AppInterface, STATES } from "@/utils/global";
|
||||
import { MkbHandler } from "./base-mkb-handler";
|
||||
import { t } from "@/utils/translation";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { ButtonStyle, CE, createButton } from "@/utils/html";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { BxLogger } from "@/utils/bx-logger";
|
||||
import { MkbPopup } from "./mkb-popup";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { NativeMkbMode } from "@/enums/pref-values";
|
||||
|
||||
type NativeMouseData = {
|
||||
X: number,
|
||||
@@ -15,7 +19,7 @@ type NativeMouseData = {
|
||||
Buttons: number,
|
||||
WheelX: number,
|
||||
WheelY: number,
|
||||
Type? : 0, // 0: Relative, 1: Absolute
|
||||
Type?: 0, // 0: Relative, 1: Absolute
|
||||
}
|
||||
|
||||
type XcloudInputSink = {
|
||||
@@ -23,30 +27,47 @@ type XcloudInputSink = {
|
||||
}
|
||||
|
||||
export class NativeMkbHandler extends MkbHandler {
|
||||
private static instance: NativeMkbHandler;
|
||||
public static getInstance = () => NativeMkbHandler.instance ?? (NativeMkbHandler.instance = new NativeMkbHandler());
|
||||
private static instance: NativeMkbHandler | null | undefined;
|
||||
public static getInstance(): typeof NativeMkbHandler['instance'] {
|
||||
if (typeof NativeMkbHandler.instance === 'undefined') {
|
||||
if (NativeMkbHandler.isAllowed()) {
|
||||
NativeMkbHandler.instance = new NativeMkbHandler();
|
||||
} else {
|
||||
NativeMkbHandler.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
return NativeMkbHandler.instance;
|
||||
}
|
||||
private readonly LOG_TAG = 'NativeMkbHandler';
|
||||
|
||||
#pointerClient: PointerClient | undefined;
|
||||
#enabled: boolean = false;
|
||||
static isAllowed = () => {
|
||||
return STATES.browser.capabilities.emulatedNativeMkb && getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON;
|
||||
}
|
||||
|
||||
#mouseButtonsPressed = 0;
|
||||
#mouseWheelX = 0;
|
||||
#mouseWheelY = 0;
|
||||
private pointerClient: PointerClient | undefined;
|
||||
private enabled = false;
|
||||
|
||||
#mouseVerticalMultiply = 0;
|
||||
#mouseHorizontalMultiply = 0;
|
||||
private mouseButtonsPressed = 0;
|
||||
private mouseWheelX = 0;
|
||||
private mouseWheelY = 0;
|
||||
|
||||
#inputSink: XcloudInputSink | undefined;
|
||||
private mouseVerticalMultiply = 0;
|
||||
private mouseHorizontalMultiply = 0;
|
||||
|
||||
#$message?: HTMLElement;
|
||||
private inputSink: XcloudInputSink | undefined;
|
||||
|
||||
private popup!: MkbPopup;
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||
|
||||
this.popup = MkbPopup.getInstance();
|
||||
this.popup.attachMkbHandler(this);
|
||||
}
|
||||
|
||||
#onKeyboardEvent(e: KeyboardEvent) {
|
||||
private onKeyboardEvent(e: KeyboardEvent) {
|
||||
if (e.type === 'keyup' && e.code === 'F8') {
|
||||
e.preventDefault();
|
||||
this.toggle();
|
||||
@@ -54,110 +75,63 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
}
|
||||
}
|
||||
|
||||
#onPointerLockRequested(e: Event) {
|
||||
private onPointerLockRequested(e: Event) {
|
||||
AppInterface.requestPointerCapture();
|
||||
this.start();
|
||||
}
|
||||
|
||||
#onPointerLockExited(e: Event) {
|
||||
private onPointerLockExited(e: Event) {
|
||||
AppInterface.releasePointerCapture();
|
||||
this.stop();
|
||||
}
|
||||
|
||||
#onPollingModeChanged = (e: Event) => {
|
||||
if (!this.#$message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = (e as any).mode;
|
||||
if (mode === 'none') {
|
||||
this.#$message.classList.remove('bx-offscreen');
|
||||
} else {
|
||||
this.#$message.classList.add('bx-offscreen');
|
||||
}
|
||||
private onPollingModeChanged = (e: Event) => {
|
||||
const move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none';
|
||||
this.popup.moveOffscreen(move);
|
||||
}
|
||||
|
||||
#onDialogShown = () => {
|
||||
private onDialogShown = () => {
|
||||
document.pointerLockElement && document.exitPointerLock();
|
||||
}
|
||||
|
||||
#initMessage() {
|
||||
if (!this.#$message) {
|
||||
this.#$message = CE('div', {'class': 'bx-mkb-pointer-lock-msg'},
|
||||
CE('div', {},
|
||||
CE('p', {}, t('native-mkb')),
|
||||
CE('p', {}, t('press-key-to-toggle-mkb', {key: 'F8'})),
|
||||
),
|
||||
|
||||
CE('div', {'data-type': 'native'},
|
||||
createButton({
|
||||
style: ButtonStyle.PRIMARY | ButtonStyle.FULL_WIDTH | ButtonStyle.TALL,
|
||||
label: t('activate'),
|
||||
onClick: ((e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.toggle(true);
|
||||
}).bind(this),
|
||||
}),
|
||||
|
||||
createButton({
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH,
|
||||
label: t('ignore'),
|
||||
onClick: e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.#$message?.classList.add('bx-gone');
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.#$message.isConnected) {
|
||||
document.documentElement.appendChild(this.#$message);
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(event: Event) {
|
||||
switch (event.type) {
|
||||
case 'keyup':
|
||||
this.#onKeyboardEvent(event as KeyboardEvent);
|
||||
this.onKeyboardEvent(event as KeyboardEvent);
|
||||
break;
|
||||
|
||||
case BxEvent.XCLOUD_DIALOG_SHOWN:
|
||||
this.#onDialogShown();
|
||||
this.onDialogShown();
|
||||
break;
|
||||
|
||||
case BxEvent.POINTER_LOCK_REQUESTED:
|
||||
this.#onPointerLockRequested(event);
|
||||
this.onPointerLockRequested(event);
|
||||
break;
|
||||
case BxEvent.POINTER_LOCK_EXITED:
|
||||
this.#onPointerLockExited(event);
|
||||
this.onPointerLockExited(event);
|
||||
break;
|
||||
|
||||
case BxEvent.XCLOUD_POLLING_MODE_CHANGED:
|
||||
this.#onPollingModeChanged(event);
|
||||
this.onPollingModeChanged(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.#pointerClient = PointerClient.getInstance();
|
||||
this.#inputSink = window.BX_EXPOSED.inputSink;
|
||||
this.pointerClient = PointerClient.getInstance();
|
||||
this.inputSink = window.BX_EXPOSED.inputSink;
|
||||
|
||||
// Stop keyboard input at startup
|
||||
this.#updateInputConfigurationAsync(false);
|
||||
this.updateInputConfigurationAsync(false);
|
||||
|
||||
try {
|
||||
this.#pointerClient.start(STATES.pointerServerPort, this);
|
||||
this.pointerClient.start(STATES.pointerServerPort, this);
|
||||
} catch (e) {
|
||||
Toast.show('Cannot enable Mouse & Keyboard feature');
|
||||
}
|
||||
|
||||
this.#mouseVerticalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY);
|
||||
this.#mouseHorizontalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY);
|
||||
this.mouseVerticalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY);
|
||||
this.mouseHorizontalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY);
|
||||
|
||||
window.addEventListener('keyup', this);
|
||||
|
||||
@@ -166,14 +140,13 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this);
|
||||
window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this);
|
||||
|
||||
this.#initMessage();
|
||||
|
||||
if (AppInterface) {
|
||||
Toast.show(t('press-key-to-toggle-mkb', {key: `<b>F8</b>`}), t('native-mkb'), {html: true});
|
||||
this.#$message?.classList.add('bx-gone');
|
||||
} else {
|
||||
this.#$message?.classList.remove('bx-gone');
|
||||
const shortcutKey = StreamSettings.findKeyboardShortcut(ShortcutAction.MKB_TOGGLE);
|
||||
if (shortcutKey) {
|
||||
const msg = t('press-key-to-toggle-mkb', { key: `<b>${KeyHelper.codeToKeyName(shortcutKey)}</b>` });
|
||||
Toast.show(msg, t('native-mkb'), { html: true });
|
||||
}
|
||||
|
||||
this.waitForMouseData(false);
|
||||
}
|
||||
|
||||
toggle(force?: boolean) {
|
||||
@@ -181,7 +154,7 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
if (typeof force !== 'undefined') {
|
||||
setEnable = force;
|
||||
} else {
|
||||
setEnable = !this.#enabled;
|
||||
setEnable = !this.enabled;
|
||||
}
|
||||
|
||||
if (setEnable) {
|
||||
@@ -191,7 +164,7 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
}
|
||||
}
|
||||
|
||||
#updateInputConfigurationAsync(enabled: boolean) {
|
||||
private updateInputConfigurationAsync(enabled: boolean) {
|
||||
window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({
|
||||
enableKeyboardInput: enabled,
|
||||
enableMouseInput: enabled,
|
||||
@@ -201,27 +174,27 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
}
|
||||
|
||||
start() {
|
||||
this.#resetMouseInput();
|
||||
this.#enabled = true;
|
||||
this.resetMouseInput();
|
||||
this.enabled = true;
|
||||
|
||||
this.#updateInputConfigurationAsync(true);
|
||||
this.updateInputConfigurationAsync(true);
|
||||
|
||||
window.BX_EXPOSED.stopTakRendering = true;
|
||||
this.#$message?.classList.add('bx-gone');
|
||||
this.waitForMouseData(false);
|
||||
|
||||
Toast.show(t('native-mkb'), t('enabled'), {instant: true});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.#resetMouseInput();
|
||||
this.#enabled = false;
|
||||
this.#updateInputConfigurationAsync(false);
|
||||
this.resetMouseInput();
|
||||
this.enabled = false;
|
||||
this.updateInputConfigurationAsync(false);
|
||||
|
||||
this.#$message?.classList.remove('bx-gone');
|
||||
this.waitForMouseData(true);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.#pointerClient?.stop();
|
||||
this.pointerClient?.stop();
|
||||
window.removeEventListener('keyup', this);
|
||||
|
||||
window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this);
|
||||
@@ -229,16 +202,16 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this);
|
||||
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this);
|
||||
|
||||
this.#$message?.classList.add('bx-gone');
|
||||
this.waitForMouseData(false);
|
||||
}
|
||||
|
||||
handleMouseMove(data: MkbMouseMove): void {
|
||||
this.#sendMouseInput({
|
||||
this.sendMouseInput({
|
||||
X: data.movementX,
|
||||
Y: data.movementY,
|
||||
Buttons: this.#mouseButtonsPressed,
|
||||
WheelX: this.#mouseWheelX,
|
||||
WheelY: this.#mouseWheelY,
|
||||
Buttons: this.mouseButtonsPressed,
|
||||
WheelX: this.mouseWheelX,
|
||||
WheelY: this.mouseWheelY,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -246,71 +219,72 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
const { pointerButton, pressed } = data;
|
||||
|
||||
if (pressed) {
|
||||
this.#mouseButtonsPressed |= pointerButton!;
|
||||
this.mouseButtonsPressed |= pointerButton!;
|
||||
} else {
|
||||
this.#mouseButtonsPressed ^= pointerButton!;
|
||||
this.mouseButtonsPressed ^= pointerButton!;
|
||||
}
|
||||
this.#mouseButtonsPressed = Math.max(0, this.#mouseButtonsPressed);
|
||||
this.mouseButtonsPressed = Math.max(0, this.mouseButtonsPressed);
|
||||
|
||||
this.#sendMouseInput({
|
||||
this.sendMouseInput({
|
||||
X: 0,
|
||||
Y: 0,
|
||||
Buttons: this.#mouseButtonsPressed,
|
||||
WheelX: this.#mouseWheelX,
|
||||
WheelY: this.#mouseWheelY,
|
||||
Buttons: this.mouseButtonsPressed,
|
||||
WheelX: this.mouseWheelX,
|
||||
WheelY: this.mouseWheelY,
|
||||
});
|
||||
}
|
||||
|
||||
handleMouseWheel(data: MkbMouseWheel): boolean {
|
||||
const { vertical, horizontal } = data;
|
||||
|
||||
this.#mouseWheelX = horizontal;
|
||||
if (this.#mouseHorizontalMultiply && this.#mouseHorizontalMultiply !== 1) {
|
||||
this.#mouseWheelX *= this.#mouseHorizontalMultiply;
|
||||
this.mouseWheelX = horizontal;
|
||||
if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) {
|
||||
this.mouseWheelX *= this.mouseHorizontalMultiply;
|
||||
}
|
||||
|
||||
this.#mouseWheelY = vertical;
|
||||
if (this.#mouseVerticalMultiply && this.#mouseVerticalMultiply !== 1) {
|
||||
this.#mouseWheelY *= this.#mouseVerticalMultiply;
|
||||
this.mouseWheelY = vertical;
|
||||
if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) {
|
||||
this.mouseWheelY *= this.mouseVerticalMultiply;
|
||||
}
|
||||
|
||||
this.#sendMouseInput({
|
||||
this.sendMouseInput({
|
||||
X: 0,
|
||||
Y: 0,
|
||||
Buttons: this.#mouseButtonsPressed,
|
||||
WheelX: this.#mouseWheelX,
|
||||
WheelY: this.#mouseWheelY,
|
||||
Buttons: this.mouseButtonsPressed,
|
||||
WheelX: this.mouseWheelX,
|
||||
WheelY: this.mouseWheelY,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setVerticalScrollMultiplier(vertical: number) {
|
||||
this.#mouseVerticalMultiply = vertical;
|
||||
this.mouseVerticalMultiply = vertical;
|
||||
}
|
||||
|
||||
setHorizontalScrollMultiplier(horizontal: number) {
|
||||
this.#mouseHorizontalMultiply = horizontal;
|
||||
this.mouseHorizontalMultiply = horizontal;
|
||||
}
|
||||
|
||||
waitForMouseData(enabled: boolean): void {
|
||||
waitForMouseData(showPopup: boolean) {
|
||||
this.popup.toggleVisibility(showPopup);
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.#enabled;
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
#sendMouseInput(data: NativeMouseData) {
|
||||
private sendMouseInput(data: NativeMouseData) {
|
||||
data.Type = 0; // Relative
|
||||
this.#inputSink?.onMouseInput(data);
|
||||
this.inputSink?.onMouseInput(data);
|
||||
}
|
||||
|
||||
#resetMouseInput() {
|
||||
this.#mouseButtonsPressed = 0;
|
||||
this.#mouseWheelX = 0;
|
||||
this.#mouseWheelY = 0;
|
||||
private resetMouseInput() {
|
||||
this.mouseButtonsPressed = 0;
|
||||
this.mouseWheelX = 0;
|
||||
this.mouseWheelY = 0;
|
||||
|
||||
this.#sendMouseInput({
|
||||
this.sendMouseInput({
|
||||
X: 0,
|
||||
Y: 0,
|
||||
Buttons: 0,
|
||||
|
0
src/modules/mkb/pointer-client.ts
Normal file → Executable file
0
src/modules/mkb/pointer-client.ts
Normal file → Executable file
131
src/modules/patcher.ts
Normal file → Executable file
131
src/modules/patcher.ts
Normal file → Executable file
@@ -1,6 +1,5 @@
|
||||
import { AppInterface, SCRIPT_VERSION, STATES } from "@utils/global";
|
||||
import { SCRIPT_VERSION, STATES } from "@utils/global";
|
||||
import { BX_FLAGS } from "@utils/bx-flags";
|
||||
import { VibrationManager } from "@modules/vibration-manager";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
import { hashCode, renderString } from "@utils/utils";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
@@ -13,13 +12,14 @@ import codeRemotePlayEnable from "./patches/remote-play-enable.js" with { type:
|
||||
import codeRemotePlayKeepAlive from "./patches/remote-play-keep-alive.js" with { type: "text" };
|
||||
import codeVibrationAdjust from "./patches/vibration-adjust.js" with { type: "text" };
|
||||
import { FeatureGates } from "@/utils/feature-gates.js";
|
||||
import { UiSection } from "@/enums/ui-sections.js";
|
||||
import { PrefKey } from "@/enums/pref-keys.js";
|
||||
import { getPref, StreamTouchController } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { GamePassCloudGallery } from "@/enums/game-pass-gallery.js";
|
||||
import { t } from "@/utils/translation.js";
|
||||
import { PrefKey, StorageKey } from "@/enums/pref-keys.js";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { GamePassCloudGallery } from "@/enums/game-pass-gallery";
|
||||
import { t } from "@/utils/translation";
|
||||
import { NativeMkbMode, TouchControllerMode, UiLayout, UiSection } from "@/enums/pref-values";
|
||||
|
||||
type PatchArray = (keyof typeof PATCHES)[];
|
||||
type PathName = keyof typeof PATCHES;
|
||||
type PatchArray = PathName[];
|
||||
|
||||
class PatcherUtils {
|
||||
static indexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number {
|
||||
@@ -117,7 +117,7 @@ const PATCHES = {
|
||||
return false;
|
||||
}
|
||||
|
||||
const layout = getPref(PrefKey.UI_LAYOUT) === 'tv' ? 'tv' : 'default';
|
||||
const layout = getPref<UiLayout>(PrefKey.UI_LAYOUT) === UiLayout.TV ? UiLayout.TV : UiLayout.DEFAULT;
|
||||
return str.replace(text, `?"${layout}":"${layout}"`);
|
||||
},
|
||||
|
||||
@@ -211,7 +211,7 @@ const PATCHES = {
|
||||
|
||||
// Patch polling rate
|
||||
const tmp = str.substring(setTimeoutIndex, setTimeoutIndex + 150);
|
||||
const tmpPatched = tmp.replaceAll('Math.max(0,4-', 'Math.max(0,window.BX_CONTROLLER_POLLING_RATE-');
|
||||
const tmpPatched = tmp.replaceAll('Math.max(0,4-', 'Math.max(0,window.BX_STREAM_SETTINGS.controllerPollingRate - ');
|
||||
str = PatcherUtils.replaceWith(str, setTimeoutIndex, tmp, tmpPatched);
|
||||
|
||||
// Block gamepad stats collecting
|
||||
@@ -268,7 +268,6 @@ logFunc(logTag, '//', logMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
VibrationManager.updateGlobalVars();
|
||||
str = str.replaceAll(text, text + codeVibrationAdjust);
|
||||
return str;
|
||||
},
|
||||
@@ -419,9 +418,9 @@ if (window.BX_EXPOSED.stopTakRendering) {
|
||||
}
|
||||
|
||||
let autoOffCode = '';
|
||||
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.OFF) {
|
||||
if (getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) {
|
||||
autoOffCode = 'return;';
|
||||
} else if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) {
|
||||
} else if (getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF)) {
|
||||
autoOffCode = `
|
||||
const gamepads = window.navigator.getGamepads();
|
||||
let gamepadFound = false;
|
||||
@@ -476,7 +475,7 @@ e.guideUI = null;
|
||||
`;
|
||||
|
||||
// Remove the TAK Edit button when the touch controller is disabled
|
||||
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.OFF) {
|
||||
if (getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) {
|
||||
newCode += 'e.canShowTakHUD = false;';
|
||||
}
|
||||
|
||||
@@ -491,7 +490,8 @@ e.guideUI = null;
|
||||
}
|
||||
|
||||
const newCode = `
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e.toLowerCase()});
|
||||
window.BX_STREAM_SETTINGS.xCloudPollingMode = e.toLowerCase();
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED);
|
||||
`;
|
||||
str = str.replace(text, text + newCode);
|
||||
return str;
|
||||
@@ -587,7 +587,7 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar});
|
||||
return false;
|
||||
}
|
||||
|
||||
const opacity = (getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) / 100).toFixed(1);
|
||||
const opacity = (getPref<TouchControllerDefaultOpacity>(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) / 100).toFixed(1);
|
||||
const newCode = `opacityMultiplier: ${opacity}`;
|
||||
str = str.replace(text, newCode);
|
||||
return str;
|
||||
@@ -648,7 +648,16 @@ true` + text;
|
||||
},
|
||||
|
||||
enableNativeMkb(str: string) {
|
||||
let text = 'e.mouseSupported&&e.keyboardSupported&&e.fullscreenSupported;';
|
||||
// l = t.mouseSupported && t.keyboardSupported && t.fullscreenSupported;
|
||||
let index = str.indexOf('.mouseSupported&&');
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the variable name "t"
|
||||
const varName = str.charAt(index - 1);
|
||||
// Find the full text
|
||||
let text = `${varName}.mouseSupported&&${varName}.keyboardSupported&&${varName}.fullscreenSupported;`;
|
||||
if ((!str.includes(text))) {
|
||||
return false;
|
||||
}
|
||||
@@ -827,7 +836,7 @@ true` + text;
|
||||
return false;
|
||||
}
|
||||
|
||||
const PREF_HIDE_SECTIONS = getPref(PrefKey.UI_HIDE_SECTIONS) as UiSection[];
|
||||
const PREF_HIDE_SECTIONS = getPref<UiSection[]>(PrefKey.UI_HIDE_SECTIONS);
|
||||
const siglIds: GamePassCloudGallery[] = [];
|
||||
|
||||
const sections: PartialRecord<UiSection, GamePassCloudGallery> = {
|
||||
@@ -906,31 +915,19 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
||||
// product-details-page.js#2388, 24.17.20
|
||||
detectProductDetailsPage(str: string) {
|
||||
let index = str.indexOf('{location:"ProductDetailPage",');
|
||||
index >= 0 && (index = PatcherUtils.lastIndexOf('return', str, index, 200));
|
||||
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
index = str.indexOf('return', index - 40);
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, {component: "product-details"});' + str.substring(index);
|
||||
str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-details" });' + str.substring(index);
|
||||
return str;
|
||||
},
|
||||
|
||||
detectBrowserRouterReady(str: string) {
|
||||
let text = 'BrowserRouter:()=>';
|
||||
if (!str.includes(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let index = str.indexOf('{history:this.history,');
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
index = PatcherUtils.lastIndexOf(str, 'return', index, 100);
|
||||
index >= 0 && (index = PatcherUtils.lastIndexOf(str, 'return', index, 100));
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -998,10 +995,8 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
||||
};
|
||||
|
||||
let PATCH_ORDERS: PatchArray = [
|
||||
...(getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' ? [
|
||||
...(getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
|
||||
'enableNativeMkb',
|
||||
'patchMouseAndKeyboardEnabled',
|
||||
'disableNativeRequestPointerLock',
|
||||
'exposeInputSink',
|
||||
] : []),
|
||||
|
||||
@@ -1023,19 +1018,19 @@ let PATCH_ORDERS: PatchArray = [
|
||||
'guideAchievementsDefaultLocked',
|
||||
|
||||
'enableTvRoutes',
|
||||
AppInterface && 'detectProductDetailsPage',
|
||||
// AppInterface && 'detectProductDetailsPage',
|
||||
|
||||
'supportLocalCoOp',
|
||||
'overrideStorageGetSettings',
|
||||
getPref(PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME) && 'patchSetCurrentlyFocusedInteractable',
|
||||
|
||||
getPref(PrefKey.UI_LAYOUT) !== 'default' && 'websiteLayout',
|
||||
getPref(PrefKey.LOCAL_CO_OP_ENABLED) && 'supportLocalCoOp',
|
||||
getPref<UiLayout>(PrefKey.UI_LAYOUT) !== UiLayout.DEFAULT && 'websiteLayout',
|
||||
getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole',
|
||||
|
||||
getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection',
|
||||
getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection',
|
||||
getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection',
|
||||
(getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections',
|
||||
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection',
|
||||
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection',
|
||||
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection',
|
||||
(getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections',
|
||||
|
||||
...(STATES.userAgent.capabilities.touch ? [
|
||||
'disableTouchContextMenu',
|
||||
@@ -1064,9 +1059,11 @@ let PATCH_ORDERS: PatchArray = [
|
||||
'enableConsoleLogging',
|
||||
'enableXcloudLogger',
|
||||
] : []),
|
||||
].filter(item => !!item);
|
||||
].filter((item): item is string => !!item) as PatchArray;
|
||||
|
||||
// Only when playing
|
||||
// TODO: check this
|
||||
// @ts-ignore
|
||||
let PLAYING_PATCH_ORDERS: PatchArray = [
|
||||
'patchXcloudTitleInfo',
|
||||
'disableGamepadDisconnectedScreen',
|
||||
@@ -1078,18 +1075,18 @@ let PLAYING_PATCH_ORDERS: PatchArray = [
|
||||
// 'exposeEventTarget',
|
||||
|
||||
// Patch volume control for normal stream
|
||||
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && !getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchAudioMediaStream',
|
||||
getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED) && !getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchAudioMediaStream',
|
||||
// Patch volume control for combined audio+video stream
|
||||
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchCombinedAudioVideoMediaStream',
|
||||
getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED) && getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchCombinedAudioVideoMediaStream',
|
||||
|
||||
// Skip feedback dialog
|
||||
getPref(PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG) && 'skipFeedbackDialog',
|
||||
getPref(PrefKey.UI_DISABLE_FEEDBACK_DIALOG) && 'skipFeedbackDialog',
|
||||
|
||||
...(STATES.userAgent.capabilities.touch ? [
|
||||
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.ALL && 'patchShowSensorControls',
|
||||
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.ALL && 'exposeTouchLayoutManager',
|
||||
(getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.OFF || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF) || !STATES.userAgent.capabilities.touch) && 'disableTakRenderer',
|
||||
getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity',
|
||||
getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'patchShowSensorControls',
|
||||
getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'exposeTouchLayoutManager',
|
||||
(getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF || getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF) || !STATES.userAgent.capabilities.touch) && 'disableTakRenderer',
|
||||
getPref<TouchControllerDefaultOpacity>(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity',
|
||||
'patchBabylonRendererClass',
|
||||
] : []),
|
||||
|
||||
@@ -1103,7 +1100,13 @@ let PLAYING_PATCH_ORDERS: PatchArray = [
|
||||
'patchRemotePlayMkb',
|
||||
'remotePlayConnectMode',
|
||||
] : []),
|
||||
].filter(item => !!item);
|
||||
|
||||
// Native MKB
|
||||
...(getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
|
||||
'patchMouseAndKeyboardEnabled',
|
||||
'disableNativeRequestPointerLock',
|
||||
] : []),
|
||||
].filter((item): item is string => !!item);
|
||||
|
||||
const ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS];
|
||||
|
||||
@@ -1138,7 +1141,7 @@ export class Patcher {
|
||||
|
||||
const orgFunc = this;
|
||||
const newFunc = (a: any, item: any) => {
|
||||
Patcher.patch(item);
|
||||
Patcher.checkChunks(item);
|
||||
orgFunc(a, item);
|
||||
}
|
||||
|
||||
@@ -1147,20 +1150,22 @@ export class Patcher {
|
||||
};
|
||||
}
|
||||
|
||||
static patch(item: [[number], { [key: string]: () => {} }]) {
|
||||
static checkChunks(item: [[number], { [key: string]: () => {} }]) {
|
||||
// !!! Use "caches" as variable name will break touch controller???
|
||||
// console.log('patch', '-----');
|
||||
let patchesToCheck: PatchArray;
|
||||
let appliedPatches: PatchArray;
|
||||
|
||||
const chunkData = item[1];
|
||||
const patchesMap: Record<string, PatchArray> = {};
|
||||
const patcherCache = PatcherCache.getInstance();
|
||||
|
||||
for (let id in item[1]) {
|
||||
for (const chunkId in chunkData) {
|
||||
appliedPatches = [];
|
||||
|
||||
const cachedPatches = patcherCache.getPatches(id);
|
||||
const cachedPatches = patcherCache.getPatches(chunkId);
|
||||
if (cachedPatches) {
|
||||
// clone cachedPatches
|
||||
patchesToCheck = cachedPatches.slice(0);
|
||||
patchesToCheck.push(...PATCH_ORDERS);
|
||||
} else {
|
||||
@@ -1172,7 +1177,7 @@ export class Patcher {
|
||||
continue;
|
||||
}
|
||||
|
||||
const func = item[1][id];
|
||||
const func = chunkData[chunkId];
|
||||
const funcStr = func.toString();
|
||||
let patchedFuncStr = funcStr;
|
||||
|
||||
@@ -1211,7 +1216,7 @@ export class Patcher {
|
||||
// Apply patched functions
|
||||
if (modified) {
|
||||
try {
|
||||
item[1][id] = eval(patchedFuncStr);
|
||||
chunkData[chunkId] = eval(patchedFuncStr);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
BxLogger.error(LOG_TAG, 'Error', appliedPatches, e.message, patchedFuncStr);
|
||||
@@ -1221,7 +1226,7 @@ export class Patcher {
|
||||
|
||||
// Save to cache
|
||||
if (appliedPatches.length) {
|
||||
patchesMap[id] = appliedPatches;
|
||||
patchesMap[chunkId] = appliedPatches;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1239,10 +1244,10 @@ export class PatcherCache {
|
||||
private static instance: PatcherCache;
|
||||
public static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache());
|
||||
|
||||
private readonly KEY_CACHE = 'better_xcloud_patches_cache';
|
||||
private readonly KEY_SIGNATURE = 'better_xcloud_patches_cache_signature';
|
||||
private readonly KEY_CACHE = StorageKey.PATCHES_CACHE;
|
||||
private readonly KEY_SIGNATURE = StorageKey.PATCHES_SIGNATURE;
|
||||
|
||||
private CACHE: any;
|
||||
private CACHE!: { [key: string]: PatchArray };
|
||||
|
||||
private isInitialized = false;
|
||||
|
||||
|
2
src/modules/patches/controller-shortcuts.js
Normal file → Executable file
2
src/modules/patches/controller-shortcuts.js
Normal file → Executable file
@@ -85,7 +85,7 @@ if (btnHome) {
|
||||
|
||||
this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings);
|
||||
} else {
|
||||
intervalMs = window.BX_CONTROLLER_POLLING_RATE;
|
||||
intervalMs = window.BX_STREAM_SETTINGS.controllerPollingRate;
|
||||
}
|
||||
}
|
||||
|
||||
|
0
src/modules/patches/expose-stream-session.js
Normal file → Executable file
0
src/modules/patches/expose-stream-session.js
Normal file → Executable file
40
src/modules/patches/local-co-op-enable.js
Normal file → Executable file
40
src/modules/patches/local-co-op-enable.js
Normal file → Executable file
@@ -1,21 +1,57 @@
|
||||
// Save the original onGamepadChanged() and onGamepadInput()
|
||||
this.orgOnGamepadChanged = this.onGamepadChanged;
|
||||
this.orgOnGamepadInput = this.onGamepadInput;
|
||||
|
||||
let match;
|
||||
let onGamepadChangedStr = this.onGamepadChanged.toString();
|
||||
|
||||
// Fix problem with Safari
|
||||
if (onGamepadChangedStr.startsWith('function ')) {
|
||||
onGamepadChangedStr = onGamepadChangedStr.substring(9);
|
||||
}
|
||||
|
||||
onGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]');
|
||||
eval(`this.onGamepadChanged = function ${onGamepadChangedStr}`);
|
||||
eval(`this.patchedOnGamepadChanged = function ${onGamepadChangedStr}`);
|
||||
|
||||
let onGamepadInputStr = this.onGamepadInput.toString();
|
||||
// Fix problem with Safari
|
||||
if (onGamepadInputStr.startsWith('function ')) {
|
||||
onGamepadInputStr = onGamepadInputStr.substring(9);
|
||||
}
|
||||
|
||||
match = onGamepadInputStr.match(/(\w+\.GamepadIndex)/);
|
||||
if (match) {
|
||||
const gamepadIndexVar = match[0];
|
||||
onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', `this.gamepadStates.get(${gamepadIndexVar},`);
|
||||
eval(`this.onGamepadInput = function ${onGamepadInputStr}`);
|
||||
eval(`this.patchedOnGamepadInput = function ${onGamepadInputStr}`);
|
||||
BxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support');
|
||||
} else {
|
||||
BxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support');
|
||||
}
|
||||
|
||||
// Add method to switch between patched and original methods
|
||||
this.toggleLocalCoOp = enable => {
|
||||
BxLogger.info('toggleLocalCoOp', enable ? 'Enabled' : 'Disabled');
|
||||
|
||||
this.onGamepadChanged = enable ? this.patchedOnGamepadChanged : this.orgOnGamepadChanged;
|
||||
this.onGamepadInput = enable ? this.patchedOnGamepadInput : this.orgOnGamepadInput;
|
||||
|
||||
// Reconnect all gamepads
|
||||
const gamepads = window.navigator.getGamepads();
|
||||
for (const gamepad of gamepads) {
|
||||
if (!gamepad?.connected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore virtual controller
|
||||
if (gamepad.id.includes('Better xCloud')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
window.dispatchEvent(new GamepadEvent('gamepaddisconnected', { gamepad }));
|
||||
window.dispatchEvent(new GamepadEvent('gamepadconnected', { gamepad }));
|
||||
}
|
||||
};
|
||||
|
||||
// Expose this method
|
||||
window.BX_EXPOSED.toggleLocalCoOp = this.toggleLocalCoOp.bind(this);
|
||||
|
0
src/modules/patches/remote-play-enable.js
Normal file → Executable file
0
src/modules/patches/remote-play-enable.js
Normal file → Executable file
0
src/modules/patches/remote-play-keep-alive.js
Normal file → Executable file
0
src/modules/patches/remote-play-keep-alive.js
Normal file → Executable file
0
src/modules/patches/set-currently-focused-interactable.js
Normal file → Executable file
0
src/modules/patches/set-currently-focused-interactable.js
Normal file → Executable file
27
src/modules/patches/vibration-adjust.js
Normal file → Executable file
27
src/modules/patches/vibration-adjust.js
Normal file → Executable file
@@ -1,15 +1,16 @@
|
||||
if (!window.BX_ENABLE_CONTROLLER_VIBRATION) {
|
||||
return void(0);
|
||||
}
|
||||
const gamepad = e.gamepad;
|
||||
if (gamepad?.connected) {
|
||||
const gamepadSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id];
|
||||
if (gamepadSettings) {
|
||||
const intensity = gamepadSettings.vibrationIntensity;
|
||||
|
||||
const intensity = window.BX_VIBRATION_INTENSITY;
|
||||
if (intensity === 0) {
|
||||
return void(0);
|
||||
}
|
||||
|
||||
if (intensity < 1) {
|
||||
e.leftMotorPercent *= intensity;
|
||||
e.rightMotorPercent *= intensity;
|
||||
e.leftTriggerMotorPercent *= intensity;
|
||||
e.rightTriggerMotorPercent *= intensity;
|
||||
if (intensity === 0) {
|
||||
return void(e.repeat = 0);
|
||||
} else if (intensity < 1) {
|
||||
e.leftMotorPercent *= intensity;
|
||||
e.rightMotorPercent *= intensity;
|
||||
e.leftTriggerMotorPercent *= intensity;
|
||||
e.rightTriggerMotorPercent *= intensity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
0
src/modules/player/shaders/clarity_boost.fs
Normal file → Executable file
0
src/modules/player/shaders/clarity_boost.fs
Normal file → Executable file
0
src/modules/player/shaders/clarity_boost.vert
Normal file → Executable file
0
src/modules/player/shaders/clarity_boost.vert
Normal file → Executable file
1
src/modules/player/webgl2-player.ts
Normal file → Executable file
1
src/modules/player/webgl2-player.ts
Normal file → Executable file
@@ -115,6 +115,7 @@ export class WebGL2Player {
|
||||
}
|
||||
|
||||
this.animFrameId = frameCallback(animate);
|
||||
|
||||
let draw = true;
|
||||
|
||||
// Don't draw when FPS is 0
|
||||
|
36
src/modules/remote-play-manager.ts
Normal file → Executable file
36
src/modules/remote-play-manager.ts
Normal file → Executable file
@@ -1,4 +1,4 @@
|
||||
import { STATES, AppInterface } from "@utils/global";
|
||||
import { STATES } from "@utils/global";
|
||||
import { Toast } from "@utils/toast";
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { t } from "@utils/translation";
|
||||
@@ -7,7 +7,7 @@ import { BxLogger } from "@utils/bx-logger";
|
||||
import { HeaderSection } from "./ui/header";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { RemotePlayNavigationDialog } from "./ui/dialog/remote-play-dialog";
|
||||
import { RemotePlayDialog } from "./ui/dialog/remote-play-dialog";
|
||||
|
||||
export const enum RemotePlayConsoleState {
|
||||
ON = 'On',
|
||||
@@ -34,8 +34,18 @@ type RemotePlayConsole = {
|
||||
};
|
||||
|
||||
export class RemotePlayManager {
|
||||
private static instance: RemotePlayManager;
|
||||
public static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager());
|
||||
private static instance: RemotePlayManager | null | undefined;
|
||||
public static getInstance(): typeof RemotePlayManager['instance'] {
|
||||
if (typeof RemotePlayManager.instance === 'undefined') {
|
||||
if (getPref(PrefKey.REMOTE_PLAY_ENABLED)) {
|
||||
RemotePlayManager.instance = new RemotePlayManager();
|
||||
} else {
|
||||
RemotePlayManager.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
return RemotePlayManager.instance;
|
||||
}
|
||||
private readonly LOG_TAG = 'RemotePlayManager';
|
||||
|
||||
private isInitialized = false;
|
||||
@@ -57,7 +67,7 @@ export class RemotePlayManager {
|
||||
|
||||
this.isInitialized = true;
|
||||
|
||||
this.getXhomeToken(() => {
|
||||
this.requestXhomeToken(() => {
|
||||
this.getConsolesList(() => {
|
||||
BxLogger.info(this.LOG_TAG, 'Consoles', this.consoles);
|
||||
|
||||
@@ -67,15 +77,15 @@ export class RemotePlayManager {
|
||||
});
|
||||
}
|
||||
|
||||
get xcloudToken() {
|
||||
getXcloudToken() {
|
||||
return this.XCLOUD_TOKEN;
|
||||
}
|
||||
|
||||
set xcloudToken(token: string) {
|
||||
setXcloudToken(token: string) {
|
||||
this.XCLOUD_TOKEN = token;
|
||||
}
|
||||
|
||||
get xhomeToken() {
|
||||
getXhomeToken() {
|
||||
return this.XHOME_TOKEN;
|
||||
}
|
||||
|
||||
@@ -84,7 +94,7 @@ export class RemotePlayManager {
|
||||
}
|
||||
|
||||
|
||||
private getXhomeToken(callback: any) {
|
||||
private requestXhomeToken(callback: any) {
|
||||
if (this.XHOME_TOKEN) {
|
||||
callback();
|
||||
return;
|
||||
@@ -142,7 +152,7 @@ export class RemotePlayManager {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.XHOME_TOKEN}`,
|
||||
Authorization: `Bearer ${this.XHOME_TOKEN}`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -176,7 +186,7 @@ export class RemotePlayManager {
|
||||
|
||||
play(serverId: string, resolution?: string) {
|
||||
if (resolution) {
|
||||
setPref(PrefKey.REMOTE_PLAY_RESOLUTION, resolution);
|
||||
setPref(PrefKey.REMOTE_PLAY_STREAM_RESOLUTION, resolution);
|
||||
}
|
||||
|
||||
STATES.remotePlay.config = {
|
||||
@@ -198,14 +208,16 @@ export class RemotePlayManager {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
// Show native dialog in Android app
|
||||
if (AppInterface && AppInterface.showRemotePlayDialog) {
|
||||
AppInterface.showRemotePlayDialog(JSON.stringify(this.consoles));
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
RemotePlayNavigationDialog.getInstance().show();
|
||||
RemotePlayDialog.getInstance().show();
|
||||
}
|
||||
|
||||
static detect() {
|
||||
|
0
src/modules/shortcuts/shortcut-microphone.ts → src/modules/shortcuts/microphone-shortcut.ts
Normal file → Executable file
0
src/modules/shortcuts/shortcut-microphone.ts → src/modules/shortcuts/microphone-shortcut.ts
Normal file → Executable file
9
src/modules/shortcuts/shortcut-renderer.ts → src/modules/shortcuts/renderer-shortcut.ts
Normal file → Executable file
9
src/modules/shortcuts/shortcut-renderer.ts → src/modules/shortcuts/renderer-shortcut.ts
Normal file → Executable file
@@ -1,18 +1,21 @@
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { limitVideoPlayerFps } from "../stream/stream-settings-utils";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
|
||||
export class RendererShortcut {
|
||||
static toggleVisibility(): boolean {
|
||||
static toggleVisibility() {
|
||||
const $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]');
|
||||
if (!$mediaContainer) {
|
||||
return true;
|
||||
BxEvent.dispatch(window, BxEvent.VIDEO_VISIBILITY_CHANGED, { isShowing: true });
|
||||
return;
|
||||
}
|
||||
|
||||
$mediaContainer.classList.toggle('bx-gone');
|
||||
const isShowing = !$mediaContainer.classList.contains('bx-gone');
|
||||
|
||||
// Switch FPS
|
||||
limitVideoPlayerFps(isShowing ? getPref(PrefKey.VIDEO_MAX_FPS) : 0);
|
||||
return isShowing;
|
||||
BxEvent.dispatch(window, BxEvent.VIDEO_VISIBILITY_CHANGED, { isShowing });
|
||||
}
|
||||
}
|
59
src/modules/shortcuts/shortcut-actions.ts
Executable file
59
src/modules/shortcuts/shortcut-actions.ts
Executable file
@@ -0,0 +1,59 @@
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { AppInterface, STATES } from "@/utils/global";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { t } from "@/utils/translation";
|
||||
|
||||
type ShortcutActions = {
|
||||
[key: string]: {
|
||||
[key in ShortcutAction]?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export const SHORTCUT_ACTIONS: ShortcutActions = {
|
||||
// Script
|
||||
[t('better-xcloud')]: {
|
||||
[ShortcutAction.BETTER_XCLOUD_SETTINGS_SHOW]: [t('settings'), t('show')],
|
||||
},
|
||||
|
||||
// Device
|
||||
...(!!AppInterface ? {
|
||||
[t('device')]: {
|
||||
[ShortcutAction.DEVICE_SOUND_TOGGLE]: [t('sound'), t('toggle')],
|
||||
[ShortcutAction.DEVICE_VOLUME_INC]: [t('volume'), t('increase')],
|
||||
[ShortcutAction.DEVICE_VOLUME_DEC]: [t('volume'), t('decrease')],
|
||||
|
||||
[ShortcutAction.DEVICE_BRIGHTNESS_INC]: [t('brightness'), t('increase')],
|
||||
[ShortcutAction.DEVICE_BRIGHTNESS_DEC]: [t('brightness'), t('decrease')],
|
||||
},
|
||||
} : {}),
|
||||
|
||||
// Stream
|
||||
[t('stream')]: {
|
||||
[ShortcutAction.STREAM_SCREENSHOT_CAPTURE]: [t('take-screenshot')],
|
||||
[ShortcutAction.STREAM_VIDEO_TOGGLE]: [t('video'), t('toggle')],
|
||||
|
||||
[ShortcutAction.STREAM_SOUND_TOGGLE]: [t('sound'), t('toggle')],
|
||||
|
||||
...(getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED) ? {
|
||||
[ShortcutAction.STREAM_VOLUME_INC]: [t('volume'), t('increase')],
|
||||
[ShortcutAction.STREAM_VOLUME_DEC]: [t('volume'), t('decrease')],
|
||||
} : {}),
|
||||
|
||||
[ShortcutAction.STREAM_MENU_SHOW]: [t('menu'), t('show')],
|
||||
[ShortcutAction.STREAM_STATS_TOGGLE]: [t('stats'), t('show-hide')],
|
||||
[ShortcutAction.STREAM_MICROPHONE_TOGGLE]: [t('microphone'), t('toggle')],
|
||||
},
|
||||
|
||||
// MKB
|
||||
...(STATES.browser.capabilities.mkb ? {
|
||||
[t('mouse-and-keyboard')]: {
|
||||
[ShortcutAction.MKB_TOGGLE]: [t('toggle')],
|
||||
},
|
||||
} : {}),
|
||||
|
||||
// Other
|
||||
[t('other')]: {
|
||||
[ShortcutAction.TRUE_ACHIEVEMENTS_OPEN]: [t('true-achievements'), t('show')],
|
||||
},
|
||||
} as const;
|
10
src/modules/shortcuts/shortcut-sound.ts → src/modules/shortcuts/sound-shortcut.ts
Normal file → Executable file
10
src/modules/shortcuts/shortcut-sound.ts → src/modules/shortcuts/sound-shortcut.ts
Normal file → Executable file
@@ -13,11 +13,11 @@ export enum SpeakerState {
|
||||
|
||||
export class SoundShortcut {
|
||||
static adjustGainNodeVolume(amount: number): number {
|
||||
if (!getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL)) {
|
||||
if (!getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const currentValue = getPref(PrefKey.AUDIO_VOLUME);
|
||||
const currentValue = getPref<AudioVolume>(PrefKey.AUDIO_VOLUME);
|
||||
let nearestValue: number;
|
||||
|
||||
if (amount > 0) { // Increase
|
||||
@@ -47,9 +47,9 @@ export class SoundShortcut {
|
||||
}
|
||||
|
||||
static muteUnmute() {
|
||||
if (getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && STATES.currentStream.audioGainNode) {
|
||||
if (getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED) && STATES.currentStream.audioGainNode) {
|
||||
const gainValue = STATES.currentStream.audioGainNode.gain.value;
|
||||
const settingValue = getPref(PrefKey.AUDIO_VOLUME);
|
||||
const settingValue = getPref<AudioVolume>(PrefKey.AUDIO_VOLUME);
|
||||
|
||||
let targetValue: number;
|
||||
if (settingValue === 0) { // settingValue is 0 => set to 100
|
||||
@@ -73,7 +73,7 @@ export class SoundShortcut {
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, {
|
||||
speakerState: targetValue === 0 ? SpeakerState.MUTED : SpeakerState.ENABLED,
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
0
src/modules/shortcuts/shortcut-stream-ui.ts → src/modules/shortcuts/stream-ui-shortcut.ts
Normal file → Executable file
0
src/modules/shortcuts/shortcut-stream-ui.ts → src/modules/shortcuts/stream-ui-shortcut.ts
Normal file → Executable file
4
src/modules/stream-player.ts
Normal file → Executable file
4
src/modules/stream-player.ts
Normal file → Executable file
@@ -3,11 +3,11 @@ import { isFullVersion } from "@macros/build" with {type: "macro"};
|
||||
import { CE } from "@/utils/html";
|
||||
import { WebGL2Player } from "./player/webgl2-player";
|
||||
import { ScreenshotManager } from "@/utils/screenshot-manager";
|
||||
import { StreamPlayerType, StreamVideoProcessing } from "@enums/stream-player";
|
||||
import { STATES } from "@/utils/global";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { BX_FLAGS } from "@/utils/bx-flags";
|
||||
import { StreamPlayerType, StreamVideoProcessing, VideoRatio } from "@/enums/pref-values";
|
||||
|
||||
export type StreamPlayerOptions = Partial<{
|
||||
processing: string,
|
||||
@@ -99,7 +99,7 @@ export class StreamPlayer {
|
||||
}
|
||||
|
||||
private resizePlayer() {
|
||||
const PREF_RATIO = getPref(PrefKey.VIDEO_RATIO);
|
||||
const PREF_RATIO = getPref<VideoRatio>(PrefKey.VIDEO_RATIO);
|
||||
const $video = this.$video;
|
||||
const isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport;
|
||||
|
||||
|
7
src/modules/stream/stream-badges.ts
Normal file → Executable file
7
src/modules/stream/stream-badges.ts
Normal file → Executable file
@@ -7,7 +7,8 @@ import { STATES } from "@utils/global";
|
||||
import { BxLogger } from "@/utils/bx-logger";
|
||||
import { BxIcon } from "@/utils/bx-icon";
|
||||
import { GuideMenuTab } from "../ui/guide-menu";
|
||||
import { StreamStat, StreamStatsCollector } from "@/utils/stream-stats-collector";
|
||||
import { StreamStatsCollector } from "@/utils/stream-stats-collector";
|
||||
import { StreamStat } from "@/enums/pref-values";
|
||||
|
||||
|
||||
type StreamBadgeInfo = {
|
||||
@@ -130,7 +131,7 @@ export class StreamBadges {
|
||||
return $badge;
|
||||
}
|
||||
|
||||
private async updateBadges(forceUpdate = false) {
|
||||
private updateBadges = async (forceUpdate = false) => {
|
||||
if (!this.$container || (!forceUpdate && !this.$container.isConnected)) {
|
||||
this.stop();
|
||||
return;
|
||||
@@ -181,7 +182,7 @@ export class StreamBadges {
|
||||
private async start() {
|
||||
await this.updateBadges(true);
|
||||
this.stop();
|
||||
this.intervalId = window.setInterval(this.updateBadges.bind(this), this.REFRESH_INTERVAL);
|
||||
this.intervalId = window.setInterval(this.updateBadges, this.REFRESH_INTERVAL);
|
||||
}
|
||||
|
||||
private stop() {
|
||||
|
13
src/modules/stream/stream-settings-utils.ts
Normal file → Executable file
13
src/modules/stream/stream-settings-utils.ts
Normal file → Executable file
@@ -1,16 +1,17 @@
|
||||
import { StreamPlayerType, StreamVideoProcessing } from "@enums/stream-player";
|
||||
import { STATES } from "@utils/global";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
import type { StreamPlayerOptions } from "../stream-player";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { StreamVideoProcessing, StreamPlayerType } from "@/enums/pref-values";
|
||||
import { escapeCssSelector } from "@/utils/html";
|
||||
|
||||
export function onChangeVideoPlayerType() {
|
||||
const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE);
|
||||
const $videoProcessing = document.getElementById(`bx_setting_${PrefKey.VIDEO_PROCESSING}`) as HTMLSelectElement;
|
||||
const $videoSharpness = document.getElementById(`bx_setting_${PrefKey.VIDEO_SHARPNESS}`) as HTMLElement;
|
||||
const $videoPowerPreference = document.getElementById(`bx_setting_${PrefKey.VIDEO_POWER_PREFERENCE}`) as HTMLElement;
|
||||
const $videoMaxFps = document.getElementById(`bx_setting_${PrefKey.VIDEO_MAX_FPS}`) as HTMLElement;
|
||||
const playerType = getPref<StreamPlayerType>(PrefKey.VIDEO_PLAYER_TYPE);
|
||||
const $videoProcessing = document.getElementById(`bx_setting_${escapeCssSelector(PrefKey.VIDEO_PROCESSING)}`) as HTMLSelectElement;
|
||||
const $videoSharpness = document.getElementById(`bx_setting_${escapeCssSelector(PrefKey.VIDEO_SHARPNESS)}`) as HTMLElement;
|
||||
const $videoPowerPreference = document.getElementById(`bx_setting_${escapeCssSelector(PrefKey.VIDEO_POWER_PREFERENCE)}`) as HTMLElement;
|
||||
const $videoMaxFps = document.getElementById(`bx_setting_${escapeCssSelector(PrefKey.VIDEO_MAX_FPS)}`) as HTMLElement;
|
||||
|
||||
if (!$videoProcessing) {
|
||||
return;
|
||||
|
13
src/modules/stream/stream-stats.ts
Normal file → Executable file
13
src/modules/stream/stream-stats.ts
Normal file → Executable file
@@ -4,8 +4,9 @@ import { t } from "@utils/translation"
|
||||
import { STATES } from "@utils/global"
|
||||
import { PrefKey } from "@/enums/pref-keys"
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage"
|
||||
import { StreamStat, StreamStatsCollector, type StreamStatGrade } from "@/utils/stream-stats-collector"
|
||||
import { StreamStatsCollector, type StreamStatGrade } from "@/utils/stream-stats-collector"
|
||||
import { BxLogger } from "@/utils/bx-logger"
|
||||
import { StreamStat } from "@/enums/pref-values"
|
||||
|
||||
|
||||
export class StreamStats {
|
||||
@@ -87,7 +88,7 @@ export class StreamStats {
|
||||
this.$container.classList.remove('bx-gone');
|
||||
this.$container.dataset.display = glancing ? 'glancing' : 'fixed';
|
||||
|
||||
this.intervalId = window.setInterval(this.update.bind(this), this.REFRESH_INTERVAL);
|
||||
this.intervalId = window.setInterval(this.update, this.REFRESH_INTERVAL);
|
||||
}
|
||||
|
||||
async stop(glancing=false) {
|
||||
@@ -157,7 +158,7 @@ export class StreamStats {
|
||||
this.quickGlanceObserver = null;
|
||||
}
|
||||
|
||||
private async update(forceUpdate=false) {
|
||||
private update = async (forceUpdate=false) => {
|
||||
if ((!forceUpdate && this.isHidden()) || !STATES.currentStream.peerConnection) {
|
||||
this.destroy();
|
||||
return;
|
||||
@@ -191,7 +192,7 @@ export class StreamStats {
|
||||
}
|
||||
|
||||
refreshStyles() {
|
||||
const PREF_ITEMS = getPref(PrefKey.STATS_ITEMS);
|
||||
const PREF_ITEMS = getPref<StreamStat[]>(PrefKey.STATS_ITEMS);
|
||||
|
||||
const $container = this.$container;
|
||||
$container.dataset.stats = '[' + PREF_ITEMS.join('][') + ']';
|
||||
@@ -202,7 +203,7 @@ export class StreamStats {
|
||||
}
|
||||
|
||||
hideSettingsUi() {
|
||||
if (this.isGlancing() && !getPref(PrefKey.STATS_QUICK_GLANCE)) {
|
||||
if (this.isGlancing() && !getPref(PrefKey.STATS_QUICK_GLANCE_ENABLED)) {
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
@@ -230,7 +231,7 @@ export class StreamStats {
|
||||
|
||||
static setupEvents() {
|
||||
window.addEventListener(BxEvent.STREAM_PLAYING, e => {
|
||||
const PREF_STATS_QUICK_GLANCE = getPref(PrefKey.STATS_QUICK_GLANCE);
|
||||
const PREF_STATS_QUICK_GLANCE = getPref(PrefKey.STATS_QUICK_GLANCE_ENABLED);
|
||||
const PREF_STATS_SHOW_WHEN_PLAYING = getPref(PrefKey.STATS_SHOW_WHEN_PLAYING);
|
||||
|
||||
const streamStats = StreamStats.getInstance();
|
||||
|
4
src/modules/stream/stream-ui.ts
Normal file → Executable file
4
src/modules/stream/stream-ui.ts
Normal file → Executable file
@@ -5,7 +5,7 @@ import { BxEvent } from "@utils/bx-event.ts";
|
||||
import { t } from "@utils/translation.ts";
|
||||
import { StreamBadges } from "./stream-badges.ts";
|
||||
import { StreamStats } from "./stream-stats.ts";
|
||||
import { SettingsNavigationDialog } from "../ui/dialog/settings-dialog.ts";
|
||||
import { SettingsDialog } from "../ui/dialog/settings-dialog.ts";
|
||||
|
||||
|
||||
export class StreamUiHandler {
|
||||
@@ -161,7 +161,7 @@ export class StreamUiHandler {
|
||||
e.preventDefault();
|
||||
|
||||
// Show Stream Settings dialog
|
||||
SettingsNavigationDialog.getInstance().show();
|
||||
SettingsDialog.getInstance().show();
|
||||
});
|
||||
|
||||
StreamUiHandler.$btnStreamSettings = $btnStreamSettings;
|
||||
|
32
src/modules/touch-controller.ts
Normal file → Executable file
32
src/modules/touch-controller.ts
Normal file → Executable file
@@ -6,6 +6,8 @@ import { t } from "@utils/translation";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { TouchControllerStyleCustom, TouchControllerStyleStandard } from "@/enums/pref-values";
|
||||
import { GhPagesUtils } from "@/utils/gh-pages";
|
||||
|
||||
const LOG_TAG = 'TouchController';
|
||||
|
||||
@@ -145,12 +147,9 @@ export class TouchController {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = 'https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts';
|
||||
const url = `${baseUrl}/${xboxTitleId}.json`;
|
||||
|
||||
// Get layout info
|
||||
try {
|
||||
const resp = await NATIVE_FETCH(url);
|
||||
const resp = await NATIVE_FETCH(GhPagesUtils.getUrl(`touch-layouts/${xboxTitleId}.json`));
|
||||
const json = await resp.json();
|
||||
|
||||
const layouts = {};
|
||||
@@ -161,7 +160,7 @@ export class TouchController {
|
||||
baseLayouts = TouchController.#baseCustomLayouts[layoutName];
|
||||
} else {
|
||||
try {
|
||||
const layoutUrl = `${baseUrl}/layouts/${layoutName}.json`;
|
||||
const layoutUrl = GhPagesUtils.getUrl(`touch-layouts/layouts/${layoutName}.json`);
|
||||
const resp = await NATIVE_FETCH(layoutUrl);
|
||||
const json = await resp.json();
|
||||
|
||||
@@ -188,12 +187,11 @@ export class TouchController {
|
||||
// TODO: fix this
|
||||
if (!window.BX_EXPOSED.touchLayoutManager) {
|
||||
const listener = (e: Event) => {
|
||||
window.removeEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener);
|
||||
if (TouchController.#enabled) {
|
||||
TouchController.applyCustomLayout(layoutId, 0);
|
||||
}
|
||||
};
|
||||
window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener);
|
||||
window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener, { once: true });
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -261,15 +259,7 @@ export class TouchController {
|
||||
}
|
||||
|
||||
static updateCustomList() {
|
||||
const key = 'better_xcloud_custom_touch_layouts';
|
||||
TouchController.#customList = JSON.parse(window.localStorage.getItem(key) || '[]');
|
||||
|
||||
NATIVE_FETCH('https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/ids.json')
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
TouchController.#customList = json;
|
||||
window.localStorage.setItem(key, JSON.stringify(json));
|
||||
});
|
||||
TouchController.#customList = GhPagesUtils.getTouchControlCustomList();
|
||||
}
|
||||
|
||||
static getCustomList(): string[] {
|
||||
@@ -298,8 +288,8 @@ export class TouchController {
|
||||
|
||||
TouchController.#$style = $style;
|
||||
|
||||
const PREF_STYLE_STANDARD = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD);
|
||||
const PREF_STYLE_CUSTOM = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM);
|
||||
const PREF_STYLE_STANDARD = getPref<TouchControllerStyleStandard>(PrefKey.TOUCH_CONTROLLER_STYLE_STANDARD);
|
||||
const PREF_STYLE_CUSTOM = getPref<TouchControllerStyleCustom>(PrefKey.TOUCH_CONTROLLER_STYLE_CUSTOM);
|
||||
|
||||
window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => {
|
||||
const dataChannel = (e as any).dataChannel;
|
||||
@@ -310,12 +300,12 @@ export class TouchController {
|
||||
// Apply touch controller's style
|
||||
let filter = '';
|
||||
if (TouchController.#enabled) {
|
||||
if (PREF_STYLE_STANDARD === 'white') {
|
||||
if (PREF_STYLE_STANDARD === TouchControllerStyleStandard.WHITE) {
|
||||
filter = 'grayscale(1) brightness(2)';
|
||||
} else if (PREF_STYLE_STANDARD === 'muted') {
|
||||
} else if (PREF_STYLE_STANDARD === TouchControllerStyleStandard.MUTED) {
|
||||
filter = 'sepia(0.5)';
|
||||
}
|
||||
} else if (PREF_STYLE_CUSTOM === 'muted') {
|
||||
} else if (PREF_STYLE_CUSTOM === TouchControllerStyleCustom.MUTED) {
|
||||
filter = 'sepia(0.5)';
|
||||
}
|
||||
|
||||
|
85
src/modules/ui/dialog/navigation-dialog.ts
Normal file → Executable file
85
src/modules/ui/dialog/navigation-dialog.ts
Normal file → Executable file
@@ -1,9 +1,8 @@
|
||||
import { GamepadKey } from "@/enums/mkb";
|
||||
import { GamepadKey } from "@/enums/gamepad";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { BxLogger } from "@/utils/bx-logger";
|
||||
import { STATES } from "@/utils/global";
|
||||
import { CE, isElementVisible } from "@/utils/html";
|
||||
import { setNearby } from "@/utils/navigation-utils";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
@@ -40,13 +39,22 @@ export abstract class NavigationDialog {
|
||||
|
||||
abstract $container: HTMLElement;
|
||||
dialogManager: NavigationDialogManager;
|
||||
onMountedCallbacks: Array<() => void> = [];
|
||||
|
||||
constructor() {
|
||||
this.dialogManager = NavigationDialogManager.getInstance();
|
||||
}
|
||||
|
||||
show() {
|
||||
NavigationDialogManager.getInstance().show(this);
|
||||
isCancellable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
isOverlayVisible(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
show(configs={}, clearStack=false) {
|
||||
NavigationDialogManager.getInstance().show(this, configs, clearStack);
|
||||
|
||||
const $currentFocus = this.getFocusedElement();
|
||||
// If not focusing on any element
|
||||
@@ -73,8 +81,12 @@ export abstract class NavigationDialog {
|
||||
return null;
|
||||
}
|
||||
|
||||
onBeforeMount(): void {}
|
||||
onMounted(): void {}
|
||||
onBeforeMount(configs={}): void {}
|
||||
onMounted(configs={}): void {
|
||||
for (const callback of this.onMountedCallbacks) {
|
||||
callback.call(this);
|
||||
}
|
||||
}
|
||||
onBeforeUnmount(): void {}
|
||||
onUnmounted(): void {}
|
||||
|
||||
@@ -119,12 +131,12 @@ export class NavigationDialogManager {
|
||||
};
|
||||
|
||||
private static readonly SIBLING_PROPERTY_MAP = {
|
||||
'horizontal': {
|
||||
horizontal: {
|
||||
[NavigationDirection.LEFT]: 'previousElementSibling',
|
||||
[NavigationDirection.RIGHT]: 'nextElementSibling',
|
||||
},
|
||||
|
||||
'vertical': {
|
||||
vertical: {
|
||||
[NavigationDirection.UP]: 'previousElementSibling',
|
||||
[NavigationDirection.DOWN]: 'nextElementSibling',
|
||||
},
|
||||
@@ -137,6 +149,7 @@ export class NavigationDialogManager {
|
||||
private $overlay: HTMLElement;
|
||||
private $container: HTMLElement;
|
||||
private dialog: NavigationDialog | null = null;
|
||||
private dialogsStack: Array<NavigationDialog> = [];
|
||||
|
||||
private constructor() {
|
||||
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||
@@ -145,7 +158,8 @@ export class NavigationDialogManager {
|
||||
this.$overlay.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.hide();
|
||||
|
||||
this.dialog?.isCancellable() && this.hide();
|
||||
});
|
||||
|
||||
document.documentElement.appendChild(this.$overlay);
|
||||
@@ -214,9 +228,16 @@ export class NavigationDialogManager {
|
||||
};
|
||||
}
|
||||
|
||||
private updateActiveInput(input: 'keyboard' | 'gamepad' | 'mouse') {
|
||||
// Set <html>'s activeInput
|
||||
document.documentElement.dataset.activeInput = input;
|
||||
}
|
||||
|
||||
handleEvent(event: Event) {
|
||||
switch (event.type) {
|
||||
case 'keydown':
|
||||
this.updateActiveInput('keyboard');
|
||||
|
||||
const $target = event.target as HTMLElement;
|
||||
const keyboardEvent = event as KeyboardEvent;
|
||||
const keyCode = keyboardEvent.code || keyboardEvent.key;
|
||||
@@ -259,7 +280,7 @@ export class NavigationDialogManager {
|
||||
return this.$container && !this.$container.classList.contains('bx-gone');
|
||||
}
|
||||
|
||||
private pollGamepad() {
|
||||
private pollGamepad = () => {
|
||||
const gamepads = window.navigator.getGamepads();
|
||||
|
||||
for (const gamepad of gamepads) {
|
||||
@@ -365,6 +386,12 @@ export class NavigationDialogManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateActiveInput('gamepad');
|
||||
|
||||
if (this.handleGamepad(gamepad, releasedButton)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (releasedButton === GamepadKey.A) {
|
||||
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||
return;
|
||||
@@ -372,10 +399,6 @@ export class NavigationDialogManager {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.handleGamepad(gamepad, releasedButton)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,7 +436,7 @@ export class NavigationDialogManager {
|
||||
this.gamepadHoldingIntervalId = null;
|
||||
}
|
||||
|
||||
show(dialog: NavigationDialog) {
|
||||
show(dialog: NavigationDialog, configs={}, clearStack=false) {
|
||||
this.clearGamepadHoldingInterval();
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN);
|
||||
@@ -424,20 +447,21 @@ export class NavigationDialogManager {
|
||||
// Lock scroll bar
|
||||
document.body.classList.add('bx-no-scroll');
|
||||
|
||||
// Show overlay
|
||||
this.$overlay.classList.remove('bx-gone');
|
||||
if (STATES.isPlaying) {
|
||||
this.$overlay.classList.add('bx-invisible');
|
||||
}
|
||||
|
||||
// Unmount current dialog
|
||||
this.unmountCurrentDialog();
|
||||
|
||||
// Add to dialogs stack
|
||||
this.dialogsStack.push(dialog);
|
||||
|
||||
// Setup new dialog
|
||||
this.dialog = dialog;
|
||||
dialog.onBeforeMount();
|
||||
dialog.onBeforeMount(configs);
|
||||
this.$container.appendChild(dialog.getContent());
|
||||
dialog.onMounted();
|
||||
dialog.onMounted(configs);
|
||||
|
||||
// Show overlay
|
||||
this.$overlay.classList.remove('bx-gone');
|
||||
this.$overlay.classList.toggle('bx-invisible', !dialog.isOverlayVisible());
|
||||
|
||||
// Show content
|
||||
this.$container.classList.remove('bx-gone');
|
||||
@@ -468,11 +492,24 @@ export class NavigationDialogManager {
|
||||
// Stop gamepad polling
|
||||
this.stopGamepadPolling();
|
||||
|
||||
// Remove current dialog and everything after it from dialogs stack
|
||||
if (this.dialog) {
|
||||
const dialogIndex = this.dialogsStack.indexOf(this.dialog);
|
||||
if (dialogIndex > -1) {
|
||||
this.dialogsStack = this.dialogsStack.slice(0, dialogIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Unmount dialog
|
||||
this.unmountCurrentDialog();
|
||||
|
||||
// Enable xCloud's navigation polling
|
||||
(window as any).BX_EXPOSED.disableGamepadPolling = false;
|
||||
|
||||
// Show the last dialog in dialogs stack
|
||||
if (this.dialogsStack.length) {
|
||||
this.dialogsStack[this.dialogsStack.length - 1].show();
|
||||
}
|
||||
}
|
||||
|
||||
focus($elm: NavigationElement | null): boolean {
|
||||
@@ -624,7 +661,7 @@ export class NavigationDialogManager {
|
||||
private startGamepadPolling() {
|
||||
this.stopGamepadPolling();
|
||||
|
||||
this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad.bind(this), NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);
|
||||
this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);
|
||||
}
|
||||
|
||||
private stopGamepadPolling() {
|
||||
|
205
src/modules/ui/dialog/profile-manger/base-profile-manager-dialog.ts
Executable file
205
src/modules/ui/dialog/profile-manger/base-profile-manager-dialog.ts
Executable file
@@ -0,0 +1,205 @@
|
||||
import { ButtonStyle, CE, createButton, renderPresetsList } from "@/utils/html";
|
||||
import { NavigationDialog } from "../navigation-dialog";
|
||||
import { BxIcon } from "@/utils/bx-icon";
|
||||
import { t } from "@/utils/translation";
|
||||
import type { AllPresets, PresetRecord } from "@/types/presets";
|
||||
import type { BasePresetsTable } from "@/utils/local-db/base-presets-table";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
|
||||
export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends NavigationDialog {
|
||||
$container!: HTMLElement;
|
||||
|
||||
private title: string;
|
||||
protected presetsDb: BasePresetsTable<T>;
|
||||
protected allPresets!: AllPresets<T>;
|
||||
protected currentPresetId: number = 0;
|
||||
|
||||
private $presets!: HTMLSelectElement;
|
||||
private $header!: HTMLElement;
|
||||
protected $content!: HTMLElement;
|
||||
|
||||
private $btnRename!: HTMLButtonElement;
|
||||
private $btnDelete!: HTMLButtonElement;
|
||||
|
||||
protected abstract readonly BLANK_PRESET_DATA: T['data'];
|
||||
|
||||
constructor(title: string, presetsDb: BasePresetsTable<T>) {
|
||||
super();
|
||||
|
||||
this.title = title;
|
||||
this.presetsDb = presetsDb;
|
||||
}
|
||||
|
||||
protected abstract switchPreset(id: number): void;
|
||||
|
||||
protected updateButtonStates() {
|
||||
const isDefaultPreset = this.currentPresetId <= 0;
|
||||
this.$btnRename.disabled = isDefaultPreset;
|
||||
this.$btnDelete.disabled = isDefaultPreset;
|
||||
}
|
||||
|
||||
private async renderPresetsList() {
|
||||
this.allPresets = await this.presetsDb.getPresets();
|
||||
if (!this.currentPresetId) {
|
||||
this.currentPresetId = this.allPresets.default[0];
|
||||
}
|
||||
|
||||
renderPresetsList<T>(this.$presets, this.allPresets, this.currentPresetId);
|
||||
}
|
||||
|
||||
private promptNewName(action: string,value='') {
|
||||
let newName: string | null = '';
|
||||
while (!newName) {
|
||||
newName = prompt(`[${action}] ${t('prompt-preset-name')}`, value);
|
||||
if (newName === null) {
|
||||
return false;
|
||||
}
|
||||
newName = newName.trim();
|
||||
}
|
||||
|
||||
return newName ? newName : false;
|
||||
};
|
||||
|
||||
private async renderDialog() {
|
||||
this.$presets = CE<HTMLSelectElement>('select', { tabindex: -1 });
|
||||
|
||||
const $select = BxSelectElement.create(this.$presets);
|
||||
$select.classList.add('bx-full-width');
|
||||
$select.addEventListener('input', e => {
|
||||
this.switchPreset(parseInt(($select as HTMLSelectElement).value));
|
||||
});
|
||||
|
||||
const $header = CE('div', {
|
||||
class: 'bx-dialog-preset-tools',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
focus: $select,
|
||||
},
|
||||
},
|
||||
$select,
|
||||
|
||||
// Rename button
|
||||
this.$btnRename = createButton({
|
||||
title: t('rename'),
|
||||
icon: BxIcon.CURSOR_TEXT,
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: async () => {
|
||||
const preset = this.allPresets.data[this.currentPresetId];
|
||||
|
||||
const newName = this.promptNewName(t('rename'), preset.name);
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update preset with new name
|
||||
preset.name = newName;
|
||||
|
||||
await this.presetsDb.updatePreset(preset);
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
|
||||
// Delete button
|
||||
this.$btnDelete = createButton({
|
||||
icon: BxIcon.TRASH,
|
||||
title: t('delete'),
|
||||
style: ButtonStyle.DANGER | ButtonStyle.FOCUSABLE,
|
||||
onClick: async (e) => {
|
||||
if (!confirm(t('confirm-delete-preset'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.presetsDb.deletePreset(this.currentPresetId);
|
||||
delete this.allPresets.data[this.currentPresetId];
|
||||
this.currentPresetId = parseInt(Object.keys(this.allPresets.data)[0]);
|
||||
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
|
||||
// New button
|
||||
createButton({
|
||||
icon: BxIcon.NEW,
|
||||
title: t('new'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: async (e) => {
|
||||
const newName = this.promptNewName(t('new'));
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new preset selected name
|
||||
const newId = await this.presetsDb.newPreset(newName, this.BLANK_PRESET_DATA);
|
||||
this.currentPresetId = newId;
|
||||
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
|
||||
// Copy button
|
||||
createButton({
|
||||
icon: BxIcon.COPY,
|
||||
title: t('copy'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: async (e) => {
|
||||
const preset = this.allPresets.data[this.currentPresetId];
|
||||
|
||||
const newName = this.promptNewName(t('copy'), `${preset.name} (2)`);
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new preset with selected name
|
||||
const newId = await this.presetsDb.newPreset(newName, preset.data);
|
||||
this.currentPresetId = newId;
|
||||
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
);
|
||||
this.$header = $header;
|
||||
|
||||
this.$container = CE('div', { class: 'bx-centered-dialog' },
|
||||
CE('div', { class: 'bx-dialog-title' },
|
||||
CE('p', {}, this.title),
|
||||
createButton({
|
||||
icon: BxIcon.CLOSE,
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR | ButtonStyle.GHOST,
|
||||
onClick: e => this.hide(),
|
||||
}),
|
||||
),
|
||||
$header,
|
||||
CE('div', { class: 'bx-dialog-content bx-hide-scroll-bar' }, this.$content),
|
||||
);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.renderPresetsList();
|
||||
this.switchPreset(this.currentPresetId);
|
||||
}
|
||||
|
||||
async onBeforeMount(configs:{ id?: number }={}) {
|
||||
if (configs?.id) {
|
||||
this.currentPresetId = configs.id;
|
||||
}
|
||||
|
||||
// Select first preset
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
getDialog(): NavigationDialog {
|
||||
return this;
|
||||
}
|
||||
|
||||
getContent(): HTMLElement {
|
||||
if (!this.$container) {
|
||||
this.renderDialog();
|
||||
}
|
||||
|
||||
return this.$container;
|
||||
}
|
||||
|
||||
focusIfNeeded(): void {
|
||||
this.dialogManager.focus(this.$header);
|
||||
}
|
||||
}
|
194
src/modules/ui/dialog/profile-manger/controller-shortcuts-manager-dialog.ts
Executable file
194
src/modules/ui/dialog/profile-manger/controller-shortcuts-manager-dialog.ts
Executable file
@@ -0,0 +1,194 @@
|
||||
import { t } from "@/utils/translation";
|
||||
import { BaseProfileManagerDialog } from "./base-profile-manager-dialog";
|
||||
import { CE } from "@/utils/html";
|
||||
import { GamepadKey, GamepadKeyName } from "@/enums/gamepad";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { PrompFont } from "@/enums/prompt-font";
|
||||
import { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { deepClone } from "@/utils/global";
|
||||
import { setNearby } from "@/utils/navigation-utils";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import type { ControllerShortcutPresetData, ControllerShortcutPresetRecord } from "@/types/presets";
|
||||
import { ControllerShortcutsTable } from "@/utils/local-db/controller-shortcuts-table";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { SHORTCUT_ACTIONS } from "@/modules/shortcuts/shortcut-actions";
|
||||
|
||||
export class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog<ControllerShortcutPresetRecord> {
|
||||
private static instance: ControllerShortcutsManagerDialog;
|
||||
public static getInstance = () => ControllerShortcutsManagerDialog.instance ?? (ControllerShortcutsManagerDialog.instance = new ControllerShortcutsManagerDialog(t('controller-shortcuts')));
|
||||
// private readonly LOG_TAG = 'ControllerShortcutsManagerDialog';
|
||||
|
||||
protected $content: HTMLElement;
|
||||
private selectActions: Partial<Record<GamepadKey, [HTMLSelectElement, HTMLSelectElement | null]>> = {};
|
||||
|
||||
protected readonly BLANK_PRESET_DATA = {
|
||||
mapping: {},
|
||||
};
|
||||
|
||||
private readonly BUTTONS_ORDER = [
|
||||
GamepadKey.Y, GamepadKey.A, GamepadKey.X, GamepadKey.B,
|
||||
GamepadKey.UP, GamepadKey.DOWN, GamepadKey.LEFT, GamepadKey.RIGHT,
|
||||
GamepadKey.SELECT, GamepadKey.START,
|
||||
GamepadKey.LB, GamepadKey.RB,
|
||||
GamepadKey.LT, GamepadKey.RT,
|
||||
GamepadKey.L3, GamepadKey.R3,
|
||||
];
|
||||
|
||||
constructor(title: string) {
|
||||
super(title, ControllerShortcutsTable.getInstance());
|
||||
|
||||
const PREF_CONTROLLER_FRIENDLY_UI = getPref(PrefKey.UI_CONTROLLER_FRIENDLY);
|
||||
|
||||
// Read actions from localStorage
|
||||
// ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage();
|
||||
|
||||
const $baseSelect = CE<HTMLSelectElement>('select', { autocomplete: 'off' }, CE('option', { value: '' }, '---'));
|
||||
for (const groupLabel in SHORTCUT_ACTIONS) {
|
||||
const items = SHORTCUT_ACTIONS[groupLabel];
|
||||
if (!items) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $optGroup = CE<HTMLOptGroupElement>('optgroup', { label: groupLabel });
|
||||
for (const action in items) {
|
||||
const crumbs = items[action as keyof typeof items];
|
||||
if (!crumbs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = crumbs.join(' ❯ ');
|
||||
const $option = CE<HTMLOptionElement>('option', { value: action }, label);
|
||||
$optGroup.appendChild($option);
|
||||
}
|
||||
|
||||
$baseSelect.appendChild($optGroup);
|
||||
}
|
||||
|
||||
const $content = CE('div', {
|
||||
class: 'bx-controller-shortcuts-manager-container',
|
||||
});
|
||||
|
||||
const onActionChanged = (e: Event) => {
|
||||
const $target = e.target as HTMLSelectElement;
|
||||
|
||||
// const profile = $selectProfile.value;
|
||||
// const button: unknown = $target.dataset.button;
|
||||
const action = $target.value as ShortcutAction;
|
||||
|
||||
if (!PREF_CONTROLLER_FRIENDLY_UI) {
|
||||
const $fakeSelect = $target.previousElementSibling! as HTMLSelectElement;
|
||||
let fakeText = '---';
|
||||
if (action) {
|
||||
const $selectedOption = $target.options[$target.selectedIndex];
|
||||
const $optGroup = $selectedOption.parentElement as HTMLOptGroupElement;
|
||||
fakeText = $optGroup.label + ' ❯ ' + $selectedOption.text;
|
||||
}
|
||||
($fakeSelect.firstElementChild as HTMLOptionElement).text = fakeText;
|
||||
}
|
||||
|
||||
// Update preset
|
||||
if (!(e as any).ignoreOnChange) {
|
||||
this.updatePreset();
|
||||
}
|
||||
};
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.appendChild(CE('p', {class: 'bx-shortcut-note'},
|
||||
CE('span', {class: 'bx-prompt'}, PrompFont.HOME),
|
||||
': ' + t('controller-shortcuts-xbox-note'),
|
||||
));
|
||||
|
||||
for (const button of this.BUTTONS_ORDER) {
|
||||
const prompt = GamepadKeyName[button][1];
|
||||
|
||||
const $row = CE('div', {
|
||||
class: 'bx-shortcut-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
});
|
||||
const $label = CE('label', {class: 'bx-prompt'}, `${PrompFont.HOME}${prompt}`);
|
||||
const $div = CE('div', {class: 'bx-shortcut-actions'});
|
||||
|
||||
let $fakeSelect: HTMLSelectElement | null = null;
|
||||
if (!PREF_CONTROLLER_FRIENDLY_UI) {
|
||||
$fakeSelect = CE<HTMLSelectElement>('select', { autocomplete: 'off' },
|
||||
CE('option', {}, '---'),
|
||||
);
|
||||
|
||||
$div.appendChild($fakeSelect);
|
||||
}
|
||||
|
||||
const $select = BxSelectElement.create($baseSelect.cloneNode(true) as HTMLSelectElement);
|
||||
$select.dataset.button = button.toString();
|
||||
$select.classList.add('bx-full-width');
|
||||
$select.addEventListener('input', onActionChanged);
|
||||
|
||||
this.selectActions[button] = [$select, $fakeSelect];
|
||||
|
||||
$div.appendChild($select);
|
||||
setNearby($row, {
|
||||
focus: $select,
|
||||
});
|
||||
|
||||
$row.append($label, $div);
|
||||
fragment.appendChild($row);
|
||||
}
|
||||
|
||||
$content.appendChild(fragment);
|
||||
|
||||
this.$content = $content;
|
||||
}
|
||||
|
||||
protected switchPreset(id: number): void {
|
||||
const preset = this.allPresets.data[id];
|
||||
if (!preset) {
|
||||
this.currentPresetId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentPresetId = id;
|
||||
const isDefaultPreset = id <= 0;
|
||||
const actions = preset.data;
|
||||
|
||||
// Reset selects' values
|
||||
let button: unknown;
|
||||
for (button in this.selectActions) {
|
||||
const [$select, $fakeSelect] = this.selectActions[button as GamepadKey]!;
|
||||
$select.value = actions.mapping[button as GamepadKey] || '';
|
||||
$select.disabled = isDefaultPreset;
|
||||
$fakeSelect && ($fakeSelect.disabled = isDefaultPreset);
|
||||
|
||||
BxEvent.dispatch($select, 'input', {
|
||||
ignoreOnChange: true,
|
||||
manualTrigger: true,
|
||||
});
|
||||
}
|
||||
|
||||
super.updateButtonStates();
|
||||
}
|
||||
|
||||
private updatePreset() {
|
||||
const newData: ControllerShortcutPresetData = deepClone(this.BLANK_PRESET_DATA);
|
||||
|
||||
let button: unknown;
|
||||
for (button in this.selectActions) {
|
||||
const [$select, _] = this.selectActions[button as GamepadKey]!;
|
||||
|
||||
const action = $select.value;
|
||||
if (!action) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newData.mapping[button as GamepadKey] = action as ShortcutAction;
|
||||
}
|
||||
|
||||
const preset = this.allPresets.data[this.currentPresetId];
|
||||
preset.data = newData;
|
||||
this.presetsDb.updatePreset(preset);
|
||||
|
||||
StreamSettings.refreshControllerSettings();
|
||||
}
|
||||
}
|
151
src/modules/ui/dialog/profile-manger/keyboard-shortcuts-manager-dialog.ts
Executable file
151
src/modules/ui/dialog/profile-manger/keyboard-shortcuts-manager-dialog.ts
Executable file
@@ -0,0 +1,151 @@
|
||||
import { t } from "@/utils/translation";
|
||||
import { BaseProfileManagerDialog } from "./base-profile-manager-dialog";
|
||||
import type { KeyboardShortcutPresetData, KeyboardShortcutPresetRecord } from "@/types/presets";
|
||||
import { CE, createSettingRow } from "@/utils/html";
|
||||
import { KeyboardShortcutDefaultId, KeyboardShortcutsTable } from "@/utils/local-db/keyboard-shortcuts-table";
|
||||
import { SHORTCUT_ACTIONS } from "@/modules/shortcuts/shortcut-actions";
|
||||
import { BxKeyBindingButton, BxKeyBindingButtonFlag } from "@/web-components/bx-key-binding-button";
|
||||
import type { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { deepClone } from "@/utils/global";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
|
||||
type KeyboardShortcutButtonDataset = {
|
||||
action: ShortcutAction,
|
||||
}
|
||||
|
||||
export class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog<KeyboardShortcutPresetRecord> {
|
||||
private static instance: KeyboardShortcutsManagerDialog;
|
||||
public static getInstance = () => KeyboardShortcutsManagerDialog.instance ?? (KeyboardShortcutsManagerDialog.instance = new KeyboardShortcutsManagerDialog(t('keyboard-shortcuts')));
|
||||
// private readonly LOG_TAG = 'KeyboardShortcutsManagerDialog';
|
||||
|
||||
protected $content: HTMLElement;
|
||||
private readonly allKeyElements: BxKeyBindingButton[] = [];
|
||||
|
||||
protected readonly BLANK_PRESET_DATA: KeyboardShortcutPresetData = {
|
||||
mapping: {},
|
||||
};
|
||||
|
||||
constructor(title: string) {
|
||||
super(title, KeyboardShortcutsTable.getInstance());
|
||||
|
||||
const $rows = CE('div', { class: 'bx-keyboard-shortcuts-manager-container' });
|
||||
|
||||
for (const groupLabel in SHORTCUT_ACTIONS) {
|
||||
const items = SHORTCUT_ACTIONS[groupLabel];
|
||||
if (!items) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $fieldSet = CE<HTMLFieldSetElement>('fieldset', {}, CE('legend', {}, groupLabel));
|
||||
for (const action in items) {
|
||||
const crumbs = items[action as keyof typeof items];
|
||||
if (!crumbs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = crumbs.join(' ❯ ');
|
||||
const $btn = BxKeyBindingButton.create({
|
||||
title: label,
|
||||
isPrompt: false,
|
||||
onChanged: this.onKeyChanged,
|
||||
|
||||
allowedFlags: [BxKeyBindingButtonFlag.KEYBOARD_PRESS, BxKeyBindingButtonFlag.KEYBOARD_MODIFIER],
|
||||
});
|
||||
$btn.classList.add('bx-full-width');
|
||||
$btn.dataset.action = action;
|
||||
this.allKeyElements.push($btn);
|
||||
|
||||
const $row = createSettingRow(label, CE('div', { class: 'bx-binding-button-wrapper' }, $btn));
|
||||
$fieldSet.appendChild($row);
|
||||
}
|
||||
|
||||
// Don't append empty <fieldset>
|
||||
if ($fieldSet.childElementCount > 1) {
|
||||
$rows.appendChild($fieldSet);
|
||||
}
|
||||
}
|
||||
|
||||
this.$content = CE('div', {}, $rows);
|
||||
}
|
||||
|
||||
private onKeyChanged = (e: Event) => {
|
||||
const $current = e.target as BxKeyBindingButton;
|
||||
const keyInfo = $current.keyInfo;
|
||||
|
||||
// Unbind duplicated keys
|
||||
if (keyInfo) {
|
||||
for (const $elm of this.allKeyElements) {
|
||||
if ($elm === $current) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($elm.keyInfo?.code === keyInfo.code && $elm.keyInfo?.modifiers === keyInfo.modifiers) {
|
||||
// Unbind manually
|
||||
$elm.unbindKey(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save preset
|
||||
this.savePreset();
|
||||
}
|
||||
|
||||
private parseDataset($btn: BxKeyBindingButton): KeyboardShortcutButtonDataset {
|
||||
const dataset = $btn.dataset;
|
||||
return {
|
||||
action: dataset.action as ShortcutAction,
|
||||
};
|
||||
}
|
||||
|
||||
protected switchPreset(id: number): void {
|
||||
const preset = this.allPresets.data[id];
|
||||
if (!preset) {
|
||||
this.currentPresetId = KeyboardShortcutDefaultId.OFF;
|
||||
return;
|
||||
}
|
||||
|
||||
const presetData = preset.data;
|
||||
this.currentPresetId = id;
|
||||
const isDefaultPreset = id <= 0;
|
||||
this.updateButtonStates();
|
||||
|
||||
// Update buttons
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const { action } = this.parseDataset($elm);
|
||||
|
||||
const keyInfo = presetData.mapping[action];
|
||||
if (keyInfo) {
|
||||
$elm.bindKey(keyInfo, true)
|
||||
} else {
|
||||
$elm.unbindKey(true);
|
||||
}
|
||||
|
||||
$elm.disabled = isDefaultPreset;
|
||||
}
|
||||
}
|
||||
|
||||
private savePreset() {
|
||||
const presetData = deepClone(this.BLANK_PRESET_DATA) as KeyboardShortcutPresetData;
|
||||
|
||||
// Get mapping
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const { action } = this.parseDataset($elm);
|
||||
|
||||
const mapping = presetData.mapping;
|
||||
if ($elm.keyInfo) {
|
||||
mapping[action] = $elm.keyInfo;
|
||||
}
|
||||
}
|
||||
|
||||
const oldPreset = this.allPresets.data[this.currentPresetId];
|
||||
const newPreset = {
|
||||
id: this.currentPresetId,
|
||||
name: oldPreset.name,
|
||||
data: presetData,
|
||||
};
|
||||
this.presetsDb.updatePreset(newPreset);
|
||||
|
||||
this.allPresets.data[this.currentPresetId] = newPreset;
|
||||
StreamSettings.refreshKeyboardShortcuts();
|
||||
}
|
||||
}
|
254
src/modules/ui/dialog/profile-manger/mkb-mapping-manager-dialog.ts
Executable file
254
src/modules/ui/dialog/profile-manger/mkb-mapping-manager-dialog.ts
Executable file
@@ -0,0 +1,254 @@
|
||||
import type { MkbPresetData, MkbPresetRecord } from "@/types/presets";
|
||||
import { BaseProfileManagerDialog } from "./base-profile-manager-dialog";
|
||||
import { t } from "@/utils/translation";
|
||||
import { MkbMappingPresetsTable } from "@/utils/local-db/mkb-mapping-presets-table";
|
||||
import { GamepadKey, GamepadKeyName } from "@/enums/gamepad";
|
||||
import { CE, createSettingRow } from "@/utils/html";
|
||||
import { MouseMapTo, MkbPresetKey, type KeyCode } from "@/enums/mkb";
|
||||
import { BxKeyBindingButton, BxKeyBindingButtonFlag } from "@/web-components/bx-key-binding-button";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { BxNumberStepper } from "@/web-components/bx-number-stepper";
|
||||
import { deepClone } from "@/utils/global";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
|
||||
type MkbButtonDataset = {
|
||||
keySlot: number,
|
||||
buttonIndex: GamepadKey,
|
||||
}
|
||||
|
||||
export class MkbMappingManagerDialog extends BaseProfileManagerDialog<MkbPresetRecord> {
|
||||
private static instance: MkbMappingManagerDialog;
|
||||
public static getInstance = () => MkbMappingManagerDialog.instance ?? (MkbMappingManagerDialog.instance = new MkbMappingManagerDialog(t('virtual-controller')));
|
||||
|
||||
declare protected $content: HTMLElement;
|
||||
|
||||
private readonly KEYS_PER_BUTTON = 2;
|
||||
private readonly BUTTONS_ORDER = [
|
||||
GamepadKey.HOME,
|
||||
GamepadKey.UP, GamepadKey.DOWN, GamepadKey.LEFT, GamepadKey.RIGHT,
|
||||
GamepadKey.A, GamepadKey.B, GamepadKey.X, GamepadKey.Y,
|
||||
GamepadKey.LB, GamepadKey.RB, GamepadKey.LT, GamepadKey.RT,
|
||||
GamepadKey.SELECT, GamepadKey.START,
|
||||
GamepadKey.L3, GamepadKey.LS_UP, GamepadKey.LS_DOWN, GamepadKey.LS_LEFT, GamepadKey.LS_RIGHT,
|
||||
GamepadKey.R3, GamepadKey.RS_UP, GamepadKey.RS_DOWN, GamepadKey.RS_LEFT, GamepadKey.RS_RIGHT,
|
||||
];
|
||||
|
||||
protected readonly BLANK_PRESET_DATA: MkbPresetData = {
|
||||
mapping: {},
|
||||
mouse: {
|
||||
[MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo.RS,
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_X]: 100,
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 100,
|
||||
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20,
|
||||
},
|
||||
};
|
||||
|
||||
private readonly allKeyElements: BxKeyBindingButton[] = [];
|
||||
private $mouseMapTo!: BxSelectElement;
|
||||
private $mouseSensitivityX!: BxNumberStepper;
|
||||
private $mouseSensitivityY!: BxNumberStepper;
|
||||
private $mouseDeadzone!: BxNumberStepper;
|
||||
|
||||
constructor(title: string) {
|
||||
super(title, MkbMappingPresetsTable.getInstance());
|
||||
this.render();
|
||||
}
|
||||
|
||||
private onBindingKey = (e: MouseEvent) => {
|
||||
const $btn = e.target as HTMLButtonElement;
|
||||
if ($btn.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
private parseDataset($btn: BxKeyBindingButton): MkbButtonDataset {
|
||||
const dataset = $btn.dataset;
|
||||
return {
|
||||
keySlot: parseInt(dataset.keySlot!),
|
||||
buttonIndex: parseInt(dataset.buttonIndex!),
|
||||
};
|
||||
}
|
||||
|
||||
private onKeyChanged = (e: Event) => {
|
||||
const $current = e.target as BxKeyBindingButton;
|
||||
const keyInfo = $current.keyInfo;
|
||||
|
||||
// Unbind duplicated keys
|
||||
if (keyInfo) {
|
||||
for (const $elm of this.allKeyElements) {
|
||||
if ($elm !== $current && $elm.keyInfo?.code === keyInfo.code) {
|
||||
// Unbind manually
|
||||
$elm.unbindKey(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save preset
|
||||
this.savePreset();
|
||||
}
|
||||
|
||||
private render() {
|
||||
const $rows = CE('div', {},
|
||||
CE('i', { class: 'bx-mkb-note' }, t('right-click-to-unbind')),
|
||||
);
|
||||
|
||||
for (const buttonIndex of this.BUTTONS_ORDER) {
|
||||
const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex];
|
||||
|
||||
let $elm;
|
||||
const $fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < this.KEYS_PER_BUTTON; i++) {
|
||||
$elm = BxKeyBindingButton.create({
|
||||
title: buttonPrompt,
|
||||
isPrompt: true,
|
||||
allowedFlags: [BxKeyBindingButtonFlag.KEYBOARD_PRESS, BxKeyBindingButtonFlag.MOUSE_CLICK, BxKeyBindingButtonFlag.MOUSE_WHEEL],
|
||||
onChanged: this.onKeyChanged,
|
||||
});
|
||||
|
||||
$elm.dataset.buttonIndex = buttonIndex.toString();
|
||||
$elm.dataset.keySlot = i.toString();
|
||||
|
||||
$elm.addEventListener('mouseup', this.onBindingKey);
|
||||
|
||||
$fragment.appendChild($elm);
|
||||
this.allKeyElements.push($elm);
|
||||
}
|
||||
|
||||
const $keyRow = CE('div', {
|
||||
class: 'bx-mkb-key-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
},
|
||||
CE('label', { title: buttonName }, buttonPrompt),
|
||||
$fragment,
|
||||
);
|
||||
|
||||
$rows.appendChild($keyRow);
|
||||
}
|
||||
|
||||
const savePreset = () => this.savePreset();
|
||||
const $extraSettings = CE('div', {},
|
||||
createSettingRow(
|
||||
t('map-mouse-to'),
|
||||
this.$mouseMapTo = BxSelectElement.create(CE('select', { _on: { input: savePreset } },
|
||||
CE('option', { value: MouseMapTo.RS }, t('right-stick')),
|
||||
CE('option', { value: MouseMapTo.LS }, t('left-stick')),
|
||||
CE('option', { value: MouseMapTo.OFF }, t('off')),
|
||||
)),
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('horizontal-sensitivity'),
|
||||
this.$mouseSensitivityX = BxNumberStepper.create('hor_sensitivity', 0, 1, 300, {
|
||||
suffix: '%',
|
||||
exactTicks: 50,
|
||||
}, savePreset),
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('vertical-sensitivity'),
|
||||
this.$mouseSensitivityY = BxNumberStepper.create('ver_sensitivity', 0, 1, 300, {
|
||||
suffix: '%',
|
||||
exactTicks: 50,
|
||||
}, savePreset),
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('deadzone-counterweight'),
|
||||
this.$mouseDeadzone = BxNumberStepper.create('deadzone_counterweight', 0, 1, 50, {
|
||||
suffix: '%',
|
||||
exactTicks: 10,
|
||||
}, savePreset),
|
||||
),
|
||||
);
|
||||
|
||||
this.$content = CE('div', {},
|
||||
$rows,
|
||||
$extraSettings,
|
||||
);
|
||||
}
|
||||
|
||||
protected switchPreset(id: number): void {
|
||||
const preset = this.allPresets.data[id];
|
||||
if (!preset) {
|
||||
this.currentPresetId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const presetData = preset.data;
|
||||
this.currentPresetId = id;
|
||||
const isDefaultPreset = id <= 0;
|
||||
this.updateButtonStates();
|
||||
|
||||
// Update buttons
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const { buttonIndex, keySlot } = this.parseDataset($elm);
|
||||
|
||||
const buttonKeys = presetData.mapping[buttonIndex];
|
||||
if (buttonKeys && buttonKeys[keySlot]) {
|
||||
$elm.bindKey({
|
||||
code: buttonKeys[keySlot],
|
||||
}, true)
|
||||
} else {
|
||||
$elm.unbindKey(true);
|
||||
}
|
||||
|
||||
$elm.disabled = isDefaultPreset;
|
||||
}
|
||||
|
||||
// Update mouse settings
|
||||
const mouse = presetData.mouse;
|
||||
this.$mouseMapTo.value = mouse.mapTo.toString();
|
||||
this.$mouseSensitivityX.value = mouse.sensitivityX.toString();
|
||||
this.$mouseSensitivityY.value = mouse.sensitivityY.toString();
|
||||
this.$mouseDeadzone.value = mouse.deadzoneCounterweight.toString();
|
||||
|
||||
this.$mouseMapTo.disabled = isDefaultPreset;
|
||||
this.$mouseSensitivityX.dataset.disabled = isDefaultPreset.toString();
|
||||
this.$mouseSensitivityY.dataset.disabled = isDefaultPreset.toString();
|
||||
this.$mouseDeadzone.dataset.disabled = isDefaultPreset.toString();
|
||||
}
|
||||
|
||||
private savePreset() {
|
||||
const presetData = deepClone(this.BLANK_PRESET_DATA) as MkbPresetData;
|
||||
|
||||
// Get mapping
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const { buttonIndex, keySlot } = this.parseDataset($elm);
|
||||
const mapping = presetData.mapping;
|
||||
if (!mapping[buttonIndex]) {
|
||||
mapping[buttonIndex] = [];
|
||||
}
|
||||
|
||||
if (!$elm.keyInfo) {
|
||||
// Remove empty key from mapping
|
||||
delete mapping[buttonIndex][keySlot];
|
||||
} else {
|
||||
mapping[buttonIndex][keySlot] = $elm.keyInfo.code as KeyCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Get mouse settings
|
||||
const mouse = presetData.mouse;
|
||||
mouse.mapTo = parseInt(this.$mouseMapTo.value) as MouseMapTo;
|
||||
mouse.sensitivityX = parseInt(this.$mouseSensitivityX.value);
|
||||
mouse.sensitivityY = parseInt(this.$mouseSensitivityY.value);
|
||||
mouse.deadzoneCounterweight = parseInt(this.$mouseDeadzone.value);
|
||||
|
||||
const oldPreset = this.allPresets.data[this.currentPresetId];
|
||||
const newPreset = {
|
||||
id: this.currentPresetId,
|
||||
name: oldPreset.name,
|
||||
data: presetData,
|
||||
};
|
||||
this.presetsDb.updatePreset(newPreset);
|
||||
|
||||
this.allPresets.data[this.currentPresetId] = newPreset;
|
||||
StreamSettings.refreshMkbSettings();
|
||||
}
|
||||
}
|
22
src/modules/ui/dialog/remote-play-dialog.ts
Normal file → Executable file
22
src/modules/ui/dialog/remote-play-dialog.ts
Normal file → Executable file
@@ -8,11 +8,12 @@ import { RemotePlayConsoleState, RemotePlayManager } from "@/modules/remote-play
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { BxLogger } from "@/utils/bx-logger";
|
||||
import { StreamResolution } from "@/enums/pref-values";
|
||||
|
||||
|
||||
export class RemotePlayNavigationDialog extends NavigationDialog {
|
||||
private static instance: RemotePlayNavigationDialog;
|
||||
public static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog());
|
||||
export class RemotePlayDialog extends NavigationDialog {
|
||||
private static instance: RemotePlayDialog;
|
||||
public static getInstance = () => RemotePlayDialog.instance ?? (RemotePlayDialog.instance = new RemotePlayDialog());
|
||||
private readonly LOG_TAG = 'RemotePlayNavigationDialog';
|
||||
|
||||
private readonly STATE_LABELS: Record<RemotePlayConsoleState, string> = {
|
||||
@@ -35,21 +36,18 @@ export class RemotePlayNavigationDialog extends NavigationDialog {
|
||||
|
||||
const $settingNote = CE('p', {});
|
||||
|
||||
const currentResolution = getPref(PrefKey.REMOTE_PLAY_RESOLUTION);
|
||||
const currentResolution = getPref(PrefKey.REMOTE_PLAY_STREAM_RESOLUTION);
|
||||
let $resolutions : HTMLSelectElement | NavigationElement = CE<HTMLSelectElement>('select', {},
|
||||
CE('option', {value: '1080p'}, '1080p'),
|
||||
CE('option', {value: '720p'}, '720p'),
|
||||
CE('option', { value: StreamResolution.DIM_720P }, '720p'),
|
||||
CE('option', { value: StreamResolution.DIM_1080P }, '1080p'),
|
||||
);
|
||||
|
||||
if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
|
||||
$resolutions = BxSelectElement.wrap($resolutions as HTMLSelectElement);
|
||||
}
|
||||
|
||||
$resolutions = BxSelectElement.create($resolutions as HTMLSelectElement);
|
||||
$resolutions.addEventListener('input', (e: Event) => {
|
||||
const value = (e.target as HTMLSelectElement).value;
|
||||
|
||||
$settingNote.textContent = value === '1080p' ? '✅ ' + t('can-stream-xbox-360-games') : '❌ ' + t('cant-stream-xbox-360-games');
|
||||
setPref(PrefKey.REMOTE_PLAY_RESOLUTION, value);
|
||||
setPref(PrefKey.REMOTE_PLAY_STREAM_RESOLUTION, value);
|
||||
});
|
||||
|
||||
($resolutions as any).value = currentResolution;
|
||||
@@ -67,7 +65,7 @@ export class RemotePlayNavigationDialog extends NavigationDialog {
|
||||
$fragment.appendChild($qualitySettings);
|
||||
|
||||
// Render consoles list
|
||||
const manager = RemotePlayManager.getInstance();
|
||||
const manager = RemotePlayManager.getInstance()!;
|
||||
const consoles = manager.getConsoles();
|
||||
|
||||
for (let con of consoles) {
|
||||
|
842
src/modules/ui/dialog/settings-dialog.ts
Normal file → Executable file
842
src/modules/ui/dialog/settings-dialog.ts
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
201
src/modules/ui/dialog/settings/controller-extra.ts
Executable file
201
src/modules/ui/dialog/settings/controller-extra.ts
Executable file
@@ -0,0 +1,201 @@
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { getUniqueGamepadNames } from "@/utils/gamepad";
|
||||
import { CE, removeChildElements, createButton, ButtonStyle, createSettingRow, renderPresetsList } from "@/utils/html";
|
||||
import { t } from "@/utils/translation";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import { ControllerShortcutsManagerDialog } from "../profile-manger/controller-shortcuts-manager-dialog";
|
||||
import type { SettingsDialog } from "../settings-dialog";
|
||||
import { ControllerShortcutsTable } from "@/utils/local-db/controller-shortcuts-table";
|
||||
import { BxNumberStepper } from "@/web-components/bx-number-stepper";
|
||||
import { ControllerSettingsTable } from "@/utils/local-db/controller-settings-table";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
|
||||
export class ControllerExtraSettings extends HTMLElement {
|
||||
currentControllerId!: string;
|
||||
controllerIds!: string[];
|
||||
|
||||
$selectControllers!: BxSelectElement;
|
||||
$selectShortcuts!: BxSelectElement;
|
||||
$vibrationIntensity!: BxNumberStepper;
|
||||
|
||||
updateLayout!: () => void;
|
||||
switchController!: (id: string) => void;
|
||||
getCurrentControllerId!: () => string | null;
|
||||
saveSettings!: () => void;
|
||||
|
||||
static renderSettings(this: SettingsDialog): HTMLElement {
|
||||
const $container = CE<ControllerExtraSettings>('label', {
|
||||
class: 'bx-settings-row bx-controller-extra-settings',
|
||||
});
|
||||
|
||||
$container.updateLayout = ControllerExtraSettings.updateLayout.bind($container);
|
||||
$container.switchController = ControllerExtraSettings.switchController.bind($container);
|
||||
$container.getCurrentControllerId = ControllerExtraSettings.getCurrentControllerId.bind($container);
|
||||
$container.saveSettings = ControllerExtraSettings.saveSettings.bind($container);
|
||||
|
||||
const $selectControllers = BxSelectElement.create(CE<HTMLSelectElement>('select', {
|
||||
autocomplete: 'off',
|
||||
_on: {
|
||||
input: (e: Event) => {
|
||||
$container.switchController($selectControllers.value);
|
||||
},
|
||||
},
|
||||
}));
|
||||
$selectControllers.classList.add('bx-full-width');
|
||||
|
||||
const $selectShortcuts = BxSelectElement.create(CE<HTMLSelectElement>('select', {
|
||||
autocomplete: 'off',
|
||||
_on: {
|
||||
input: $container.saveSettings,
|
||||
},
|
||||
}));
|
||||
|
||||
const $vibrationIntensity = BxNumberStepper.create('controller_vibration_intensity', 50, 0, 100, {
|
||||
steps: 10,
|
||||
suffix: '%',
|
||||
exactTicks: 20,
|
||||
customTextValue: (value: any) => {
|
||||
value = parseInt(value);
|
||||
return value === 0 ? t('off') : value + '%';
|
||||
},
|
||||
}, $container.saveSettings);
|
||||
|
||||
$container.append(
|
||||
CE('span', {}, t('no-controllers-connected')),
|
||||
CE('div', { class: 'bx-controller-extra-wrapper' },
|
||||
$selectControllers,
|
||||
|
||||
CE('div', {class: 'bx-sub-content-box'},
|
||||
createSettingRow(
|
||||
t('controller-shortcuts-in-game'),
|
||||
CE('div', {
|
||||
class: 'bx-preset-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
},
|
||||
$selectShortcuts,
|
||||
createButton({
|
||||
label: t('manage'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: () => ControllerShortcutsManagerDialog.getInstance().show({
|
||||
id: parseInt($container.$selectShortcuts.value),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
{ multiLines: true },
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('vibration-intensity'),
|
||||
$vibrationIntensity,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$container.$selectControllers = $selectControllers;
|
||||
$container.$selectShortcuts = $selectShortcuts;
|
||||
$container.$vibrationIntensity = $vibrationIntensity;
|
||||
|
||||
$container.updateLayout();
|
||||
|
||||
// Detect when gamepad connected/disconnect
|
||||
window.addEventListener('gamepadconnected', $container.updateLayout);
|
||||
window.addEventListener('gamepaddisconnected', $container.updateLayout);
|
||||
|
||||
// Refresh layout when parent dialog is shown
|
||||
this.onMountedCallbacks.push(() => {
|
||||
$container.updateLayout();
|
||||
});
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
private static async updateLayout(this: ControllerExtraSettings, e?: GamepadEvent) {
|
||||
this.controllerIds = getUniqueGamepadNames();
|
||||
|
||||
this.dataset.hasGamepad = (this.controllerIds.length > 0).toString();
|
||||
if (this.controllerIds.length === 0) {
|
||||
// No gamepads
|
||||
return;
|
||||
}
|
||||
|
||||
const $fragment = document.createDocumentFragment();
|
||||
|
||||
// Remove old controllers
|
||||
removeChildElements(this.$selectControllers);
|
||||
|
||||
// Render controller list
|
||||
for (const name of this.controllerIds) {
|
||||
const $option = CE<HTMLOptionElement>('option', { value: name }, name);
|
||||
$fragment.appendChild($option);
|
||||
}
|
||||
|
||||
this.$selectControllers.appendChild($fragment);
|
||||
|
||||
// Render shortcut presets
|
||||
const allShortcutPresets = await ControllerShortcutsTable.getInstance().getPresets();
|
||||
renderPresetsList(this.$selectShortcuts, allShortcutPresets, null, true);
|
||||
|
||||
for (const name of this.controllerIds) {
|
||||
const $option = CE<HTMLOptionElement>('option', { value: name }, name);
|
||||
$fragment.appendChild($option);
|
||||
}
|
||||
|
||||
BxEvent.dispatch(this.$selectControllers, 'input');
|
||||
}
|
||||
|
||||
private static async switchController(this: ControllerExtraSettings, id: string) {
|
||||
this.currentControllerId = id;
|
||||
if (!this.getCurrentControllerId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const controllerSettings = await ControllerSettingsTable.getInstance().getControllerData(this.currentControllerId);
|
||||
|
||||
// Update UI
|
||||
this.$selectShortcuts.value = controllerSettings.shortcutPresetId.toString();
|
||||
this.$vibrationIntensity.value = controllerSettings.vibrationIntensity.toString();
|
||||
}
|
||||
|
||||
private static getCurrentControllerId(this: ControllerExtraSettings) {
|
||||
// Validate current ID
|
||||
if (this.currentControllerId) {
|
||||
if (this.controllerIds.includes(this.currentControllerId)) {
|
||||
return this.currentControllerId;
|
||||
}
|
||||
|
||||
this.currentControllerId = '';
|
||||
}
|
||||
|
||||
// Get first ID
|
||||
if (!this.currentControllerId) {
|
||||
this.currentControllerId = this.controllerIds[0];
|
||||
}
|
||||
|
||||
if (this.currentControllerId) {
|
||||
return this.currentControllerId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async saveSettings(this: ControllerExtraSettings) {
|
||||
if (!this.getCurrentControllerId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: ControllerSettingsRecord = {
|
||||
id: this.currentControllerId,
|
||||
data: {
|
||||
shortcutPresetId: parseInt(this.$selectShortcuts.value),
|
||||
vibrationIntensity: parseInt(this.$vibrationIntensity.value),
|
||||
},
|
||||
};
|
||||
|
||||
await ControllerSettingsTable.getInstance().put(data);
|
||||
|
||||
StreamSettings.refreshControllerSettings();
|
||||
}
|
||||
}
|
131
src/modules/ui/dialog/settings/mkb-extra.ts
Executable file
131
src/modules/ui/dialog/settings/mkb-extra.ts
Executable file
@@ -0,0 +1,131 @@
|
||||
import { ButtonStyle, CE, createButton, createSettingRow, renderPresetsList } from "@/utils/html";
|
||||
import type { SettingsDialog } from "../settings-dialog";
|
||||
import { MkbMappingPresetsTable } from "@/utils/local-db/mkb-mapping-presets-table";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import { t } from "@/utils/translation";
|
||||
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { MkbMappingManagerDialog } from "../profile-manger/mkb-mapping-manager-dialog";
|
||||
import { KeyboardShortcutsManagerDialog } from "../profile-manger/keyboard-shortcuts-manager-dialog";
|
||||
import { KeyboardShortcutsTable } from "@/utils/local-db/keyboard-shortcuts-table";
|
||||
import { SettingElement } from "@/utils/setting-element";
|
||||
import { STORAGE } from "@/utils/global";
|
||||
import { EmulatedMkbHandler } from "@/modules/mkb/mkb-handler";
|
||||
|
||||
export class MkbExtraSettings extends HTMLElement {
|
||||
private $mappingPresets!: BxSelectElement;
|
||||
private $shortcutsPresets!: BxSelectElement;
|
||||
|
||||
private updateLayout!: typeof MkbExtraSettings['updateLayout'];
|
||||
private saveMkbSettings!: typeof MkbExtraSettings['saveMkbSettings'];
|
||||
private saveShortcutsSettings!: typeof MkbExtraSettings['saveShortcutsSettings'];
|
||||
|
||||
static renderSettings(this: SettingsDialog): HTMLElement {
|
||||
const $container = document.createDocumentFragment() as unknown as MkbExtraSettings;
|
||||
|
||||
$container.updateLayout = MkbExtraSettings.updateLayout.bind($container);
|
||||
$container.saveMkbSettings = MkbExtraSettings.saveMkbSettings.bind($container);
|
||||
$container.saveShortcutsSettings = MkbExtraSettings.saveShortcutsSettings.bind($container);
|
||||
|
||||
const $mappingPresets = BxSelectElement.create(CE<HTMLSelectElement>('select', {
|
||||
autocomplete: 'off',
|
||||
_on: {
|
||||
input: $container.saveMkbSettings,
|
||||
},
|
||||
}));
|
||||
|
||||
const $shortcutsPresets = BxSelectElement.create(CE<HTMLSelectElement>('select', {
|
||||
autocomplete: 'off',
|
||||
_on: {
|
||||
input: $container.saveShortcutsSettings,
|
||||
},
|
||||
}));
|
||||
|
||||
$container.append(
|
||||
...(getPref(PrefKey.MKB_ENABLED) ? [
|
||||
createSettingRow(
|
||||
t('virtual-controller'),
|
||||
CE('div', {
|
||||
class: 'bx-preset-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
},
|
||||
$mappingPresets,
|
||||
createButton({
|
||||
label: t('manage'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: () => MkbMappingManagerDialog.getInstance().show({
|
||||
id: parseInt($container.$mappingPresets.value),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
{ multiLines: true },
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('virtual-controller-slot'),
|
||||
SettingElement.fromPref(PrefKey.MKB_P1_SLOT, STORAGE.Global, () => {
|
||||
EmulatedMkbHandler.getInstance()?.updateGamepadSlots();
|
||||
}),
|
||||
),
|
||||
] : []),
|
||||
|
||||
createSettingRow(
|
||||
t('keyboard-shortcuts-in-game'),
|
||||
CE('div', {
|
||||
class: 'bx-preset-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
},
|
||||
$shortcutsPresets,
|
||||
createButton({
|
||||
label: t('manage'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: () => KeyboardShortcutsManagerDialog.getInstance().show({
|
||||
id: parseInt($container.$shortcutsPresets.value),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
{ multiLines: true },
|
||||
),
|
||||
);
|
||||
|
||||
$container.$mappingPresets = $mappingPresets;
|
||||
$container.$shortcutsPresets = $shortcutsPresets;
|
||||
|
||||
$container.updateLayout();
|
||||
// Refresh layout when parent dialog is shown
|
||||
this.onMountedCallbacks.push(() => {
|
||||
$container.updateLayout();
|
||||
});
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
private static async updateLayout(this: MkbExtraSettings) {
|
||||
// Render shortcut presets
|
||||
const mappingPresets = await MkbMappingPresetsTable.getInstance().getPresets();
|
||||
renderPresetsList(this.$mappingPresets, mappingPresets, getPref<MkbPresetId>(PrefKey.MKB_P1_MAPPING_PRESET_ID), false);
|
||||
|
||||
// Render shortcut presets
|
||||
const shortcutsPresets = await KeyboardShortcutsTable.getInstance().getPresets();
|
||||
renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getPref<MkbPresetId>(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID), true);
|
||||
}
|
||||
|
||||
private static async saveMkbSettings(this: MkbExtraSettings) {
|
||||
const presetId = parseInt(this.$mappingPresets.value);
|
||||
setPref<MkbPresetId>(PrefKey.MKB_P1_MAPPING_PRESET_ID, presetId);
|
||||
|
||||
StreamSettings.refreshMkbSettings();
|
||||
}
|
||||
|
||||
private static async saveShortcutsSettings(this: MkbExtraSettings) {
|
||||
const presetId = parseInt(this.$shortcutsPresets.value);
|
||||
setPref<KeyboardShortcutsPresetId>(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID, presetId);
|
||||
|
||||
StreamSettings.refreshKeyboardShortcuts();
|
||||
}
|
||||
}
|
337
src/modules/ui/dialog/settings/suggestions.ts
Executable file
337
src/modules/ui/dialog/settings/suggestions.ts
Executable file
@@ -0,0 +1,337 @@
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { BX_FLAGS, NATIVE_FETCH, type BxFlags } from "@/utils/bx-flags";
|
||||
import { STORAGE } from "@/utils/global";
|
||||
import { CE, removeChildElements, createButton, ButtonStyle, escapeCssSelector } from "@/utils/html";
|
||||
import type { BxHtmlSettingElement } from "@/utils/setting-element";
|
||||
import { getPref, setPref, getPrefDefinition } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { t } from "@/utils/translation";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import type { SettingsDialog } from "../settings-dialog";
|
||||
import type { RecommendedSettings, SuggestedSettingProfile } from "@/types/setting-definition";
|
||||
import { DeviceVibrationMode, TouchControllerMode } from "@/enums/pref-values";
|
||||
import { GhPagesUtils } from "@/utils/gh-pages";
|
||||
|
||||
export class SuggestionsSetting {
|
||||
static async renderSuggestions(this: SettingsDialog, e: Event) {
|
||||
const $btnSuggest = (e.target as HTMLElement).closest('div')!;
|
||||
$btnSuggest.toggleAttribute('bx-open');
|
||||
|
||||
let $content = $btnSuggest.nextElementSibling as HTMLElement;
|
||||
if ($content) {
|
||||
BxEvent.dispatch($content.querySelector('select'), 'input');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get labels
|
||||
let settingTabGroup: keyof typeof this.SETTINGS_UI;
|
||||
for (settingTabGroup in this.SETTINGS_UI) {
|
||||
const settingTab = this.SETTINGS_UI[settingTabGroup];
|
||||
|
||||
if (!settingTab || !settingTab.items || typeof settingTab.items === 'function') {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const settingTabContent of settingTab.items) {
|
||||
if (!settingTabContent || settingTabContent instanceof HTMLElement || !settingTabContent.items) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const setting of settingTabContent.items) {
|
||||
let prefKey: PrefKey | undefined;
|
||||
|
||||
if (typeof setting === 'string') {
|
||||
prefKey = setting;
|
||||
} else if (typeof setting === 'object') {
|
||||
prefKey = setting.pref as PrefKey;
|
||||
}
|
||||
|
||||
if (prefKey) {
|
||||
this.suggestedSettingLabels[prefKey] = settingTabContent.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get recommended settings for Android devices
|
||||
let recommendedDevice: string | null = '';
|
||||
|
||||
if (BX_FLAGS.DeviceInfo.deviceType.includes('android')) {
|
||||
if (BX_FLAGS.DeviceInfo.androidInfo) {
|
||||
recommendedDevice = await SuggestionsSetting.getRecommendedSettings.call(this, BX_FLAGS.DeviceInfo.androidInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
recommendedDevice = await this.getRecommendedSettings({
|
||||
manufacturer: 'Lenovo',
|
||||
board: 'kona',
|
||||
model: 'Lenovo TB-9707F',
|
||||
});
|
||||
*/
|
||||
|
||||
const hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0;
|
||||
|
||||
// Add some specific setings based on device type
|
||||
const deviceType = BX_FLAGS.DeviceInfo.deviceType;
|
||||
if (deviceType === 'android-handheld') {
|
||||
// Disable touch
|
||||
SuggestionsSetting.addDefaultSuggestedSetting.call(this, PrefKey.TOUCH_CONTROLLER_MODE, TouchControllerMode.OFF);
|
||||
// Enable device vibration
|
||||
SuggestionsSetting.addDefaultSuggestedSetting.call(this, PrefKey.DEVICE_VIBRATION_MODE, DeviceVibrationMode.ON);
|
||||
} else if (deviceType === 'android') {
|
||||
// Enable device vibration
|
||||
SuggestionsSetting.addDefaultSuggestedSetting.call(this, PrefKey.DEVICE_VIBRATION_MODE, DeviceVibrationMode.AUTO);
|
||||
} else if (deviceType === 'android-tv') {
|
||||
// Disable touch
|
||||
SuggestionsSetting.addDefaultSuggestedSetting.call(this, PrefKey.TOUCH_CONTROLLER_MODE, TouchControllerMode.OFF);
|
||||
}
|
||||
|
||||
// Set value for Default profile
|
||||
SuggestionsSetting.generateDefaultSuggestedSettings.call(this);
|
||||
|
||||
// Start rendering
|
||||
const $suggestedSettings = CE('div', {class: 'bx-suggest-wrapper'});
|
||||
const $select = CE<HTMLSelectElement>('select', {},
|
||||
hasRecommendedSettings && CE('option', {value: 'recommended'}, t('recommended')),
|
||||
!hasRecommendedSettings && CE('option', {value: 'highest'}, t('highest-quality')),
|
||||
CE('option', {value: 'default'}, t('default')),
|
||||
CE('option', {value: 'lowest'}, t('lowest-quality')),
|
||||
);
|
||||
$select.addEventListener('input', e => {
|
||||
const profile = $select.value as SuggestedSettingProfile;
|
||||
|
||||
// Empty children
|
||||
removeChildElements($suggestedSettings);
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
let note: HTMLElement | string | undefined;
|
||||
if (profile === 'recommended') {
|
||||
note = t('recommended-settings-for-device', {device: recommendedDevice});
|
||||
} else if (profile === 'highest') {
|
||||
// Add note for "Highest quality" profile
|
||||
note = '⚠️ ' + t('highest-quality-note');
|
||||
}
|
||||
|
||||
note && fragment.appendChild(CE('div', {class: 'bx-suggest-note'}, note));
|
||||
|
||||
const settings = this.suggestedSettings[profile];
|
||||
let prefKey: PrefKey;
|
||||
for (prefKey in settings) {
|
||||
let suggestedValue;
|
||||
const definition = getPrefDefinition(prefKey);
|
||||
if (definition && definition.transformValue) {
|
||||
suggestedValue = definition.transformValue.get.call(definition, settings[prefKey]);
|
||||
} else {
|
||||
suggestedValue = settings[prefKey];
|
||||
}
|
||||
|
||||
const currentValue = getPref(prefKey, false);
|
||||
const currentValueText = STORAGE.Global.getValueText(prefKey, currentValue);
|
||||
const isSameValue = currentValue === suggestedValue;
|
||||
|
||||
let $child: HTMLElement;
|
||||
let $value: HTMLElement | string;
|
||||
if (isSameValue) {
|
||||
// No changes
|
||||
$value = currentValueText;
|
||||
} else {
|
||||
const suggestedValueText = STORAGE.Global.getValueText(prefKey, suggestedValue);
|
||||
$value = currentValueText + ' ➔ ' + suggestedValueText;
|
||||
}
|
||||
|
||||
let $checkbox: HTMLInputElement;
|
||||
const breadcrumb = this.suggestedSettingLabels[prefKey] + ' ❯ ' + STORAGE.Global.getLabel(prefKey);
|
||||
const id = escapeCssSelector(`bx_suggest_${prefKey}`);
|
||||
|
||||
$child = CE('div', {
|
||||
class: `bx-suggest-row ${isSameValue ? 'bx-suggest-ok' : 'bx-suggest-change'}`,
|
||||
},
|
||||
$checkbox = CE('input', {
|
||||
type: 'checkbox',
|
||||
tabindex: 0,
|
||||
checked: true,
|
||||
id: id,
|
||||
}),
|
||||
CE('label', {
|
||||
for: id,
|
||||
},
|
||||
CE('div', {
|
||||
class: 'bx-suggest-label',
|
||||
}, breadcrumb),
|
||||
CE('div', {
|
||||
class: 'bx-suggest-value',
|
||||
}, $value),
|
||||
),
|
||||
);
|
||||
|
||||
if (isSameValue) {
|
||||
$checkbox.disabled = true;
|
||||
$checkbox.checked = true;
|
||||
}
|
||||
|
||||
fragment.appendChild($child);
|
||||
}
|
||||
|
||||
$suggestedSettings.appendChild(fragment);
|
||||
});
|
||||
|
||||
BxEvent.dispatch($select, 'input');
|
||||
|
||||
const onClickApply = () => {
|
||||
const profile = $select.value as SuggestedSettingProfile;
|
||||
const settings = this.suggestedSettings[profile];
|
||||
|
||||
let prefKey: PrefKey;
|
||||
for (prefKey in settings) {
|
||||
let suggestedValue = settings[prefKey];
|
||||
|
||||
const $checkBox = $content.querySelector<HTMLInputElement>(`#bx_suggest_${escapeCssSelector(prefKey)}`)!;
|
||||
if (!$checkBox.checked || $checkBox.disabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $control = this.settingElements[prefKey] as HTMLElement;
|
||||
|
||||
// Set value directly if the control element is not available
|
||||
if (!$control) {
|
||||
setPref(prefKey, suggestedValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transform value
|
||||
const settingDefinition = getPrefDefinition(prefKey);
|
||||
if (settingDefinition.transformValue) {
|
||||
suggestedValue = settingDefinition.transformValue.get.call(settingDefinition, suggestedValue);
|
||||
}
|
||||
|
||||
if ('setValue' in $control) {
|
||||
($control as BxHtmlSettingElement).setValue(suggestedValue);
|
||||
} else {
|
||||
($control as HTMLInputElement).value = suggestedValue;
|
||||
}
|
||||
|
||||
BxEvent.dispatch($control, 'input', {
|
||||
manualTrigger: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh suggested settings
|
||||
BxEvent.dispatch($select, 'input');
|
||||
};
|
||||
|
||||
// Apply button
|
||||
const $btnApply = createButton({
|
||||
label: t('apply'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
onClick: onClickApply,
|
||||
});
|
||||
|
||||
$content = CE('div', {
|
||||
class: 'bx-sub-content-box bx-suggest-box',
|
||||
_nearby: {
|
||||
orientation: 'vertical',
|
||||
}
|
||||
},
|
||||
BxSelectElement.create($select, true),
|
||||
$suggestedSettings,
|
||||
$btnApply,
|
||||
|
||||
BX_FLAGS.DeviceInfo.deviceType.includes('android') && CE('a', {
|
||||
class: 'bx-suggest-link bx-focusable',
|
||||
href: 'https://better-xcloud.github.io/guide/android-webview-tweaks/',
|
||||
target: '_blank',
|
||||
tabindex: 0,
|
||||
}, '🤓 ' + t('how-to-improve-app-performance')),
|
||||
|
||||
BX_FLAGS.DeviceInfo.deviceType.includes('android') && !hasRecommendedSettings && CE('a', {
|
||||
class: 'bx-suggest-link bx-focusable',
|
||||
href: 'https://github.com/redphx/better-xcloud-devices',
|
||||
target: '_blank',
|
||||
tabindex: 0,
|
||||
}, t('suggest-settings-link')),
|
||||
);
|
||||
|
||||
$btnSuggest.insertAdjacentElement('afterend', $content);
|
||||
}
|
||||
|
||||
private static async getRecommendedSettings(this: SettingsDialog, androidInfo: BxFlags['DeviceInfo']['androidInfo']): Promise<string | null> {
|
||||
function normalize(str: string) {
|
||||
return str.toLowerCase()
|
||||
.trim()
|
||||
.replaceAll(/\s+/g, '-')
|
||||
.replaceAll(/-+/g, '-');
|
||||
}
|
||||
|
||||
// Get recommended settings from GitHub
|
||||
try {
|
||||
let {brand, board, model} = androidInfo!;
|
||||
brand = normalize(brand);
|
||||
board = normalize(board);
|
||||
model = normalize(model);
|
||||
|
||||
const url = GhPagesUtils.getUrl(`devices/${brand}/${board}-${model}.json`);
|
||||
const response = await NATIVE_FETCH(url);
|
||||
const json = (await response.json()) as RecommendedSettings;
|
||||
const recommended: PartialRecord<PrefKey, any> = {};
|
||||
|
||||
// Only supports schema version 2
|
||||
if (json.schema_version !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scriptSettings = json.settings.script;
|
||||
|
||||
// Set base settings
|
||||
if (scriptSettings._base) {
|
||||
let base = typeof scriptSettings._base === 'string' ? [scriptSettings._base] : scriptSettings._base;
|
||||
for (const profile of base) {
|
||||
Object.assign(recommended, this.suggestedSettings[profile]);
|
||||
}
|
||||
|
||||
delete scriptSettings._base;
|
||||
}
|
||||
|
||||
// Override settings
|
||||
let key: Exclude<keyof typeof scriptSettings, '_base'>;
|
||||
// @ts-ignore
|
||||
for (key in scriptSettings) {
|
||||
recommended[key] = scriptSettings[key];
|
||||
}
|
||||
|
||||
// Update device type in BxFlags
|
||||
BX_FLAGS.DeviceInfo.deviceType = json.device_type;
|
||||
|
||||
this.suggestedSettings.recommended = recommended;
|
||||
|
||||
return json.device_name;
|
||||
} catch (e) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static addDefaultSuggestedSetting(this: SettingsDialog, prefKey: PrefKey, value: any) {
|
||||
let key: keyof typeof this.suggestedSettings;
|
||||
for (key in this.suggestedSettings) {
|
||||
if (key !== 'default' && !(prefKey in this.suggestedSettings)) {
|
||||
this.suggestedSettings[key][prefKey] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static generateDefaultSuggestedSettings(this: SettingsDialog) {
|
||||
let key: keyof typeof this.suggestedSettings;
|
||||
for (key in this.suggestedSettings) {
|
||||
if (key === 'default') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let prefKey: PrefKey;
|
||||
for (prefKey in this.suggestedSettings[key]) {
|
||||
if (!(prefKey in this.suggestedSettings.default)) {
|
||||
this.suggestedSettings.default[prefKey] = getPrefDefinition(prefKey).default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
0
src/modules/ui/fullscreen-text.ts
Normal file → Executable file
0
src/modules/ui/fullscreen-text.ts
Normal file → Executable file
38
src/modules/ui/game-tile.ts
Normal file → Executable file
38
src/modules/ui/game-tile.ts
Normal file → Executable file
@@ -3,10 +3,14 @@ import { BxIcon } from "@/utils/bx-icon";
|
||||
import { CE, createSvgIcon, getReactProps, isElementVisible, secondsToHms } from "@/utils/html";
|
||||
import { XcloudApi } from "@/utils/xcloud-api";
|
||||
|
||||
export class GameTile {
|
||||
static #timeout: number | null;
|
||||
interface GameTimeElement extends HTMLElement {
|
||||
hasWaitTime?: boolean;
|
||||
}
|
||||
|
||||
static async #showWaitTime($elm: HTMLElement, productId: string) {
|
||||
export class GameTile {
|
||||
static timeoutId: number | null;
|
||||
|
||||
private static async showWaitTime($elm: HTMLElement, productId: string) {
|
||||
if (($elm as any).hasWaitTime) {
|
||||
return;
|
||||
}
|
||||
@@ -32,14 +36,14 @@ export class GameTile {
|
||||
}
|
||||
}
|
||||
|
||||
static #requestWaitTime($elm: HTMLElement, productId: string) {
|
||||
GameTile.#timeout && clearTimeout(GameTile.#timeout);
|
||||
GameTile.#timeout = window.setTimeout(async () => {
|
||||
GameTile.#showWaitTime($elm, productId);
|
||||
private static requestWaitTime($elm: HTMLElement, productId: string) {
|
||||
GameTile.timeoutId && clearTimeout(GameTile.timeoutId);
|
||||
GameTile.timeoutId = window.setTimeout(async () => {
|
||||
GameTile.showWaitTime($elm, productId);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
static #findProductId($elm: HTMLElement): string | null {
|
||||
private static findProductId($elm: HTMLElement): string | null {
|
||||
let productId = null;
|
||||
|
||||
try {
|
||||
@@ -55,6 +59,7 @@ export class GameTile {
|
||||
} else if ($elm.tagName === 'A' && $elm.className.includes('GameItem')) {
|
||||
let props = getReactProps($elm.parentElement!);
|
||||
props = props.children.props;
|
||||
|
||||
if (props.location !== 'NonStreamableGameItem') {
|
||||
if ('productId' in props) {
|
||||
productId = props.productId;
|
||||
@@ -73,19 +78,20 @@ export class GameTile {
|
||||
window.addEventListener(BxEvent.NAVIGATION_FOCUS_CHANGED, e => {
|
||||
const $elm = (e as any).element;
|
||||
const className = $elm.className || '';
|
||||
|
||||
if (className.includes('MruGameCard')) {
|
||||
// Show the wait time of every games in the "Jump back in" section all at once
|
||||
const $ol = $elm.closest('ol');
|
||||
if ($ol && !($ol as any).hasWaitTime) {
|
||||
($ol as any).hasWaitTime = true;
|
||||
$ol.querySelectorAll('button[class*=MruGameCard]').forEach(($elm: HTMLElement) => {
|
||||
const productId = GameTile.#findProductId($elm);
|
||||
productId && GameTile.#showWaitTime($elm, productId);
|
||||
const $ol = $elm.closest('ol') as GameTimeElement;
|
||||
if ($ol && !$ol.hasWaitTime) {
|
||||
$ol.hasWaitTime = true;
|
||||
$ol.querySelectorAll<HTMLElement>('button[class*=MruGameCard]').forEach(($elm: HTMLElement) => {
|
||||
const productId = GameTile.findProductId($elm);
|
||||
productId && GameTile.showWaitTime($elm, productId);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const productId = GameTile.#findProductId($elm);
|
||||
productId && GameTile.#requestWaitTime($elm, productId);
|
||||
const productId = GameTile.findProductId($elm);
|
||||
productId && GameTile.requestWaitTime($elm, productId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
20
src/modules/ui/guide-menu.ts
Normal file → Executable file
20
src/modules/ui/guide-menu.ts
Normal file → Executable file
@@ -4,7 +4,7 @@ import { BxEvent } from "@/utils/bx-event";
|
||||
import { AppInterface, STATES } from "@/utils/global";
|
||||
import { createButton, ButtonStyle, CE } from "@/utils/html";
|
||||
import { t } from "@/utils/translation";
|
||||
import { SettingsNavigationDialog } from "./dialog/settings-dialog";
|
||||
import { SettingsDialog } from "./dialog/settings-dialog";
|
||||
import { TrueAchievements } from "@/utils/true-achievements";
|
||||
import { BxIcon } from "@/utils/bx-icon";
|
||||
|
||||
@@ -38,15 +38,15 @@ export class GuideMenu {
|
||||
scriptSettings: createButton({
|
||||
label: t('better-xcloud'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY,
|
||||
onClick: (() => {
|
||||
onClick: () => {
|
||||
// Wait until the Guide dialog is closed
|
||||
window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, e => {
|
||||
setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50);
|
||||
setTimeout(() => SettingsDialog.getInstance().show(), 50);
|
||||
}, {once: true});
|
||||
|
||||
// Close all xCloud's dialogs
|
||||
this.closeGuideMenu();
|
||||
}).bind(this),
|
||||
},
|
||||
}),
|
||||
|
||||
closeApp: AppInterface && createButton({
|
||||
@@ -68,7 +68,7 @@ export class GuideMenu {
|
||||
label: t('reload-page'),
|
||||
title: t('reload-page'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
onClick: (() => {
|
||||
onClick: () => {
|
||||
// Close all xCloud's dialogs
|
||||
this.closeGuideMenu();
|
||||
|
||||
@@ -77,7 +77,7 @@ export class GuideMenu {
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
}).bind(this),
|
||||
},
|
||||
}),
|
||||
|
||||
backToHome: createButton({
|
||||
@@ -85,12 +85,12 @@ export class GuideMenu {
|
||||
label: t('back-to-home'),
|
||||
title: t('back-to-home'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
onClick: (() => {
|
||||
onClick: () => {
|
||||
// Close all xCloud's dialogs
|
||||
this.closeGuideMenu();
|
||||
|
||||
confirm(t('back-to-home-confirm')) && (window.location.href = window.location.href.substring(0, 31));
|
||||
}).bind(this),
|
||||
},
|
||||
attributes: {
|
||||
'data-state': 'playing',
|
||||
},
|
||||
@@ -164,7 +164,7 @@ export class GuideMenu {
|
||||
$target.insertAdjacentElement('afterend', $buttons);
|
||||
}
|
||||
|
||||
async onShown(e: Event) {
|
||||
private onShown = async (e: Event) => {
|
||||
const where = (e as any).where as GuideMenuTab;
|
||||
|
||||
if (where === GuideMenuTab.HOME) {
|
||||
@@ -174,7 +174,7 @@ export class GuideMenu {
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown.bind(this));
|
||||
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown);
|
||||
}
|
||||
|
||||
observe($addedElm: HTMLElement) {
|
||||
|
12
src/modules/ui/header.ts
Normal file → Executable file
12
src/modules/ui/header.ts
Normal file → Executable file
@@ -4,7 +4,7 @@ import { BxIcon } from "@utils/bx-icon";
|
||||
import { getPreferredServerRegion } from "@utils/region";
|
||||
import { RemotePlayManager } from "@/modules/remote-play-manager";
|
||||
import { t } from "@utils/translation";
|
||||
import { SettingsNavigationDialog } from "./dialog/settings-dialog";
|
||||
import { SettingsDialog } from "./dialog/settings-dialog";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { BxLogger } from "@/utils/bx-logger";
|
||||
@@ -29,14 +29,14 @@ export class HeaderSection {
|
||||
icon: BxIcon.REMOTE_PLAY,
|
||||
title: t('remote-play'),
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR,
|
||||
onClick: e => RemotePlayManager.getInstance().togglePopup(),
|
||||
onClick: e => RemotePlayManager.getInstance()?.togglePopup(),
|
||||
});
|
||||
|
||||
this.$btnSettings = createButton({
|
||||
classes: ['bx-header-settings-button'],
|
||||
label: '???',
|
||||
style: ButtonStyle.FROSTED | ButtonStyle.DROP_SHADOW | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_HEIGHT,
|
||||
onClick: e => SettingsNavigationDialog.getInstance().show(),
|
||||
onClick: e => SettingsDialog.getInstance().show(),
|
||||
});
|
||||
|
||||
this.$buttonsWrapper = CE('div', {},
|
||||
@@ -50,7 +50,7 @@ export class HeaderSection {
|
||||
return;
|
||||
}
|
||||
|
||||
const PREF_LATEST_VERSION = getPref(PrefKey.LATEST_VERSION);
|
||||
const PREF_LATEST_VERSION = getPref<VersionLatest>(PrefKey.VERSION_LATEST);
|
||||
|
||||
// Setup Settings button
|
||||
const $btnSettings = this.$btnSettings;
|
||||
@@ -69,7 +69,7 @@ export class HeaderSection {
|
||||
$parent.appendChild(this.$buttonsWrapper);
|
||||
}
|
||||
|
||||
private checkHeader() {
|
||||
private checkHeader = () => {
|
||||
let $target = document.querySelector('#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]');
|
||||
if (!$target) {
|
||||
$target = document.querySelector('div[class^=UnsupportedMarketPage-module__buttons]');
|
||||
@@ -90,7 +90,7 @@ export class HeaderSection {
|
||||
this.observer && this.observer.disconnect();
|
||||
this.observer = new MutationObserver(mutationList => {
|
||||
this.timeoutId && clearTimeout(this.timeoutId);
|
||||
this.timeoutId = window.setTimeout(this.checkHeader.bind(this), 2000);
|
||||
this.timeoutId = window.setTimeout(this.checkHeader, 2000);
|
||||
});
|
||||
this.observer.observe($root, {subtree: true, childList: true});
|
||||
|
||||
|
2
src/modules/ui/product-details.ts
Normal file → Executable file
2
src/modules/ui/product-details.ts
Normal file → Executable file
@@ -10,7 +10,6 @@ export class ProductDetailsPage {
|
||||
icon: BxIcon.CREATE_SHORTCUT,
|
||||
label: t('create-shortcut'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
tabIndex: 0,
|
||||
onClick: e => {
|
||||
AppInterface.createShortcut(window.location.pathname.substring(6));
|
||||
},
|
||||
@@ -20,7 +19,6 @@ export class ProductDetailsPage {
|
||||
icon: BxIcon.DOWNLOAD,
|
||||
label: t('wallpaper'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
tabIndex: 0,
|
||||
onClick: e => {
|
||||
const details = parseDetailsPath(window.location.pathname);
|
||||
details && AppInterface.downloadWallpapers(details.titleSlug, details.productId);
|
||||
|
0
src/modules/ui/ui.ts
Normal file → Executable file
0
src/modules/ui/ui.ts
Normal file → Executable file
@@ -1,152 +0,0 @@
|
||||
import { AppInterface } from "@utils/global";
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { ControllerDeviceVibration, getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
const VIBRATION_DATA_MAP = {
|
||||
'gamepadIndex': 8,
|
||||
'leftMotorPercent': 8,
|
||||
'rightMotorPercent': 8,
|
||||
'leftTriggerMotorPercent': 8,
|
||||
'rightTriggerMotorPercent': 8,
|
||||
'durationMs': 16,
|
||||
// 'delayMs': 16,
|
||||
// 'repeat': 8,
|
||||
};
|
||||
|
||||
type VibrationData = {
|
||||
[key in keyof typeof VIBRATION_DATA_MAP]?: number;
|
||||
}
|
||||
|
||||
export class VibrationManager {
|
||||
static #playDeviceVibration(data: Required<VibrationData>) {
|
||||
// console.log(+new Date, data);
|
||||
|
||||
if (AppInterface) {
|
||||
AppInterface.vibrate(JSON.stringify(data), window.BX_VIBRATION_INTENSITY);
|
||||
return;
|
||||
}
|
||||
|
||||
const intensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * window.BX_VIBRATION_INTENSITY;
|
||||
if (intensity === 0 || intensity === 100) {
|
||||
// Stop vibration
|
||||
window.navigator.vibrate(intensity ? data.durationMs : 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const pulseDuration = 200;
|
||||
const onDuration = Math.floor(pulseDuration * intensity / 100);
|
||||
const offDuration = pulseDuration - onDuration;
|
||||
|
||||
const repeats = Math.ceil(data.durationMs / pulseDuration);
|
||||
|
||||
const pulses = Array(repeats).fill([onDuration, offDuration]).flat();
|
||||
// console.log(pulses);
|
||||
|
||||
window.navigator.vibrate(pulses);
|
||||
}
|
||||
|
||||
static supportControllerVibration() {
|
||||
return Gamepad.prototype.hasOwnProperty('vibrationActuator');
|
||||
}
|
||||
|
||||
static supportDeviceVibration() {
|
||||
return !!window.navigator.vibrate;
|
||||
}
|
||||
|
||||
static updateGlobalVars(stopVibration: boolean = true) {
|
||||
window.BX_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? getPref(PrefKey.CONTROLLER_ENABLE_VIBRATION) : false;
|
||||
window.BX_VIBRATION_INTENSITY = getPref(PrefKey.CONTROLLER_VIBRATION_INTENSITY) / 100;
|
||||
|
||||
if (!VibrationManager.supportDeviceVibration()) {
|
||||
window.BX_ENABLE_DEVICE_VIBRATION = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop vibration
|
||||
stopVibration && window.navigator.vibrate(0);
|
||||
|
||||
const value = getPref(PrefKey.CONTROLLER_DEVICE_VIBRATION);
|
||||
let enabled;
|
||||
|
||||
if (value === ControllerDeviceVibration.ON) {
|
||||
enabled = true;
|
||||
} else if (value === ControllerDeviceVibration.AUTO) {
|
||||
enabled = true;
|
||||
const gamepads = window.navigator.getGamepads();
|
||||
for (const gamepad of gamepads) {
|
||||
if (gamepad) {
|
||||
enabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
window.BX_ENABLE_DEVICE_VIBRATION = enabled;
|
||||
}
|
||||
|
||||
static #onMessage(e: MessageEvent) {
|
||||
if (!window.BX_ENABLE_DEVICE_VIBRATION) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof e !== 'object' || !(e.data instanceof ArrayBuffer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataView = new DataView(e.data);
|
||||
let offset = 0;
|
||||
|
||||
let messageType;
|
||||
if (dataView.byteLength === 13) { // version >= 8
|
||||
messageType = dataView.getUint16(offset, true);
|
||||
offset += Uint16Array.BYTES_PER_ELEMENT;
|
||||
} else {
|
||||
messageType = dataView.getUint8(offset);
|
||||
offset += Uint8Array.BYTES_PER_ELEMENT;
|
||||
}
|
||||
|
||||
if (!(messageType & 128)) { // Vibration
|
||||
return;
|
||||
}
|
||||
|
||||
const vibrationType = dataView.getUint8(offset);
|
||||
offset += Uint8Array.BYTES_PER_ELEMENT;
|
||||
|
||||
if (vibrationType !== 0) { // FourMotorRumble
|
||||
return;
|
||||
}
|
||||
|
||||
const data: VibrationData = {};
|
||||
let key: keyof typeof VIBRATION_DATA_MAP;
|
||||
for (key in VIBRATION_DATA_MAP) {
|
||||
if (VIBRATION_DATA_MAP[key] === 16) {
|
||||
data[key] = dataView.getUint16(offset, true);
|
||||
offset += Uint16Array.BYTES_PER_ELEMENT;
|
||||
} else {
|
||||
data[key] = dataView.getUint8(offset);
|
||||
offset += Uint8Array.BYTES_PER_ELEMENT;
|
||||
}
|
||||
}
|
||||
|
||||
VibrationManager.#playDeviceVibration(data as Required<VibrationData>);
|
||||
}
|
||||
|
||||
static initialSetup() {
|
||||
window.addEventListener('gamepadconnected', e => VibrationManager.updateGlobalVars());
|
||||
window.addEventListener('gamepaddisconnected', e => VibrationManager.updateGlobalVars());
|
||||
|
||||
VibrationManager.updateGlobalVars(false);
|
||||
|
||||
window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => {
|
||||
const dataChannel = (e as any).dataChannel;
|
||||
if (!dataChannel || dataChannel.label !== 'input') {
|
||||
return;
|
||||
}
|
||||
|
||||
dataChannel.addEventListener('message', VibrationManager.#onMessage);
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user