mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-13 16:39:16 +02:00
Controller customization feature
This commit is contained in:
@@ -20,7 +20,7 @@ export class 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) {
|
||||
if (getPref(PrefKey.GAME_BAR_POSITION) !== GameBarPosition.OFF) {
|
||||
GameBar.instance = new GameBar();
|
||||
} else {
|
||||
GameBar.instance = null;
|
||||
@@ -46,7 +46,7 @@ export class GameBar {
|
||||
|
||||
let $container;
|
||||
|
||||
const position = getPref<GameBarPosition>(PrefKey.GAME_BAR_POSITION);
|
||||
const position = getPref(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' }),
|
||||
@@ -55,7 +55,7 @@ export class GameBar {
|
||||
|
||||
this.actions = [
|
||||
new ScreenshotAction(),
|
||||
...(STATES.userAgent.capabilities.touch && (getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) !== TouchControllerMode.OFF) ? [new TouchControlAction()] : []),
|
||||
...(STATES.userAgent.capabilities.touch && (getPref(PrefKey.TOUCH_CONTROLLER_MODE) !== TouchControllerMode.OFF) ? [new TouchControlAction()] : []),
|
||||
new SpeakerAction(),
|
||||
new RendererAction(),
|
||||
new MicrophoneAction(),
|
||||
|
@@ -37,7 +37,7 @@ export class LoadingScreen {
|
||||
|
||||
LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl);
|
||||
|
||||
if (getPref<LoadingScreenRocket>(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE) {
|
||||
if (getPref(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE) {
|
||||
LoadingScreen.hideRocket();
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export class LoadingScreen {
|
||||
|
||||
static setupWaitTime(waitTime: number) {
|
||||
// Hide rocket when queing
|
||||
if (getPref<LoadingScreenRocket>(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE_QUEUE) {
|
||||
if (getPref(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE_QUEUE) {
|
||||
LoadingScreen.hideRocket();
|
||||
}
|
||||
|
||||
|
@@ -580,7 +580,7 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
|
||||
updateGamepadSlots() {
|
||||
// Set gamepad slot
|
||||
this.VIRTUAL_GAMEPAD.index = getPref<number>(PrefKey.MKB_P1_SLOT) - 1;
|
||||
this.VIRTUAL_GAMEPAD.index = getPref(PrefKey.MKB_P1_SLOT) - 1;
|
||||
}
|
||||
|
||||
start() {
|
||||
|
@@ -7,6 +7,7 @@ import { NativeMkbHandler } from "./native-mkb-handler";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
import { BxEventBus } from "@/utils/bx-event-bus";
|
||||
import { BxIcon } from "@/utils/bx-icon";
|
||||
|
||||
type MkbPopupType = 'virtual' | 'native';
|
||||
|
||||
@@ -90,6 +91,7 @@ export class MkbPopup {
|
||||
|
||||
createButton({
|
||||
label: t('manage'),
|
||||
icon: BxIcon.MANAGE,
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: () => {
|
||||
const dialog = SettingsDialog.getInstance();
|
||||
|
@@ -43,7 +43,7 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
private readonly LOG_TAG = 'NativeMkbHandler';
|
||||
|
||||
static isAllowed = () => {
|
||||
return STATES.browser.capabilities.emulatedNativeMkb && getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON;
|
||||
return STATES.browser.capabilities.emulatedNativeMkb && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON;
|
||||
}
|
||||
|
||||
private pointerClient: PointerClient | undefined;
|
||||
|
@@ -1,22 +1,30 @@
|
||||
import type { PatchArray, PatchName, PatchPage } from "./patcher";
|
||||
|
||||
export class PatcherUtils {
|
||||
static indexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number {
|
||||
static indexOf(txt: string, searchString: string, startIndex: number, maxRange=0, after=false): number {
|
||||
if (startIndex < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const index = txt.indexOf(searchString, startIndex);
|
||||
if (index < 0 || (maxRange && index - startIndex > maxRange)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return index;
|
||||
return after ? index + searchString.length : index;
|
||||
}
|
||||
|
||||
static lastIndexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number {
|
||||
static lastIndexOf(txt: string, searchString: string, startIndex: number, maxRange=0, after=false): number {
|
||||
if (startIndex < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const index = txt.lastIndexOf(searchString, startIndex);
|
||||
if (index < 0 || (maxRange && startIndex - index > maxRange)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return index;
|
||||
return after ? index + searchString.length : index;
|
||||
}
|
||||
|
||||
static insertAt(txt: string, index: number, insertString: string): string {
|
||||
|
@@ -4,10 +4,10 @@ import { BxLogger } from "@utils/bx-logger";
|
||||
import { blockSomeNotifications, hashCode, renderString } from "@utils/utils";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
|
||||
import codeControllerShortcuts from "./patches/controller-shortcuts.js" with { type: "text" };
|
||||
import codeControllerCustomization from "./patches/controller-customization.js" with { type: "text" };
|
||||
import codePollGamepad from "./patches/poll-gamepad.js" with { type: "text" };
|
||||
import codeExposeStreamSession from "./patches/expose-stream-session.js" with { type: "text" };
|
||||
import codeLocalCoOpEnable from "./patches/local-co-op-enable.js" with { type: "text" };
|
||||
import codeRemotePlayEnable from "./patches/remote-play-enable.js" with { type: "text" };
|
||||
import codeRemotePlayKeepAlive from "./patches/remote-play-keep-alive.js" with { type: "text" };
|
||||
import codeVibrationAdjust from "./patches/vibration-adjust.js" with { type: "text" };
|
||||
import { PrefKey, StorageKey } from "@/enums/pref-keys.js";
|
||||
@@ -88,7 +88,7 @@ const PATCHES = {
|
||||
return false;
|
||||
}
|
||||
|
||||
const layout = getPref<UiLayout>(PrefKey.UI_LAYOUT) === UiLayout.TV ? UiLayout.TV : UiLayout.DEFAULT;
|
||||
const layout = getPref(PrefKey.UI_LAYOUT) === UiLayout.TV ? UiLayout.TV : UiLayout.DEFAULT;
|
||||
return str.replace(text, `?"${layout}":"${layout}"`);
|
||||
},
|
||||
|
||||
@@ -120,7 +120,9 @@ const PATCHES = {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str.replace(text, codeRemotePlayEnable);
|
||||
const newCode = `connectMode: window.BX_REMOTE_PLAY_CONFIG ? "xhome-connect" : "cloud-connect",
|
||||
remotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',`;
|
||||
return str.replace(text, newCode);
|
||||
},
|
||||
|
||||
// Remote Play: Disable achievement toast
|
||||
@@ -191,17 +193,34 @@ const PATCHES = {
|
||||
codeBlock = codeBlock.replace('this.inputPollingDurationStats.addValue', '');
|
||||
}
|
||||
|
||||
// Map the Share button on Xbox Series controller with the capturing screenshot feature
|
||||
const match = codeBlock.match(/this\.gamepadTimestamps\.set\((\w+)\.index/);
|
||||
if (match) {
|
||||
const gamepadVar = match[1];
|
||||
const newCode = renderString(codeControllerShortcuts, {
|
||||
gamepadVar,
|
||||
});
|
||||
|
||||
codeBlock = codeBlock.replace('this.gamepadTimestamps.set', newCode + 'this.gamepadTimestamps.set');
|
||||
// Controller shortcuts
|
||||
let match = codeBlock.match(/this\.gamepadTimestamps\.set\(([A-Za-z0-9_$]+)\.index/);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let newCode = renderString(codePollGamepad, {
|
||||
gamepadVar: match[1],
|
||||
});
|
||||
codeBlock = codeBlock.replace('this.gamepadTimestamps.set', newCode + 'this.gamepadTimestamps.set');
|
||||
|
||||
// Controller customization
|
||||
match = codeBlock.match(/let ([A-Za-z0-9_$]+)=this\.gamepadMappings\.find/);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const xCloudGamepadVar = match[1];
|
||||
const inputFeedbackManager = PatcherUtils.indexOf(codeBlock, 'this.inputFeedbackManager.onGamepadConnected(', 0, 10000);
|
||||
const backetIndex = PatcherUtils.indexOf(codeBlock, '}', inputFeedbackManager, 100);
|
||||
if (backetIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let customizationCode = ';'; // End previous code line
|
||||
customizationCode += renderString(codeControllerCustomization, { xCloudGamepadVar });
|
||||
codeBlock = PatcherUtils.insertAt(codeBlock, backetIndex, customizationCode);
|
||||
|
||||
str = str.substring(0, index) + codeBlock + str.substring(setTimeoutIndex);
|
||||
return str;
|
||||
},
|
||||
@@ -357,7 +376,7 @@ if (window.BX_EXPOSED.stopTakRendering) {
|
||||
}
|
||||
|
||||
let autoOffCode = '';
|
||||
if (getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) {
|
||||
if (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) {
|
||||
autoOffCode = 'return;';
|
||||
} else if (getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF)) {
|
||||
autoOffCode = `
|
||||
@@ -414,7 +433,7 @@ e.guideUI = null;
|
||||
`;
|
||||
|
||||
// Remove the TAK Edit button when the touch controller is disabled
|
||||
if (getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) {
|
||||
if (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) {
|
||||
newCode += 'e.canShowTakHUD = false;';
|
||||
}
|
||||
|
||||
@@ -534,7 +553,7 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar});
|
||||
return false;
|
||||
}
|
||||
|
||||
const opacity = (getPref<TouchControllerDefaultOpacity>(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) / 100).toFixed(1);
|
||||
const opacity = (getPref(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) / 100).toFixed(1);
|
||||
const newCode = `opacityMultiplier: ${opacity}`;
|
||||
str = str.replace(text, newCode);
|
||||
return str;
|
||||
@@ -771,7 +790,7 @@ true` + text;
|
||||
return false;
|
||||
}
|
||||
|
||||
const PREF_HIDE_SECTIONS = getPref<UiSection[]>(PrefKey.UI_HIDE_SECTIONS);
|
||||
const PREF_HIDE_SECTIONS = getPref(PrefKey.UI_HIDE_SECTIONS);
|
||||
const siglIds: GamePassCloudGallery[] = [];
|
||||
|
||||
const sections: PartialRecord<UiSection, GamePassCloudGallery> = {
|
||||
@@ -962,7 +981,7 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
||||
|
||||
// Find index after {
|
||||
index = str.indexOf('{', index) + 1;
|
||||
const blockFeatures = getPref<BlockFeature[]>(PrefKey.BLOCK_FEATURES);
|
||||
const blockFeatures = getPref(PrefKey.BLOCK_FEATURES);
|
||||
const filters = [];
|
||||
if (blockFeatures.includes(BlockFeature.NOTIFICATIONS_INVITES)) {
|
||||
filters.push('GameInvite', 'PartyInvite');
|
||||
@@ -987,7 +1006,7 @@ ${subsVar} = subs;
|
||||
};
|
||||
|
||||
let PATCH_ORDERS = PatcherUtils.filterPatches([
|
||||
...(AppInterface && getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
|
||||
...(AppInterface && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
|
||||
'enableNativeMkb',
|
||||
'exposeInputSink',
|
||||
'disableAbsoluteMouse',
|
||||
@@ -1019,7 +1038,7 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([
|
||||
'overrideStorageGetSettings',
|
||||
getPref(PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME) && 'patchSetCurrentFocus',
|
||||
|
||||
getPref<UiLayout>(PrefKey.UI_LAYOUT) !== UiLayout.DEFAULT && 'websiteLayout',
|
||||
getPref(PrefKey.UI_LAYOUT) !== UiLayout.DEFAULT && 'websiteLayout',
|
||||
getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole',
|
||||
|
||||
...(STATES.userAgent.capabilities.touch ? [
|
||||
@@ -1055,7 +1074,7 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([
|
||||
] : []),
|
||||
]);
|
||||
|
||||
const hideSections = getPref<UiSection[]>(PrefKey.UI_HIDE_SECTIONS);
|
||||
const hideSections = getPref(PrefKey.UI_HIDE_SECTIONS);
|
||||
let HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
|
||||
hideSections.includes(UiSection.NEWS) && 'ignoreNewsSection',
|
||||
hideSections.includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection',
|
||||
@@ -1086,11 +1105,11 @@ let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
|
||||
getPref(PrefKey.UI_DISABLE_FEEDBACK_DIALOG) && 'skipFeedbackDialog',
|
||||
|
||||
...(STATES.userAgent.capabilities.touch ? [
|
||||
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)) && 'disableTakRenderer',
|
||||
getPref<TouchControllerDefaultOpacity>(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity',
|
||||
(getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) !== TouchControllerMode.OFF && (getPref(PrefKey.MKB_ENABLED) || getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON)) && 'patchBabylonRendererClass',
|
||||
getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'patchShowSensorControls',
|
||||
getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'exposeTouchLayoutManager',
|
||||
(getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF || getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF)) && 'disableTakRenderer',
|
||||
getPref(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity',
|
||||
(getPref(PrefKey.TOUCH_CONTROLLER_MODE) !== TouchControllerMode.OFF && (getPref(PrefKey.MKB_ENABLED) || getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON)) && 'patchBabylonRendererClass',
|
||||
] : []),
|
||||
|
||||
BX_FLAGS.EnableXcloudLogging && 'enableConsoleLogging',
|
||||
@@ -1105,7 +1124,7 @@ let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
|
||||
] : []),
|
||||
|
||||
// Native MKB
|
||||
...(AppInterface && getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
|
||||
...(AppInterface && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
|
||||
'patchMouseAndKeyboardEnabled',
|
||||
'disableNativeRequestPointerLock',
|
||||
] : []),
|
||||
|
@@ -1,99 +0,0 @@
|
||||
if (window.BX_EXPOSED.disableGamepadPolling) {
|
||||
this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(50) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, 50);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentGamepad = ${gamepadVar};
|
||||
|
||||
// Share button on XS controller
|
||||
if (currentGamepad.buttons[17] && currentGamepad.buttons[17].pressed) {
|
||||
window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));
|
||||
}
|
||||
|
||||
const btnHome = currentGamepad.buttons[16];
|
||||
if (btnHome) {
|
||||
if (!this.bxHomeStates) {
|
||||
this.bxHomeStates = {};
|
||||
}
|
||||
|
||||
let intervalMs = 0;
|
||||
let hijack = false;
|
||||
|
||||
if (btnHome.pressed) {
|
||||
hijack = true;
|
||||
intervalMs = 16;
|
||||
this.gamepadIsIdle.set(currentGamepad.index, false);
|
||||
|
||||
if (this.bxHomeStates[currentGamepad.index]) {
|
||||
const lastTimestamp = this.bxHomeStates[currentGamepad.index].timestamp;
|
||||
|
||||
if (currentGamepad.timestamp !== lastTimestamp) {
|
||||
this.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp;
|
||||
|
||||
const handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad);
|
||||
if (handled) {
|
||||
this.bxHomeStates[currentGamepad.index].shortcutPressed += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// First time pressing > save current timestamp
|
||||
window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index);
|
||||
this.bxHomeStates[currentGamepad.index] = {
|
||||
shortcutPressed: 0,
|
||||
timestamp: currentGamepad.timestamp,
|
||||
};
|
||||
}
|
||||
} else if (this.bxHomeStates[currentGamepad.index]) {
|
||||
hijack = true;
|
||||
const info = structuredClone(this.bxHomeStates[currentGamepad.index]);
|
||||
|
||||
// Home button released
|
||||
this.bxHomeStates[currentGamepad.index] = null;
|
||||
|
||||
if (info.shortcutPressed === 0) {
|
||||
const fakeGamepadMappings = [{
|
||||
GamepadIndex: currentGamepad.index,
|
||||
A: 0,
|
||||
B: 0,
|
||||
X: 0,
|
||||
Y: 0,
|
||||
LeftShoulder: 0,
|
||||
RightShoulder: 0,
|
||||
LeftTrigger: 0,
|
||||
RightTrigger: 0,
|
||||
View: 0,
|
||||
Menu: 0,
|
||||
LeftThumb: 0,
|
||||
RightThumb: 0,
|
||||
DPadUp: 0,
|
||||
DPadDown: 0,
|
||||
DPadLeft: 0,
|
||||
DPadRight: 0,
|
||||
Nexus: 1,
|
||||
LeftThumbXAxis: 0,
|
||||
LeftThumbYAxis: 0,
|
||||
RightThumbXAxis: 0,
|
||||
RightThumbYAxis: 0,
|
||||
PhysicalPhysicality: 0,
|
||||
VirtualPhysicality: 0,
|
||||
Dirty: true,
|
||||
Virtual: false,
|
||||
}];
|
||||
|
||||
const isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500;
|
||||
intervalMs = isLongPress ? 500 : 100;
|
||||
|
||||
this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings);
|
||||
} else {
|
||||
intervalMs = window.BX_STREAM_SETTINGS.controllerPollingRate;
|
||||
}
|
||||
}
|
||||
|
||||
if (hijack && intervalMs) {
|
||||
// Listen to next button press
|
||||
this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs);
|
||||
|
||||
// Hijack this button
|
||||
return;
|
||||
}
|
||||
}
|
@@ -1,2 +0,0 @@
|
||||
connectMode: window.BX_REMOTE_PLAY_CONFIG ? "xhome-connect" : "cloud-connect",
|
||||
remotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',
|
@@ -1,7 +0,0 @@
|
||||
const msg = JSON.parse(e);
|
||||
if (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) {
|
||||
try {
|
||||
this.sendKeepAlive();
|
||||
return;
|
||||
} catch (ex) { console.log(ex); }
|
||||
}
|
167
src/modules/patcher/patches/src/controller-customization.ts
Normal file
167
src/modules/patcher/patches/src/controller-customization.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { BxEvent as BxEventNamespace } from "@/utils/bx-event";
|
||||
|
||||
// "currentGamepad" variable in poll-gamepad.js
|
||||
declare const currentGamepad: Gamepad;
|
||||
declare const $xCloudGamepadVar$: XcloudGamepad;
|
||||
declare const BxEvent: typeof BxEventNamespace;
|
||||
|
||||
// Share button on XS controller
|
||||
const shareButtonPressed = currentGamepad.buttons[17]?.pressed;
|
||||
let shareButtonHandled = false;
|
||||
|
||||
const xCloudGamepad: XcloudGamepad = $xCloudGamepadVar$;
|
||||
if (currentGamepad.id in window.BX_STREAM_SETTINGS.controllers) {
|
||||
const controller = window.BX_STREAM_SETTINGS.controllers[currentGamepad.id];
|
||||
if (controller?.customization) {
|
||||
const MIN_RANGE = 0.1;
|
||||
|
||||
const { mapping, ranges } = controller.customization;
|
||||
const pressedButtons: Partial<Record<keyof XcloudGamepad, number>> = {};
|
||||
const releasedButtons: Partial<Record<keyof XcloudGamepad, number>> = {};
|
||||
let isModified = false;
|
||||
|
||||
// Limit left trigger range
|
||||
if (ranges.LeftTrigger) {
|
||||
const [from, to] = ranges.LeftTrigger;
|
||||
xCloudGamepad.LeftTrigger = xCloudGamepad.LeftTrigger > to ? 1 : xCloudGamepad.LeftTrigger;
|
||||
xCloudGamepad.LeftTrigger = xCloudGamepad.LeftTrigger < from ? 0 : xCloudGamepad.LeftTrigger;
|
||||
}
|
||||
|
||||
// Limit right trigger range
|
||||
if (ranges.RightTrigger) {
|
||||
const [from, to] = ranges.RightTrigger;
|
||||
xCloudGamepad.RightTrigger = xCloudGamepad.RightTrigger > to ? 1 : xCloudGamepad.RightTrigger;
|
||||
xCloudGamepad.RightTrigger = xCloudGamepad.RightTrigger < from ? 0 : xCloudGamepad.RightTrigger;
|
||||
}
|
||||
|
||||
// Limit left stick deadzone
|
||||
if (ranges.LeftThumb) {
|
||||
const [from, to] = ranges.LeftThumb;
|
||||
|
||||
const xAxis = xCloudGamepad.LeftThumbXAxis;
|
||||
const yAxis = xCloudGamepad.LeftThumbYAxis;
|
||||
|
||||
const range = Math.abs(Math.sqrt(xAxis * xAxis + yAxis * yAxis));
|
||||
let newRange = range > to ? 1 : range;
|
||||
newRange = newRange < from ? 0 : newRange;
|
||||
|
||||
if (newRange !== range) {
|
||||
xCloudGamepad.LeftThumbXAxis = xAxis * (newRange / range);
|
||||
xCloudGamepad.LeftThumbYAxis = yAxis * (newRange / range);
|
||||
}
|
||||
}
|
||||
|
||||
// Limit right stick deadzone
|
||||
if (ranges.RightThumb) {
|
||||
const [from, to] = ranges.RightThumb;
|
||||
|
||||
const xAxis = xCloudGamepad.RightThumbXAxis;
|
||||
const yAxis = xCloudGamepad.RightThumbYAxis;
|
||||
|
||||
const range = Math.abs(Math.sqrt(xAxis * xAxis + yAxis * yAxis));
|
||||
let newRange = range > to ? 1 : range;
|
||||
newRange = newRange < from ? 0 : newRange;
|
||||
|
||||
if (newRange !== range) {
|
||||
xCloudGamepad.RightThumbXAxis = xAxis * (newRange / range);
|
||||
xCloudGamepad.RightThumbYAxis = yAxis * (newRange / range);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the Share button
|
||||
if (shareButtonPressed && 'Share' in mapping) {
|
||||
const targetButton = mapping['Share'];
|
||||
if (typeof targetButton === 'string') {
|
||||
pressedButtons[targetButton] = 1;
|
||||
}
|
||||
|
||||
// Don't send capturing request
|
||||
shareButtonHandled = true;
|
||||
delete mapping['Share'];
|
||||
}
|
||||
|
||||
// Handle other buttons
|
||||
let key: keyof typeof mapping;
|
||||
for (key in mapping) {
|
||||
const mappedKey = mapping[key];
|
||||
|
||||
if (key === 'LeftStickAxes' || key === 'RightStickAxes') {
|
||||
let sourceX: keyof XcloudGamepad;
|
||||
let sourceY: keyof XcloudGamepad;
|
||||
let targetX: keyof XcloudGamepad;
|
||||
let targetY: keyof XcloudGamepad;
|
||||
|
||||
if (key === 'LeftStickAxes') {
|
||||
sourceX = 'LeftThumbXAxis';
|
||||
sourceY = 'LeftThumbYAxis';
|
||||
targetX = 'RightThumbXAxis';
|
||||
targetY = 'RightThumbYAxis';
|
||||
} else {
|
||||
sourceX = 'RightThumbXAxis';
|
||||
sourceY = 'RightThumbYAxis';
|
||||
targetX = 'LeftThumbXAxis';
|
||||
targetY = 'LeftThumbYAxis';
|
||||
}
|
||||
|
||||
if (typeof mappedKey === 'string') {
|
||||
// Calculate moved range
|
||||
const rangeX = xCloudGamepad[sourceX];
|
||||
const rangeY = xCloudGamepad[sourceY];
|
||||
const movedRange = Math.abs(Math.sqrt(rangeX * rangeX + rangeY * rangeY));
|
||||
const moved = movedRange >= MIN_RANGE;
|
||||
|
||||
// Swap sticks
|
||||
if (moved) {
|
||||
pressedButtons[targetX] = rangeX;
|
||||
pressedButtons[targetY] = rangeY;
|
||||
}
|
||||
}
|
||||
|
||||
// Unbind original stick
|
||||
releasedButtons[sourceX] = 0;
|
||||
releasedButtons[sourceY] = 0;
|
||||
|
||||
isModified = true;
|
||||
} else if (typeof mappedKey === 'string') {
|
||||
let pressed = false;
|
||||
let value = 0;
|
||||
|
||||
if (key === 'LeftTrigger' || key === 'RightTrigger') {
|
||||
// Only set pressed state when pressing pass max range
|
||||
const currentRange = xCloudGamepad[key];
|
||||
if (mappedKey === 'LeftTrigger' || mappedKey === 'RightTrigger') {
|
||||
pressed = currentRange >= MIN_RANGE;
|
||||
value = currentRange;
|
||||
} else {
|
||||
pressed = true;
|
||||
value = currentRange >= 0.9 ? 1 : 0;
|
||||
}
|
||||
} else if (xCloudGamepad[key]) {
|
||||
pressed = true;
|
||||
value = xCloudGamepad[key] as number;
|
||||
}
|
||||
|
||||
if (pressed) {
|
||||
// Only copy button value when it's being pressed
|
||||
pressedButtons[mappedKey] = value;
|
||||
// Unbind original button
|
||||
releasedButtons[key] = 0;
|
||||
|
||||
isModified = true;
|
||||
}
|
||||
} else if (mappedKey === false) {
|
||||
// Disable key
|
||||
pressedButtons[key] = 0;
|
||||
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
isModified && Object.assign(xCloudGamepad, releasedButtons, pressedButtons);
|
||||
}
|
||||
}
|
||||
|
||||
// Capture screenshot when the Share button is pressed
|
||||
if (shareButtonPressed && !shareButtonHandled) {
|
||||
window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));
|
||||
}
|
17
src/modules/patcher/patches/expose-stream-session.js → src/modules/patcher/patches/src/expose-stream-session.ts
Executable file → Normal file
17
src/modules/patcher/patches/expose-stream-session.js → src/modules/patcher/patches/src/expose-stream-session.ts
Executable file → Normal file
@@ -1,7 +1,15 @@
|
||||
window.BX_EXPOSED.streamSession = this;
|
||||
import type { MicrophoneState } from "@/modules/shortcuts/microphone-shortcut";
|
||||
import { BxEvent as BxEventNamespace } from "@/utils/bx-event";
|
||||
|
||||
const orgSetMicrophoneState = this.setMicrophoneState.bind(this);
|
||||
this.setMicrophoneState = state => {
|
||||
declare const $this$: any;
|
||||
declare const BxEvent: typeof BxEventNamespace;
|
||||
|
||||
const self = $this$;
|
||||
window.BX_EXPOSED.streamSession = self;
|
||||
|
||||
// Patch setMicrophoneState()
|
||||
const orgSetMicrophoneState = self.setMicrophoneState.bind(self);
|
||||
self.setMicrophoneState = (state: MicrophoneState) => {
|
||||
orgSetMicrophoneState(state);
|
||||
window.BxEventBus.Stream.emit('microphone.state.changed', { state });
|
||||
};
|
||||
@@ -9,7 +17,7 @@ this.setMicrophoneState = state => {
|
||||
window.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY));
|
||||
|
||||
// Patch updateDimensions() to make native touch work correctly with WebGL2
|
||||
let updateDimensionsStr = this.updateDimensions.toString();
|
||||
let updateDimensionsStr = self.updateDimensions.toString();
|
||||
|
||||
if (updateDimensionsStr.startsWith('function ')) {
|
||||
updateDimensionsStr = updateDimensionsStr.substring(9);
|
||||
@@ -19,7 +27,6 @@ if (updateDimensionsStr.startsWith('function ')) {
|
||||
const renderTargetVar = updateDimensionsStr.match(/if\((\w+)\){/)[1];
|
||||
|
||||
updateDimensionsStr = updateDimensionsStr.replaceAll(renderTargetVar + '.scroll', 'scroll');
|
||||
|
||||
updateDimensionsStr = updateDimensionsStr.replace(`if(${renderTargetVar}){`, `
|
||||
if (${renderTargetVar}) {
|
||||
const scrollWidth = ${renderTargetVar}.dataset.width ? parseInt(${renderTargetVar}.dataset.width) : ${renderTargetVar}.scrollWidth;
|
27
src/modules/patcher/patches/local-co-op-enable.js → src/modules/patcher/patches/src/local-co-op-enable.ts
Executable file → Normal file
27
src/modules/patcher/patches/local-co-op-enable.js → src/modules/patcher/patches/src/local-co-op-enable.ts
Executable file → Normal file
@@ -1,9 +1,14 @@
|
||||
import { BxLogger as OrgBxLogger } from "@/utils/bx-logger";
|
||||
|
||||
declare const BxLogger: typeof OrgBxLogger;
|
||||
declare const $this$: any;
|
||||
|
||||
// Save the original onGamepadChanged() and onGamepadInput()
|
||||
this.orgOnGamepadChanged = this.onGamepadChanged;
|
||||
this.orgOnGamepadInput = this.onGamepadInput;
|
||||
$this$.orgOnGamepadChanged = $this$.onGamepadChanged;
|
||||
$this$.orgOnGamepadInput = $this$.onGamepadInput;
|
||||
|
||||
let match;
|
||||
let onGamepadChangedStr = this.onGamepadChanged.toString();
|
||||
let onGamepadChangedStr = $this$.onGamepadChanged.toString();
|
||||
|
||||
// Fix problem with Safari
|
||||
if (onGamepadChangedStr.startsWith('function ')) {
|
||||
@@ -11,9 +16,9 @@ if (onGamepadChangedStr.startsWith('function ')) {
|
||||
}
|
||||
|
||||
onGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]');
|
||||
eval(`this.patchedOnGamepadChanged = function ${onGamepadChangedStr}`);
|
||||
eval(`$this$.patchedOnGamepadChanged = function ${onGamepadChangedStr}`);
|
||||
|
||||
let onGamepadInputStr = this.onGamepadInput.toString();
|
||||
let onGamepadInputStr = $this$.onGamepadInput.toString();
|
||||
// Fix problem with Safari
|
||||
if (onGamepadInputStr.startsWith('function ')) {
|
||||
onGamepadInputStr = onGamepadInputStr.substring(9);
|
||||
@@ -22,19 +27,19 @@ if (onGamepadInputStr.startsWith('function ')) {
|
||||
match = onGamepadInputStr.match(/(\w+\.GamepadIndex)/);
|
||||
if (match) {
|
||||
const gamepadIndexVar = match[0];
|
||||
onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', `this.gamepadStates.get(${gamepadIndexVar},`);
|
||||
eval(`this.patchedOnGamepadInput = function ${onGamepadInputStr}`);
|
||||
onGamepadInputStr = onGamepadInputStr.replace('$this$.gamepadStates.get(', `$this$.gamepadStates.get(${gamepadIndexVar},`);
|
||||
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 => {
|
||||
$this$.toggleLocalCoOp = (enable: boolean) => {
|
||||
BxLogger.info('toggleLocalCoOp', enable ? 'Enabled' : 'Disabled');
|
||||
|
||||
this.onGamepadChanged = enable ? this.patchedOnGamepadChanged : this.orgOnGamepadChanged;
|
||||
this.onGamepadInput = enable ? this.patchedOnGamepadInput : this.orgOnGamepadInput;
|
||||
$this$.onGamepadChanged = enable ? $this$.patchedOnGamepadChanged : $this$.orgOnGamepadChanged;
|
||||
$this$.onGamepadInput = enable ? $this$.patchedOnGamepadInput : $this$.orgOnGamepadInput;
|
||||
|
||||
// Reconnect all gamepads
|
||||
const gamepads = window.navigator.getGamepads();
|
||||
@@ -54,4 +59,4 @@ this.toggleLocalCoOp = enable => {
|
||||
};
|
||||
|
||||
// Expose this method
|
||||
window.BX_EXPOSED.toggleLocalCoOp = this.toggleLocalCoOp.bind(this);
|
||||
window.BX_EXPOSED.toggleLocalCoOp = $this$.toggleLocalCoOp.bind(this);
|
98
src/modules/patcher/patches/src/poll-gamepad.ts
Normal file
98
src/modules/patcher/patches/src/poll-gamepad.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
type GamepadManager = {
|
||||
pollGamepadssetTimeoutTimerID: number;
|
||||
intervalWorker: any;
|
||||
pollGamepads(pollGamepads: any, arg1: number): any;
|
||||
gamepadIsIdle: any;
|
||||
inputSink: any;
|
||||
inputConfiguration: any;
|
||||
|
||||
bxHomeStates: any;
|
||||
};
|
||||
|
||||
declare const $gamepadVar$: Gamepad;
|
||||
declare const $this$: GamepadManager;
|
||||
|
||||
const self = $this$;
|
||||
if (window.BX_EXPOSED.disableGamepadPolling) {
|
||||
self.inputConfiguration.useIntervalWorkerThreadForInput && self.intervalWorker ? self.intervalWorker.scheduleTimer(50) : self.pollGamepadssetTimeoutTimerID = window.setTimeout(self.pollGamepads, 50);
|
||||
// @ts-ignore
|
||||
return;
|
||||
}
|
||||
|
||||
const currentGamepad = $gamepadVar$;
|
||||
|
||||
const btnHome = currentGamepad.buttons[16];
|
||||
// Controller shortcuts
|
||||
if (btnHome) {
|
||||
if (!self.bxHomeStates) {
|
||||
self.bxHomeStates = {};
|
||||
}
|
||||
|
||||
let intervalMs = 0;
|
||||
let hijack = false;
|
||||
|
||||
if (btnHome.pressed) {
|
||||
hijack = true;
|
||||
intervalMs = 16;
|
||||
self.gamepadIsIdle.set(currentGamepad.index, false);
|
||||
|
||||
if (self.bxHomeStates[currentGamepad.index]) {
|
||||
const lastTimestamp = self.bxHomeStates[currentGamepad.index].timestamp;
|
||||
|
||||
if (currentGamepad.timestamp !== lastTimestamp) {
|
||||
self.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp;
|
||||
|
||||
const handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad);
|
||||
if (handled) {
|
||||
self.bxHomeStates[currentGamepad.index].shortcutPressed += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// First time pressing > save current timestamp
|
||||
window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index);
|
||||
self.bxHomeStates[currentGamepad.index] = {
|
||||
shortcutPressed: 0,
|
||||
timestamp: currentGamepad.timestamp,
|
||||
};
|
||||
}
|
||||
} else if (self.bxHomeStates[currentGamepad.index]) {
|
||||
hijack = true;
|
||||
const info = structuredClone(self.bxHomeStates[currentGamepad.index]);
|
||||
|
||||
// Home button released
|
||||
self.bxHomeStates[currentGamepad.index] = null;
|
||||
|
||||
if (info.shortcutPressed === 0) {
|
||||
const fakeGamepadMappings: XcloudGamepad[] = [{
|
||||
GamepadIndex: currentGamepad.index,
|
||||
A: 0, B: 0, X: 0, Y: 0,
|
||||
LeftShoulder: 0, RightShoulder: 0,
|
||||
LeftTrigger: 0, RightTrigger: 0,
|
||||
View: 0, Menu: 0,
|
||||
LeftThumb: 0, RightThumb: 0,
|
||||
DPadUp: 0, DPadDown: 0, DPadLeft: 0, DPadRight: 0,
|
||||
Nexus: 1,
|
||||
LeftThumbXAxis: 0, LeftThumbYAxis: 0,
|
||||
RightThumbXAxis: 0, RightThumbYAxis: 0,
|
||||
PhysicalPhysicality: 0, VirtualPhysicality: 0,
|
||||
Dirty: true, Virtual: false,
|
||||
}];
|
||||
|
||||
const isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500;
|
||||
intervalMs = isLongPress ? 500 : 100;
|
||||
|
||||
self.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings);
|
||||
} else {
|
||||
intervalMs = window.BX_STREAM_SETTINGS.controllerPollingRate;
|
||||
}
|
||||
}
|
||||
|
||||
if (hijack && intervalMs) {
|
||||
// Listen to next button press
|
||||
self.inputConfiguration.useIntervalWorkerThreadForInput && self.intervalWorker ? self.intervalWorker.scheduleTimer(intervalMs) : self.pollGamepadssetTimeoutTimerID = setTimeout(self.pollGamepads, intervalMs);
|
||||
|
||||
// Hijack this button
|
||||
// @ts-ignore
|
||||
return;
|
||||
}
|
||||
}
|
11
src/modules/patcher/patches/src/remote-play-keep-alive.ts
Normal file
11
src/modules/patcher/patches/src/remote-play-keep-alive.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
declare const $this$: any;
|
||||
declare const e: string;
|
||||
|
||||
try {
|
||||
const msg = JSON.parse(e);
|
||||
if (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) {
|
||||
$this$.sendKeepAlive();
|
||||
// @ts-ignore
|
||||
return;
|
||||
}
|
||||
} catch (ex) { console.log(ex); }
|
26
src/modules/patcher/patches/src/vibration-adjust.ts
Normal file
26
src/modules/patcher/patches/src/vibration-adjust.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
declare const e: {
|
||||
gamepad: Gamepad;
|
||||
repeat: number;
|
||||
leftMotorPercent: number;
|
||||
rightMotorPercent: number;
|
||||
leftTriggerMotorPercent: number;
|
||||
rightTriggerMotorPercent: number;
|
||||
};
|
||||
|
||||
if (e?.gamepad?.connected) {
|
||||
const gamepadSettings = window.BX_STREAM_SETTINGS.controllers[e.gamepad.id];
|
||||
if (gamepadSettings?.customization) {
|
||||
const intensity = gamepadSettings.customization.vibrationIntensity;
|
||||
|
||||
if (intensity <= 0) {
|
||||
e.repeat = 0;
|
||||
// @ts-ignore
|
||||
return;
|
||||
} else if (intensity < 1) {
|
||||
e.leftMotorPercent *= intensity;
|
||||
e.rightMotorPercent *= intensity;
|
||||
e.leftTriggerMotorPercent *= intensity;
|
||||
e.rightTriggerMotorPercent *= intensity;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
const gamepad = e.gamepad;
|
||||
if (gamepad?.connected) {
|
||||
const gamepadSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id];
|
||||
if (gamepadSettings) {
|
||||
const intensity = gamepadSettings.vibrationIntensity;
|
||||
|
||||
if (intensity === 0) {
|
||||
return void(e.repeat = 0);
|
||||
} else if (intensity < 1) {
|
||||
e.leftMotorPercent *= intensity;
|
||||
e.rightMotorPercent *= intensity;
|
||||
e.leftTriggerMotorPercent *= intensity;
|
||||
e.rightTriggerMotorPercent *= intensity;
|
||||
}
|
||||
}
|
||||
}
|
@@ -17,7 +17,7 @@ export class SoundShortcut {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const currentValue = getPref<AudioVolume>(PrefKey.AUDIO_VOLUME);
|
||||
const currentValue = getPref(PrefKey.AUDIO_VOLUME);
|
||||
let nearestValue: number;
|
||||
|
||||
if (amount > 0) { // Increase
|
||||
@@ -49,7 +49,7 @@ export class SoundShortcut {
|
||||
static muteUnmute() {
|
||||
if (getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED) && STATES.currentStream.audioGainNode) {
|
||||
const gainValue = STATES.currentStream.audioGainNode.gain.value;
|
||||
const settingValue = getPref<AudioVolume>(PrefKey.AUDIO_VOLUME);
|
||||
const settingValue = getPref(PrefKey.AUDIO_VOLUME);
|
||||
|
||||
let targetValue: number;
|
||||
if (settingValue === 0) { // settingValue is 0 => set to 100
|
||||
|
@@ -7,7 +7,7 @@ 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, VideoPosition, VideoRatio } from "@/enums/pref-values";
|
||||
import { StreamPlayerType, StreamVideoProcessing, VideoPosition } from "@/enums/pref-values";
|
||||
|
||||
export type StreamPlayerOptions = Partial<{
|
||||
processing: string,
|
||||
@@ -44,7 +44,7 @@ export class StreamPlayer {
|
||||
|
||||
const $fragment = document.createDocumentFragment();
|
||||
|
||||
this.$videoCss = CE<HTMLStyleElement>('style', { id: 'bx-video-css' });
|
||||
this.$videoCss = CE('style', { id: 'bx-video-css' });
|
||||
$fragment.appendChild(this.$videoCss);
|
||||
|
||||
// Setup SVG filters
|
||||
@@ -60,7 +60,7 @@ export class StreamPlayer {
|
||||
id: 'bx-filter-usm-matrix',
|
||||
order: '3',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
})),
|
||||
}) as unknown as SVGFEConvolveMatrixElement),
|
||||
),
|
||||
);
|
||||
$fragment.appendChild($svg);
|
||||
@@ -98,7 +98,7 @@ export class StreamPlayer {
|
||||
}
|
||||
|
||||
private resizePlayer() {
|
||||
const PREF_RATIO = getPref<VideoRatio>(PrefKey.VIDEO_RATIO);
|
||||
const PREF_RATIO = getPref(PrefKey.VIDEO_RATIO);
|
||||
const $video = this.$video;
|
||||
const isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport;
|
||||
|
||||
@@ -142,7 +142,7 @@ export class StreamPlayer {
|
||||
|
||||
// Set position
|
||||
const $parent = $video.parentElement!;
|
||||
const position = getPref<VideoPosition>(PrefKey.VIDEO_POSITION);
|
||||
const position = getPref(PrefKey.VIDEO_POSITION);
|
||||
$parent.style.removeProperty('padding-top');
|
||||
|
||||
$parent.dataset.position = position;
|
||||
|
@@ -7,7 +7,7 @@ import { StreamVideoProcessing, StreamPlayerType } from "@/enums/pref-values";
|
||||
import { escapeCssSelector } from "@/utils/html";
|
||||
|
||||
export function onChangeVideoPlayerType() {
|
||||
const playerType = getPref<StreamPlayerType>(PrefKey.VIDEO_PLAYER_TYPE);
|
||||
const playerType = getPref(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;
|
||||
|
@@ -192,8 +192,8 @@ export class StreamStats {
|
||||
}
|
||||
|
||||
refreshStyles() {
|
||||
const PREF_ITEMS = getPref<StreamStat[]>(PrefKey.STATS_ITEMS);
|
||||
const PREF_OPACITY_BG = getPref<number>(PrefKey.STATS_OPACITY_BACKGROUND);
|
||||
const PREF_ITEMS = getPref(PrefKey.STATS_ITEMS);
|
||||
const PREF_OPACITY_BG = getPref(PrefKey.STATS_OPACITY_BACKGROUND);
|
||||
|
||||
const $container = this.$container;
|
||||
$container.dataset.stats = '[' + PREF_ITEMS.join('][') + ']';
|
||||
|
@@ -289,8 +289,8 @@ export class TouchController {
|
||||
|
||||
TouchController.#$style = $style;
|
||||
|
||||
const PREF_STYLE_STANDARD = getPref<TouchControllerStyleStandard>(PrefKey.TOUCH_CONTROLLER_STYLE_STANDARD);
|
||||
const PREF_STYLE_CUSTOM = getPref<TouchControllerStyleCustom>(PrefKey.TOUCH_CONTROLLER_STYLE_CUSTOM);
|
||||
const PREF_STYLE_STANDARD = getPref(PrefKey.TOUCH_CONTROLLER_STYLE_STANDARD);
|
||||
const PREF_STYLE_CUSTOM = getPref(PrefKey.TOUCH_CONTROLLER_STYLE_CUSTOM);
|
||||
|
||||
BxEventBus.Stream.on('dataChannelCreated', payload => {
|
||||
const { dataChannel } = payload;
|
||||
|
@@ -1,12 +1,10 @@
|
||||
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 { BxEventBus } from "@/utils/bx-event-bus";
|
||||
import { BxLogger } from "@/utils/bx-logger";
|
||||
import { CE, isElementVisible } from "@/utils/html";
|
||||
import { calculateSelectBoxes, CE, isElementVisible } from "@/utils/html";
|
||||
import { setNearby } from "@/utils/navigation-utils";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
export enum NavigationDirection {
|
||||
UP = 1,
|
||||
@@ -21,10 +19,10 @@ export type NavigationNearbyElements = Partial<{
|
||||
|
||||
focus: NavigationElement | (() => boolean),
|
||||
loop: ((direction: NavigationDirection) => boolean),
|
||||
[NavigationDirection.UP]: NavigationElement | (() => void) | 'previous' | 'next',
|
||||
[NavigationDirection.DOWN]: NavigationElement | (() => void) | 'previous' | 'next',
|
||||
[NavigationDirection.LEFT]: NavigationElement | (() => void) | 'previous' | 'next',
|
||||
[NavigationDirection.RIGHT]: NavigationElement | (() => void) | 'previous' | 'next',
|
||||
[NavigationDirection.UP]: NavigationElement,
|
||||
[NavigationDirection.DOWN]: NavigationElement,
|
||||
[NavigationDirection.LEFT]: NavigationElement,
|
||||
[NavigationDirection.RIGHT]: NavigationElement,
|
||||
}>;
|
||||
|
||||
export interface NavigationElement extends HTMLElement {
|
||||
@@ -107,16 +105,18 @@ export class NavigationDialogManager {
|
||||
|
||||
private static readonly GAMEPAD_POLLING_INTERVAL = 50;
|
||||
private static readonly GAMEPAD_KEYS = [
|
||||
GamepadKey.UP,
|
||||
GamepadKey.DOWN,
|
||||
GamepadKey.LEFT,
|
||||
GamepadKey.RIGHT,
|
||||
GamepadKey.A,
|
||||
GamepadKey.B,
|
||||
GamepadKey.LB,
|
||||
GamepadKey.RB,
|
||||
GamepadKey.LT,
|
||||
GamepadKey.RT,
|
||||
GamepadKey.A, GamepadKey.B,
|
||||
GamepadKey.X, GamepadKey.Y,
|
||||
|
||||
GamepadKey.UP, GamepadKey.RIGHT,
|
||||
GamepadKey.DOWN, GamepadKey.LEFT,
|
||||
|
||||
GamepadKey.LB, GamepadKey.RB,
|
||||
GamepadKey.LT, GamepadKey.RT,
|
||||
|
||||
GamepadKey.L3, GamepadKey.R3,
|
||||
|
||||
GamepadKey.SELECT, GamepadKey.START,
|
||||
];
|
||||
|
||||
private static readonly GAMEPAD_DIRECTION_MAP = {
|
||||
@@ -172,61 +172,21 @@ export class NavigationDialogManager {
|
||||
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, e => this.hide());
|
||||
|
||||
// Calculate minimum width of controller-friendly <select> elements
|
||||
if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
|
||||
const observer = new MutationObserver(mutationList => {
|
||||
if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get dialog
|
||||
const $dialog = mutationList[0].addedNodes[0];
|
||||
if (!$dialog || !($dialog instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find un-calculated <select> elements
|
||||
this.calculateSelectBoxes($dialog);
|
||||
});
|
||||
observer.observe(this.$container, { childList: true });
|
||||
}
|
||||
}
|
||||
|
||||
calculateSelectBoxes($root: HTMLElement) {
|
||||
const selects = Array.from($root.querySelectorAll('.bx-select:not([data-calculated]) select'));
|
||||
|
||||
for (const $select of selects) {
|
||||
const $parent = $select.parentElement! as HTMLElement;
|
||||
|
||||
// Don't apply to select.bx-full-width elements
|
||||
if ($parent.classList.contains('bx-full-width')) {
|
||||
$parent.dataset.calculated = 'true';
|
||||
const observer = new MutationObserver(mutationList => {
|
||||
if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = $select.getBoundingClientRect();
|
||||
|
||||
let $label: HTMLElement;
|
||||
let width = Math.ceil(rect.width);
|
||||
if (!width) {
|
||||
// Get dialog
|
||||
const $dialog = mutationList[0].addedNodes[0];
|
||||
if (!$dialog || !($dialog instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (($select as HTMLSelectElement).multiple) {
|
||||
$label = $parent.querySelector<HTMLElement>('.bx-select-value')!;
|
||||
width += 20; // Add checkbox's width
|
||||
} else {
|
||||
$label = $parent.querySelector<HTMLElement>('div')!;
|
||||
}
|
||||
|
||||
// Reduce width if it has <optgroup>
|
||||
if ($select.querySelector('optgroup')) {
|
||||
width -= 15;
|
||||
}
|
||||
|
||||
// Set min-width
|
||||
$label.style.minWidth = width + 'px';
|
||||
$parent.dataset.calculated = 'true';
|
||||
};
|
||||
// Find un-calculated <select> elements
|
||||
calculateSelectBoxes($dialog);
|
||||
});
|
||||
observer.observe(this.$container, { childList: true });
|
||||
}
|
||||
|
||||
private updateActiveInput(input: 'keyboard' | 'gamepad' | 'mouse') {
|
||||
@@ -369,7 +329,7 @@ export class NavigationDialogManager {
|
||||
}
|
||||
|
||||
this.clearGamepadHoldingInterval();
|
||||
}, 200);
|
||||
}, 100);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -579,6 +539,16 @@ export class NavigationDialogManager {
|
||||
const nearby = ($target as NavigationElement).nearby || {};
|
||||
const orientation = this.getOrientation($target)!;
|
||||
|
||||
if (nearby[NavigationDirection.UP] && direction === NavigationDirection.UP) {
|
||||
return nearby[NavigationDirection.UP];
|
||||
} else if (nearby[NavigationDirection.DOWN] && direction === NavigationDirection.DOWN) {
|
||||
return nearby[NavigationDirection.DOWN];
|
||||
} else if (nearby[NavigationDirection.LEFT] && direction === NavigationDirection.LEFT) {
|
||||
return nearby[NavigationDirection.LEFT];
|
||||
} else if (nearby[NavigationDirection.RIGHT] && direction === NavigationDirection.RIGHT) {
|
||||
return nearby[NavigationDirection.RIGHT];
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
let siblingProperty = (NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation])[direction];
|
||||
if (siblingProperty) {
|
||||
|
@@ -5,6 +5,7 @@ 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";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
|
||||
export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends NavigationDialog {
|
||||
$container!: HTMLElement;
|
||||
@@ -12,7 +13,8 @@ export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends N
|
||||
private title: string;
|
||||
protected presetsDb: BasePresetsTable<T>;
|
||||
protected allPresets!: AllPresets<T>;
|
||||
protected currentPresetId: number = 0;
|
||||
protected currentPresetId: number | null = null;
|
||||
protected activatedPresetId: number | null = null;
|
||||
|
||||
private $presets!: HTMLSelectElement;
|
||||
private $header!: HTMLElement;
|
||||
@@ -34,7 +36,7 @@ export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends N
|
||||
protected abstract switchPreset(id: number): void;
|
||||
|
||||
protected updateButtonStates() {
|
||||
const isDefaultPreset = this.currentPresetId <= 0;
|
||||
const isDefaultPreset = this.currentPresetId === null || this.currentPresetId <= 0;
|
||||
this.$btnRename.disabled = isDefaultPreset;
|
||||
this.$btnDelete.disabled = isDefaultPreset;
|
||||
this.$defaultNote.classList.toggle('bx-gone', !isDefaultPreset);
|
||||
@@ -42,7 +44,11 @@ export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends N
|
||||
|
||||
private async renderPresetsList() {
|
||||
this.allPresets = await this.presetsDb.getPresets();
|
||||
renderPresetsList<T>(this.$presets, this.allPresets, this.currentPresetId, { selectedIndicator: true });
|
||||
if (this.currentPresetId === null) {
|
||||
this.currentPresetId = this.allPresets.default[0];
|
||||
}
|
||||
|
||||
renderPresetsList<T>(this.$presets, this.allPresets, this.activatedPresetId, { selectedIndicator: true });
|
||||
}
|
||||
|
||||
private promptNewName(action: string,value='') {
|
||||
@@ -59,10 +65,12 @@ export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends N
|
||||
};
|
||||
|
||||
private async renderDialog() {
|
||||
this.$presets = CE<HTMLSelectElement>('select', { tabindex: -1 });
|
||||
this.$presets = CE('select', {
|
||||
class: 'bx-full-width',
|
||||
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));
|
||||
});
|
||||
@@ -82,7 +90,7 @@ export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends N
|
||||
icon: BxIcon.CURSOR_TEXT,
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: async () => {
|
||||
const preset = this.allPresets.data[this.currentPresetId];
|
||||
const preset = this.allPresets.data[this.currentPresetId!];
|
||||
|
||||
const newName = this.promptNewName(t('rename'), preset.name);
|
||||
if (!newName) {
|
||||
@@ -107,8 +115,8 @@ export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends N
|
||||
return;
|
||||
}
|
||||
|
||||
await this.presetsDb.deletePreset(this.currentPresetId);
|
||||
delete this.allPresets.data[this.currentPresetId];
|
||||
await this.presetsDb.deletePreset(this.currentPresetId!);
|
||||
delete this.allPresets.data[this.currentPresetId!];
|
||||
this.currentPresetId = parseInt(Object.keys(this.allPresets.data)[0]);
|
||||
|
||||
await this.refresh();
|
||||
@@ -140,7 +148,7 @@ export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends N
|
||||
title: t('copy'),
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY,
|
||||
onClick: async (e) => {
|
||||
const preset = this.allPresets.data[this.currentPresetId];
|
||||
const preset = this.allPresets.data[this.currentPresetId!];
|
||||
|
||||
const newName = this.promptNewName(t('copy'), `${preset.name} (2)`);
|
||||
if (!newName) {
|
||||
@@ -176,12 +184,26 @@ export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends N
|
||||
|
||||
async refresh() {
|
||||
await this.renderPresetsList();
|
||||
this.switchPreset(this.currentPresetId);
|
||||
this.$presets.value = this.currentPresetId!.toString();
|
||||
BxEvent.dispatch(this.$presets, 'input', { manualTrigger: true });
|
||||
}
|
||||
|
||||
async onBeforeMount(configs:{ id?: number }={}) {
|
||||
if (configs?.id) {
|
||||
this.currentPresetId = configs.id;
|
||||
await this.renderPresetsList();
|
||||
|
||||
let valid = false;
|
||||
if (typeof configs?.id === 'number') {
|
||||
if (configs.id in this.allPresets.data) {
|
||||
this.currentPresetId = configs.id;
|
||||
this.activatedPresetId = configs.id;
|
||||
valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid selected ID => get default ID;
|
||||
if (!valid) {
|
||||
this.currentPresetId = this.allPresets.default[0];
|
||||
this.activatedPresetId = null;
|
||||
}
|
||||
|
||||
// Select first preset
|
||||
|
@@ -0,0 +1,371 @@
|
||||
import type { ControllerCustomizationPresetData, ControllerCustomizationPresetRecord } from "@/types/presets";
|
||||
import { BaseProfileManagerDialog } from "./base-profile-manager-dialog";
|
||||
import { ControllerCustomizationsTable } from "@/utils/local-db/controller-customizations-table";
|
||||
import { t } from "@/utils/translation";
|
||||
import { GamepadKey, GamepadKeyName } from "@/enums/gamepad";
|
||||
import { ButtonStyle, CE, createButton, createSettingRow } from "@/utils/html";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { deepClone } from "@/utils/global";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { BxDualNumberStepper } from "@/web-components/bx-dual-number-stepper";
|
||||
import { NavigationDirection, type NavigationElement } from "../navigation-dialog";
|
||||
import { setNearby } from "@/utils/navigation-utils";
|
||||
import type { DualNumberStepperParams } from "@/types/setting-definition";
|
||||
import { BxNumberStepper } from "@/web-components/bx-number-stepper";
|
||||
|
||||
export class ControllerCustomizationsManagerDialog extends BaseProfileManagerDialog<ControllerCustomizationPresetRecord> {
|
||||
private static instance: ControllerCustomizationsManagerDialog;
|
||||
public static getInstance = () => ControllerCustomizationsManagerDialog.instance ?? (ControllerCustomizationsManagerDialog.instance = new ControllerCustomizationsManagerDialog(t('controller-customization')));
|
||||
|
||||
declare protected $content: HTMLElement;
|
||||
private $vibrationIntensity!: BxNumberStepper;
|
||||
private $leftTriggerRange!: BxDualNumberStepper;
|
||||
private $rightTriggerRange!: BxDualNumberStepper;
|
||||
private $leftStickDeadzone!: BxDualNumberStepper;
|
||||
private $rightStickDeadzone!: BxDualNumberStepper;
|
||||
private $btnDetect!: HTMLButtonElement;
|
||||
|
||||
protected BLANK_PRESET_DATA = {
|
||||
mapping: {},
|
||||
settings: {
|
||||
leftTriggerRange: [0, 100],
|
||||
rightTriggerRange: [0, 100],
|
||||
leftStickDeadzone: [0, 100],
|
||||
rightStickDeadzone: [0, 100],
|
||||
|
||||
vibrationIntensity: 100,
|
||||
},
|
||||
} satisfies ControllerCustomizationPresetData;
|
||||
|
||||
private selectsMap: Partial<Record<GamepadKey, HTMLSelectElement>> = {};
|
||||
private selectsOrder: GamepadKey[] = [];
|
||||
|
||||
private isDetectingButton: boolean = false;
|
||||
private detectIntervalId: number | null = null;
|
||||
|
||||
private readonly BUTTONS_ORDER = [
|
||||
GamepadKey.A, GamepadKey.B,
|
||||
GamepadKey.X, GamepadKey.Y,
|
||||
|
||||
GamepadKey.UP, GamepadKey.RIGHT,
|
||||
GamepadKey.DOWN, GamepadKey.LEFT,
|
||||
|
||||
GamepadKey.LB, GamepadKey.RB,
|
||||
GamepadKey.LT, GamepadKey.RT,
|
||||
|
||||
GamepadKey.SELECT, GamepadKey.START,
|
||||
|
||||
GamepadKey.L3, GamepadKey.R3,
|
||||
GamepadKey.LS, GamepadKey.RS,
|
||||
|
||||
GamepadKey.SHARE,
|
||||
];
|
||||
|
||||
constructor(title: string) {
|
||||
super(title, ControllerCustomizationsTable.getInstance());
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render() {
|
||||
const isControllerFriendly = getPref(PrefKey.UI_CONTROLLER_FRIENDLY);
|
||||
const $rows = CE('div', { class: 'bx-buttons-grid' });
|
||||
|
||||
const $baseSelect = CE('select', { class: 'bx-full-width' },
|
||||
CE('option', { value: '' }, '---'),
|
||||
CE('option', { value: 'false', _dataset: { label: '🚫' } }, isControllerFriendly ? '🚫' : t('off')),
|
||||
);
|
||||
const $baseButtonSelect = $baseSelect.cloneNode(true);
|
||||
const $baseStickSelect = $baseSelect.cloneNode(true);
|
||||
|
||||
const onButtonChanged = (e: Event) => {
|
||||
// Update preset
|
||||
if (!(e as any).ignoreOnChange) {
|
||||
this.updatePreset();
|
||||
}
|
||||
};
|
||||
|
||||
const boundUpdatePreset = this.updatePreset.bind(this);
|
||||
|
||||
for (const gamepadKey of this.BUTTONS_ORDER) {
|
||||
if (gamepadKey === GamepadKey.SHARE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = GamepadKeyName[gamepadKey][isControllerFriendly ? 1 : 0];
|
||||
const $target = (gamepadKey === GamepadKey.LS || gamepadKey === GamepadKey.RS) ? $baseStickSelect : $baseButtonSelect;
|
||||
$target.appendChild(CE('option', {
|
||||
value: gamepadKey,
|
||||
_dataset: { label: GamepadKeyName[gamepadKey][1] },
|
||||
}, name));
|
||||
}
|
||||
|
||||
for (const gamepadKey of this.BUTTONS_ORDER) {
|
||||
const [buttonName, buttonPrompt] = GamepadKeyName[gamepadKey];
|
||||
const $sourceSelect = (gamepadKey === GamepadKey.LS || gamepadKey === GamepadKey.RS) ? $baseStickSelect : $baseButtonSelect;
|
||||
|
||||
// Remove current button from selection
|
||||
const $clonedSelect = $sourceSelect.cloneNode(true) as HTMLSelectElement;
|
||||
$clonedSelect.querySelector(`option[value="${gamepadKey}"]`)?.remove();
|
||||
|
||||
const $select = BxSelectElement.create($clonedSelect);
|
||||
$select.dataset.index = gamepadKey.toString();
|
||||
$select.addEventListener('input', onButtonChanged);
|
||||
|
||||
this.selectsMap[gamepadKey] = $select;
|
||||
this.selectsOrder.push(gamepadKey);
|
||||
|
||||
const $row = CE('div', {
|
||||
class: 'bx-controller-key-row',
|
||||
_nearby: { orientation: 'horizontal' },
|
||||
},
|
||||
CE('label', { title: buttonName }, buttonPrompt),
|
||||
$select,
|
||||
);
|
||||
|
||||
$rows.append($row);
|
||||
}
|
||||
|
||||
// Map nearby elenemts for controller-friendly UI
|
||||
if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
|
||||
for (let i = 0; i < this.selectsOrder.length; i++) {
|
||||
const $select = this.selectsMap[this.selectsOrder[i] as unknown as GamepadKey] as NavigationElement;
|
||||
const directions = {
|
||||
[NavigationDirection.UP]: i - 2,
|
||||
[NavigationDirection.DOWN]: i + 2,
|
||||
[NavigationDirection.LEFT]: i - 1,
|
||||
[NavigationDirection.RIGHT]: i + 1,
|
||||
};
|
||||
|
||||
for (const dir in directions) {
|
||||
const idx = directions[dir as unknown as NavigationDirection];
|
||||
if (typeof this.selectsOrder[idx] === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $targetSelect = this.selectsMap[this.selectsOrder[idx] as unknown as GamepadKey];
|
||||
setNearby($select, {
|
||||
[dir]: $targetSelect,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const params: DualNumberStepperParams = {
|
||||
min: 0,
|
||||
minDiff: 1,
|
||||
max: 100,
|
||||
|
||||
steps: 1,
|
||||
};
|
||||
this.$content = CE('div', { class: 'bx-controller-customizations-container' },
|
||||
// Detect button
|
||||
this.$btnDetect = createButton({
|
||||
label: t('detect-controller-button'),
|
||||
classes: ['bx-btn-detect'],
|
||||
style: ButtonStyle.NORMAL_CASE | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_WIDTH,
|
||||
onClick: () => {
|
||||
this.startDetectingButton();
|
||||
},
|
||||
}),
|
||||
|
||||
// Mapping
|
||||
$rows,
|
||||
|
||||
// Vibration intensity
|
||||
createSettingRow(t('vibration-intensity'),
|
||||
this.$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 + '%';
|
||||
},
|
||||
}, boundUpdatePreset),
|
||||
),
|
||||
|
||||
// Range settings
|
||||
createSettingRow(t('left-trigger-range'),
|
||||
this.$leftTriggerRange = BxDualNumberStepper.create('left-trigger-range', this.BLANK_PRESET_DATA.settings.leftTriggerRange, params, boundUpdatePreset),
|
||||
),
|
||||
|
||||
createSettingRow(t('right-trigger-range'),
|
||||
this.$rightTriggerRange = BxDualNumberStepper.create('right-trigger-range', this.BLANK_PRESET_DATA.settings.rightTriggerRange, params, boundUpdatePreset),
|
||||
),
|
||||
|
||||
createSettingRow(t('left-stick-deadzone'),
|
||||
this.$leftStickDeadzone = BxDualNumberStepper.create('left-stick-deadzone', this.BLANK_PRESET_DATA.settings.leftStickDeadzone, params, boundUpdatePreset),
|
||||
),
|
||||
|
||||
createSettingRow(t('right-stick-deadzone'),
|
||||
this.$rightStickDeadzone = BxDualNumberStepper.create('right-stick-deadzone', this.BLANK_PRESET_DATA.settings.rightStickDeadzone, params, boundUpdatePreset),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private startDetectingButton() {
|
||||
this.isDetectingButton = true;
|
||||
|
||||
const { $btnDetect } = this;
|
||||
$btnDetect.classList.add('bx-monospaced', 'bx-blink-me');
|
||||
$btnDetect.disabled = true;
|
||||
|
||||
let count = 4;
|
||||
$btnDetect.textContent = `[${count}] ${t('press-any-button')}`;
|
||||
|
||||
this.detectIntervalId = window.setInterval(() => {
|
||||
count -= 1;
|
||||
if (count === 0) {
|
||||
this.stopDetectingButton();
|
||||
|
||||
// Re-focus the Detect button
|
||||
$btnDetect.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
$btnDetect.textContent = `[${count}] ${t('press-any-button')}`;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private stopDetectingButton() {
|
||||
const { $btnDetect } = this;
|
||||
$btnDetect.classList.remove('bx-monospaced', 'bx-blink-me');
|
||||
$btnDetect.textContent = t('detect-controller-button');
|
||||
$btnDetect.disabled = false;
|
||||
|
||||
this.isDetectingButton = false;
|
||||
this.detectIntervalId && window.clearInterval(this.detectIntervalId);
|
||||
this.detectIntervalId = null;
|
||||
}
|
||||
|
||||
async onBeforeMount() {
|
||||
this.stopDetectingButton();
|
||||
super.onBeforeMount(...arguments);
|
||||
}
|
||||
|
||||
onBeforeUnmount(): void {
|
||||
this.stopDetectingButton();
|
||||
StreamSettings.refreshControllerSettings();
|
||||
super.onBeforeUnmount();
|
||||
}
|
||||
|
||||
handleGamepad(button: GamepadKey): boolean {
|
||||
if (!this.isDetectingButton) {
|
||||
return super.handleGamepad(button);
|
||||
}
|
||||
|
||||
if (button in this.BUTTONS_ORDER) {
|
||||
this.stopDetectingButton();
|
||||
|
||||
const $select = this.selectsMap[button]!;
|
||||
const $label = $select.previousElementSibling!;
|
||||
$label.addEventListener('animationend', () => {
|
||||
$label.classList.remove('bx-horizontal-shaking');
|
||||
}, { once: true });
|
||||
$label.classList.add('bx-horizontal-shaking');
|
||||
|
||||
// Focus select
|
||||
if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
|
||||
this.dialogManager.focus($select);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected switchPreset(id: number): void {
|
||||
const preset = this.allPresets.data[id];
|
||||
if (!preset) {
|
||||
this.currentPresetId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
$btnDetect,
|
||||
$vibrationIntensity,
|
||||
$leftStickDeadzone,
|
||||
$rightStickDeadzone,
|
||||
$leftTriggerRange,
|
||||
$rightTriggerRange,
|
||||
selectsMap,
|
||||
} = this;
|
||||
|
||||
const presetData = preset.data;
|
||||
this.currentPresetId = id;
|
||||
const isDefaultPreset = id <= 0;
|
||||
this.updateButtonStates();
|
||||
|
||||
// Show/hide Detect button
|
||||
$btnDetect.classList.toggle('bx-gone', isDefaultPreset);
|
||||
|
||||
// Set mappings
|
||||
let buttonIndex: unknown;
|
||||
for (buttonIndex in selectsMap) {
|
||||
buttonIndex = buttonIndex as GamepadKey;
|
||||
|
||||
const $select = selectsMap[buttonIndex as GamepadKey];
|
||||
if (!$select) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mappedButton = presetData.mapping[buttonIndex as GamepadKey];
|
||||
|
||||
$select.value = typeof mappedButton === 'undefined' ? '' : mappedButton.toString();
|
||||
$select.disabled = isDefaultPreset;
|
||||
|
||||
BxEvent.dispatch($select, 'input', {
|
||||
ignoreOnChange: true,
|
||||
manualTrigger: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Add missing settings
|
||||
presetData.settings = Object.assign(this.BLANK_PRESET_DATA.settings, presetData.settings);
|
||||
|
||||
// Vibration intensity
|
||||
$vibrationIntensity.value = presetData.settings.vibrationIntensity.toString();
|
||||
$vibrationIntensity.dataset.disabled = isDefaultPreset.toString();
|
||||
|
||||
// Set extra settings
|
||||
$leftStickDeadzone.dataset.disabled = $rightStickDeadzone.dataset.disabled = $leftTriggerRange.dataset.disabled = $rightTriggerRange.dataset.disabled = isDefaultPreset.toString();
|
||||
$leftStickDeadzone.setValue(presetData.settings.leftStickDeadzone);
|
||||
$rightStickDeadzone.setValue(presetData.settings.rightStickDeadzone);
|
||||
$leftTriggerRange.setValue(presetData.settings.leftTriggerRange);
|
||||
$rightTriggerRange.setValue(presetData.settings.rightTriggerRange);
|
||||
}
|
||||
|
||||
private updatePreset() {
|
||||
const newData: ControllerCustomizationPresetData = deepClone(this.BLANK_PRESET_DATA);
|
||||
|
||||
// Set mappings
|
||||
let gamepadKey: unknown;
|
||||
for (gamepadKey in this.selectsMap) {
|
||||
const $select = this.selectsMap[gamepadKey as GamepadKey]!;
|
||||
const value = $select.value;
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mapTo = (value === 'false') ? false : parseInt(value);
|
||||
newData.mapping[gamepadKey as GamepadKey] = mapTo;
|
||||
}
|
||||
|
||||
// Set extra settings
|
||||
Object.assign(newData.settings, {
|
||||
vibrationIntensity: parseInt(this.$vibrationIntensity.value),
|
||||
|
||||
leftStickDeadzone: this.$leftStickDeadzone.getValue(),
|
||||
rightStickDeadzone: this.$rightStickDeadzone.getValue(),
|
||||
leftTriggerRange: this.$leftTriggerRange.getValue(),
|
||||
rightTriggerRange: this.$rightTriggerRange.getValue(),
|
||||
} satisfies typeof newData.settings);
|
||||
|
||||
// Update preset
|
||||
const preset = this.allPresets.data[this.currentPresetId!];
|
||||
preset.data = newData;
|
||||
this.presetsDb.updatePreset(preset);
|
||||
}
|
||||
}
|
@@ -2,12 +2,10 @@ 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";
|
||||
@@ -21,7 +19,7 @@ export class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog<C
|
||||
// private readonly LOG_TAG = 'ControllerShortcutsManagerDialog';
|
||||
|
||||
protected $content: HTMLElement;
|
||||
private selectActions: Partial<Record<GamepadKey, [HTMLSelectElement, HTMLSelectElement | null]>> = {};
|
||||
private selectActions: Partial<Record<GamepadKey, HTMLSelectElement>> = {};
|
||||
|
||||
protected readonly BLANK_PRESET_DATA = {
|
||||
mapping: {},
|
||||
@@ -39,19 +37,18 @@ export class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog<C
|
||||
constructor(title: string) {
|
||||
super(title, ControllerShortcutsTable.getInstance());
|
||||
|
||||
const PREF_CONTROLLER_FRIENDLY_UI = getPref(PrefKey.UI_CONTROLLER_FRIENDLY);
|
||||
const $baseSelect = CE('select', {
|
||||
class: 'bx-full-width',
|
||||
autocomplete: 'off',
|
||||
}, CE('option', { value: '' }, '---'));
|
||||
|
||||
// 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 });
|
||||
const $optGroup = CE('optgroup', { label: groupLabel });
|
||||
for (const action in items) {
|
||||
const crumbs = items[action as keyof typeof items];
|
||||
if (!crumbs) {
|
||||
@@ -59,7 +56,7 @@ export class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog<C
|
||||
}
|
||||
|
||||
const label = crumbs.join(' ❯ ');
|
||||
const $option = CE<HTMLOptionElement>('option', { value: action }, label);
|
||||
const $option = CE('option', { value: action }, label);
|
||||
$optGroup.appendChild($option);
|
||||
}
|
||||
|
||||
@@ -71,23 +68,6 @@ export class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog<C
|
||||
});
|
||||
|
||||
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();
|
||||
@@ -110,30 +90,18 @@ export class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog<C
|
||||
},
|
||||
});
|
||||
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];
|
||||
this.selectActions[button] = $select;
|
||||
|
||||
$div.appendChild($select);
|
||||
setNearby($row, {
|
||||
focus: $select,
|
||||
});
|
||||
|
||||
$row.append($label, $div);
|
||||
$row.append($label, $select);
|
||||
fragment.appendChild($row);
|
||||
}
|
||||
|
||||
@@ -156,10 +124,9 @@ export class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog<C
|
||||
// Reset selects' values
|
||||
let button: unknown;
|
||||
for (button in this.selectActions) {
|
||||
const [$select, $fakeSelect] = this.selectActions[button as GamepadKey]!;
|
||||
const $select = 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,
|
||||
@@ -175,8 +142,7 @@ export class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog<C
|
||||
|
||||
let button: unknown;
|
||||
for (button in this.selectActions) {
|
||||
const [$select, _] = this.selectActions[button as GamepadKey]!;
|
||||
|
||||
const $select = this.selectActions[button as GamepadKey]!;
|
||||
const action = $select.value;
|
||||
if (!action) {
|
||||
continue;
|
||||
@@ -185,10 +151,13 @@ export class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog<C
|
||||
newData.mapping[button as GamepadKey] = action as ShortcutAction;
|
||||
}
|
||||
|
||||
const preset = this.allPresets.data[this.currentPresetId];
|
||||
const preset = this.allPresets.data[this.currentPresetId!];
|
||||
preset.data = newData;
|
||||
this.presetsDb.updatePreset(preset);
|
||||
}
|
||||
|
||||
onBeforeUnmount() {
|
||||
StreamSettings.refreshControllerSettings();
|
||||
super.onBeforeUnmount();
|
||||
}
|
||||
}
|
||||
|
@@ -37,7 +37,7 @@ export class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog<Key
|
||||
continue;
|
||||
}
|
||||
|
||||
const $fieldSet = CE<HTMLFieldSetElement>('fieldset', {}, CE('legend', {}, groupLabel));
|
||||
const $fieldSet = CE('fieldset', {}, CE('legend', {}, groupLabel));
|
||||
for (const action in items) {
|
||||
const crumbs = items[action as keyof typeof items];
|
||||
if (!crumbs) {
|
||||
@@ -144,15 +144,19 @@ export class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog<Key
|
||||
}
|
||||
}
|
||||
|
||||
const oldPreset = this.allPresets.data[this.currentPresetId];
|
||||
const oldPreset = this.allPresets.data[this.currentPresetId!];
|
||||
const newPreset = {
|
||||
id: this.currentPresetId,
|
||||
id: this.currentPresetId!,
|
||||
name: oldPreset.name,
|
||||
data: presetData,
|
||||
};
|
||||
this.presetsDb.updatePreset(newPreset);
|
||||
|
||||
this.allPresets.data[this.currentPresetId] = newPreset;
|
||||
this.allPresets.data[this.currentPresetId!] = newPreset;
|
||||
}
|
||||
|
||||
onBeforeUnmount(): void {
|
||||
StreamSettings.refreshKeyboardShortcuts();
|
||||
super.onBeforeUnmount();
|
||||
}
|
||||
}
|
||||
|
@@ -121,9 +121,7 @@ export class MkbMappingManagerDialog extends BaseProfileManagerDialog<MkbPresetR
|
||||
|
||||
const $keyRow = CE('div', {
|
||||
class: 'bx-mkb-key-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
_nearby: { orientation: 'horizontal' },
|
||||
},
|
||||
CE('label', { title: buttonName }, buttonPrompt),
|
||||
$fragment,
|
||||
@@ -244,15 +242,19 @@ export class MkbMappingManagerDialog extends BaseProfileManagerDialog<MkbPresetR
|
||||
mouse.sensitivityY = parseInt(this.$mouseSensitivityY.value);
|
||||
mouse.deadzoneCounterweight = parseInt(this.$mouseDeadzone.value);
|
||||
|
||||
const oldPreset = this.allPresets.data[this.currentPresetId];
|
||||
const oldPreset = this.allPresets.data[this.currentPresetId!];
|
||||
const newPreset = {
|
||||
id: this.currentPresetId,
|
||||
id: this.currentPresetId!,
|
||||
name: oldPreset.name,
|
||||
data: presetData,
|
||||
};
|
||||
this.presetsDb.updatePreset(newPreset);
|
||||
|
||||
this.allPresets.data[this.currentPresetId] = newPreset;
|
||||
this.allPresets.data[this.currentPresetId!] = newPreset;
|
||||
}
|
||||
|
||||
onBeforeUnmount() {
|
||||
StreamSettings.refreshMkbSettings();
|
||||
super.onBeforeUnmount();
|
||||
}
|
||||
}
|
||||
|
@@ -37,10 +37,10 @@ export class RemotePlayDialog extends NavigationDialog {
|
||||
const $settingNote = CE('p', {});
|
||||
|
||||
const currentResolution = getPref(PrefKey.REMOTE_PLAY_STREAM_RESOLUTION);
|
||||
let $resolutions : HTMLSelectElement | NavigationElement = CE<HTMLSelectElement>('select', {},
|
||||
let $resolutions : HTMLSelectElement | NavigationElement = CE('select', {},
|
||||
CE('option', { value: StreamResolution.DIM_720P }, '720p'),
|
||||
CE('option', { value: StreamResolution.DIM_1080P }, '1080p'),
|
||||
// CE('option', { value: StreamResolution.DIM_1080P_HQ }, `1080p (HQ) ${t('experimental')}`),
|
||||
// CE('option', { value: StreamResolution.DIM_1080P_HQ }, `1080p (HQ)`),
|
||||
);
|
||||
|
||||
$resolutions = BxSelectElement.create($resolutions as HTMLSelectElement);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { isFullVersion } from "@macros/build" with { type: "macro" };
|
||||
|
||||
import { limitVideoPlayerFps, onChangeVideoPlayerType, updateVideoPlayer } from "@/modules/stream/stream-settings-utils";
|
||||
import { ButtonStyle, CE, createButton, createSettingRow, createSvgIcon, escapeCssSelector, type BxButtonOptions } from "@/utils/html";
|
||||
import { ButtonStyle, calculateSelectBoxes, CE, createButton, createSettingRow, createSvgIcon, escapeCssSelector, type BxButtonOptions } from "@/utils/html";
|
||||
import { NavigationDialog, NavigationDirection } from "./navigation-dialog";
|
||||
import { SoundShortcut } from "@/modules/shortcuts/sound-shortcut";
|
||||
import { StreamStats } from "@/modules/stream/stream-stats";
|
||||
@@ -37,7 +37,7 @@ type SettingTabSectionItem = Partial<{
|
||||
pref: PrefKey;
|
||||
multiLines: boolean;
|
||||
label: string;
|
||||
note: string | (() => HTMLElement);
|
||||
note: string | (() => HTMLElement) | HTMLElement;
|
||||
experimental: string;
|
||||
content: HTMLElement | (() => HTMLElement);
|
||||
options: { [key: string]: string };
|
||||
@@ -56,7 +56,7 @@ type SettingTabSection = {
|
||||
| 'stats';
|
||||
label?: string;
|
||||
unsupported?: boolean;
|
||||
unsupportedNote?: string | Text | null;
|
||||
unsupportedNote?: HTMLElement | string | Text | null;
|
||||
helpUrl?: string;
|
||||
content?: HTMLElement;
|
||||
lazyContent?: boolean | (() => HTMLElement);
|
||||
@@ -86,7 +86,7 @@ export class SettingsDialog extends NavigationDialog {
|
||||
private $btnReload!: HTMLElement;
|
||||
private $btnGlobalReload!: HTMLButtonElement;
|
||||
private $noteGlobalReload!: HTMLElement;
|
||||
private $btnSuggestion!: HTMLButtonElement;
|
||||
private $btnSuggestion!: HTMLDivElement;
|
||||
|
||||
private renderFullSettings: boolean;
|
||||
|
||||
@@ -106,7 +106,7 @@ export class SettingsDialog extends NavigationDialog {
|
||||
items: [
|
||||
// Top buttons
|
||||
($parent) => {
|
||||
const PREF_LATEST_VERSION = getPref<VersionLatest>(PrefKey.VERSION_LATEST);
|
||||
const PREF_LATEST_VERSION = getPref(PrefKey.VERSION_LATEST);
|
||||
const topButtons = [];
|
||||
|
||||
// "New version available" button
|
||||
@@ -322,7 +322,7 @@ export class SettingsDialog extends NavigationDialog {
|
||||
onCreated: (setting, $control) => {
|
||||
const defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent;
|
||||
|
||||
const $inpCustomUserAgent = CE<HTMLInputElement>('input', {
|
||||
const $inpCustomUserAgent = CE('input', {
|
||||
type: 'text',
|
||||
placeholder: defaultUserAgent,
|
||||
autocomplete: 'off',
|
||||
@@ -494,20 +494,7 @@ export class SettingsDialog extends NavigationDialog {
|
||||
}],
|
||||
}];
|
||||
|
||||
private readonly TAB_CONTROLLER_ITEMS: Array<SettingTabSection | HTMLElement | false> = [isFullVersion() && STATES.browser.capabilities.deviceVibration && {
|
||||
group: 'device',
|
||||
label: t('device'),
|
||||
items: [{
|
||||
pref: PrefKey.DEVICE_VIBRATION_MODE,
|
||||
multiLines: true,
|
||||
unsupported: !STATES.browser.capabilities.deviceVibration,
|
||||
onChange: () => StreamSettings.refreshControllerSettings(),
|
||||
}, {
|
||||
pref: PrefKey.DEVICE_VIBRATION_INTENSITY,
|
||||
unsupported: !STATES.browser.capabilities.deviceVibration,
|
||||
onChange: () => StreamSettings.refreshControllerSettings(),
|
||||
}],
|
||||
}, {
|
||||
private readonly TAB_CONTROLLER_ITEMS: Array<SettingTabSection | HTMLElement | false> = [{
|
||||
group: 'controller',
|
||||
label: t('controller'),
|
||||
helpUrl: 'https://better-xcloud.github.io/ingame-features/#controller',
|
||||
@@ -576,6 +563,19 @@ export class SettingsDialog extends NavigationDialog {
|
||||
});
|
||||
},
|
||||
}],
|
||||
}, isFullVersion() && STATES.browser.capabilities.deviceVibration && {
|
||||
group: 'device',
|
||||
label: t('device'),
|
||||
items: [{
|
||||
pref: PrefKey.DEVICE_VIBRATION_MODE,
|
||||
multiLines: true,
|
||||
unsupported: !STATES.browser.capabilities.deviceVibration,
|
||||
onChange: () => StreamSettings.refreshControllerSettings(),
|
||||
}, {
|
||||
pref: PrefKey.DEVICE_VIBRATION_INTENSITY,
|
||||
unsupported: !STATES.browser.capabilities.deviceVibration,
|
||||
onChange: () => StreamSettings.refreshControllerSettings(),
|
||||
}],
|
||||
}];
|
||||
|
||||
private readonly TAB_MKB_ITEMS: (() => Array<SettingTabSection | false>) = () => [
|
||||
@@ -764,9 +764,7 @@ export class SettingsDialog extends NavigationDialog {
|
||||
$child.classList.remove('bx-gone');
|
||||
|
||||
// Calculate size of controller-friendly select boxes
|
||||
if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
|
||||
this.dialogManager.calculateSelectBoxes($child as HTMLElement);
|
||||
}
|
||||
calculateSelectBoxes($child as HTMLElement);
|
||||
} else {
|
||||
// Hide tab content
|
||||
$child.classList.add('bx-gone');
|
||||
@@ -804,7 +802,7 @@ export class SettingsDialog extends NavigationDialog {
|
||||
}
|
||||
|
||||
private renderServerSetting(setting: SettingTabSectionItem): HTMLElement {
|
||||
let selectedValue = getPref<ServerRegionName>(PrefKey.SERVER_REGION);
|
||||
let selectedValue = getPref(PrefKey.SERVER_REGION);
|
||||
|
||||
const continents: Record<ServerContinent, {
|
||||
label: string,
|
||||
@@ -830,9 +828,8 @@ export class SettingsDialog extends NavigationDialog {
|
||||
},
|
||||
};
|
||||
|
||||
const $control = CE<HTMLSelectElement>('select', {
|
||||
const $control = CE('select', {
|
||||
id: `bx_setting_${escapeCssSelector(setting.pref!)}`,
|
||||
title: setting.label,
|
||||
tabindex: 0,
|
||||
});
|
||||
$control.name = $control.id;
|
||||
@@ -859,7 +856,7 @@ export class SettingsDialog extends NavigationDialog {
|
||||
|
||||
setting.options[value] = label;
|
||||
|
||||
const $option = CE<HTMLOptionElement>('option', { value }, label);
|
||||
const $option = CE('option', { value }, label);
|
||||
const continent = continents[region.contintent];
|
||||
if (!continent.children) {
|
||||
continent.children = [];
|
||||
|
@@ -1,14 +1,16 @@
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { getUniqueGamepadNames } from "@/utils/gamepad";
|
||||
import { CE, removeChildElements, createButton, ButtonStyle, createSettingRow, renderPresetsList } from "@/utils/html";
|
||||
import { CE, removeChildElements, createButton, ButtonStyle, createSettingRow, renderPresetsList, calculateSelectBoxes } 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";
|
||||
import { ControllerCustomizationsTable } from "@/utils/local-db/controller-customizations-table";
|
||||
import { ControllerCustomizationsManagerDialog } from "../profile-manger/controller-customizations-manager-dialog";
|
||||
import { BxIcon } from "@/utils/bx-icon";
|
||||
|
||||
export class ControllerExtraSettings extends HTMLElement {
|
||||
currentControllerId!: string;
|
||||
@@ -16,7 +18,7 @@ export class ControllerExtraSettings extends HTMLElement {
|
||||
|
||||
$selectControllers!: BxSelectElement;
|
||||
$selectShortcuts!: BxSelectElement;
|
||||
$vibrationIntensity!: BxNumberStepper;
|
||||
$selectCustomization!: BxSelectElement;
|
||||
|
||||
updateLayout!: () => void;
|
||||
switchController!: (id: string) => void;
|
||||
@@ -24,16 +26,17 @@ export class ControllerExtraSettings extends HTMLElement {
|
||||
saveSettings!: () => void;
|
||||
|
||||
static renderSettings(this: SettingsDialog): HTMLElement {
|
||||
const $container = CE<ControllerExtraSettings>('label', {
|
||||
const $container = CE('label', {
|
||||
class: 'bx-settings-row bx-controller-extra-settings',
|
||||
});
|
||||
}) as unknown as ControllerExtraSettings;
|
||||
|
||||
$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', {
|
||||
const $selectControllers = BxSelectElement.create(CE('select', {
|
||||
class: 'bx-full-width',
|
||||
autocomplete: 'off',
|
||||
_on: {
|
||||
input: (e: Event) => {
|
||||
@@ -41,24 +44,16 @@ export class ControllerExtraSettings extends HTMLElement {
|
||||
},
|
||||
},
|
||||
}));
|
||||
$selectControllers.classList.add('bx-full-width');
|
||||
|
||||
const $selectShortcuts = BxSelectElement.create(CE<HTMLSelectElement>('select', {
|
||||
const $selectShortcuts = BxSelectElement.create(CE('select', {
|
||||
autocomplete: 'off',
|
||||
_on: {
|
||||
input: $container.saveSettings,
|
||||
},
|
||||
_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);
|
||||
const $selectCustomization = BxSelectElement.create(CE('select', {
|
||||
autocomplete: 'off',
|
||||
_on: { input: $container.saveSettings },
|
||||
}));
|
||||
|
||||
$container.append(
|
||||
CE('span', {}, t('no-controllers-connected')),
|
||||
@@ -67,17 +62,16 @@ export class ControllerExtraSettings extends HTMLElement {
|
||||
|
||||
CE('div', { class: 'bx-sub-content-box' },
|
||||
createSettingRow(
|
||||
t('controller-shortcuts-in-game'),
|
||||
t('in-game-controller-shortcuts'),
|
||||
CE('div', {
|
||||
class: 'bx-preset-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
_nearby: { orientation: 'horizontal' },
|
||||
},
|
||||
$selectShortcuts,
|
||||
createButton({
|
||||
label: t('manage'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
title: t('manage'),
|
||||
icon: BxIcon.MANAGE,
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY | ButtonStyle.AUTO_HEIGHT,
|
||||
onClick: () => ControllerShortcutsManagerDialog.getInstance().show({
|
||||
id: parseInt($container.$selectShortcuts.value),
|
||||
}),
|
||||
@@ -87,8 +81,25 @@ export class ControllerExtraSettings extends HTMLElement {
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('vibration-intensity'),
|
||||
$vibrationIntensity,
|
||||
t('in-game-controller-customization'),
|
||||
CE('div', {
|
||||
class: 'bx-preset-row',
|
||||
_nearby: { orientation: 'horizontal' },
|
||||
},
|
||||
$selectCustomization,
|
||||
createButton({
|
||||
title: t('manage'),
|
||||
icon: BxIcon.MANAGE,
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY | ButtonStyle.AUTO_HEIGHT,
|
||||
onClick: () => ControllerCustomizationsManagerDialog.getInstance().show({
|
||||
id: $container.$selectCustomization.value ? parseInt($container.$selectCustomization.value) : null,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
{
|
||||
multiLines: true,
|
||||
$note: CE('div', { class: 'bx-settings-dialog-note' }, 'ⓘ ' + t('slightly-increase-input-latency')),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -96,7 +107,7 @@ export class ControllerExtraSettings extends HTMLElement {
|
||||
|
||||
$container.$selectControllers = $selectControllers;
|
||||
$container.$selectShortcuts = $selectShortcuts;
|
||||
$container.$vibrationIntensity = $vibrationIntensity;
|
||||
$container.$selectCustomization = $selectCustomization;
|
||||
|
||||
$container.updateLayout();
|
||||
|
||||
@@ -128,7 +139,7 @@ export class ControllerExtraSettings extends HTMLElement {
|
||||
|
||||
// Render controller list
|
||||
for (const name of this.controllerIds) {
|
||||
const $option = CE<HTMLOptionElement>('option', { value: name }, name);
|
||||
const $option = CE('option', { value: name }, name);
|
||||
$fragment.appendChild($option);
|
||||
}
|
||||
|
||||
@@ -138,12 +149,17 @@ export class ControllerExtraSettings extends HTMLElement {
|
||||
const allShortcutPresets = await ControllerShortcutsTable.getInstance().getPresets();
|
||||
renderPresetsList(this.$selectShortcuts, allShortcutPresets, null, { addOffValue: true });
|
||||
|
||||
// Render customization presets
|
||||
const allCustomizationPresets = await ControllerCustomizationsTable.getInstance().getPresets();
|
||||
renderPresetsList(this.$selectCustomization, allCustomizationPresets, null, { addOffValue: true });
|
||||
|
||||
for (const name of this.controllerIds) {
|
||||
const $option = CE<HTMLOptionElement>('option', { value: name }, name);
|
||||
const $option = CE('option', { value: name }, name);
|
||||
$fragment.appendChild($option);
|
||||
}
|
||||
|
||||
BxEvent.dispatch(this.$selectControllers, 'input');
|
||||
calculateSelectBoxes(this);
|
||||
}
|
||||
|
||||
private static async switchController(this: ControllerExtraSettings, id: string) {
|
||||
@@ -156,7 +172,7 @@ export class ControllerExtraSettings extends HTMLElement {
|
||||
|
||||
// Update UI
|
||||
this.$selectShortcuts.value = controllerSettings.shortcutPresetId.toString();
|
||||
this.$vibrationIntensity.value = controllerSettings.vibrationIntensity.toString();
|
||||
this.$selectCustomization.value = controllerSettings.customizationPresetId.toString();
|
||||
}
|
||||
|
||||
private static getCurrentControllerId(this: ControllerExtraSettings) {
|
||||
@@ -190,7 +206,7 @@ export class ControllerExtraSettings extends HTMLElement {
|
||||
id: this.currentControllerId,
|
||||
data: {
|
||||
shortcutPresetId: parseInt(this.$selectShortcuts.value),
|
||||
vibrationIntensity: parseInt(this.$vibrationIntensity.value),
|
||||
customizationPresetId: parseInt(this.$selectCustomization.value),
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -12,6 +12,7 @@ import { KeyboardShortcutsTable } from "@/utils/local-db/keyboard-shortcuts-tabl
|
||||
import { SettingElement } from "@/utils/setting-element";
|
||||
import { STORAGE } from "@/utils/global";
|
||||
import { EmulatedMkbHandler } from "@/modules/mkb/mkb-handler";
|
||||
import { BxIcon } from "@/utils/bx-icon";
|
||||
|
||||
export class MkbExtraSettings extends HTMLElement {
|
||||
private $mappingPresets!: BxSelectElement;
|
||||
@@ -28,14 +29,14 @@ export class MkbExtraSettings extends HTMLElement {
|
||||
$container.saveMkbSettings = MkbExtraSettings.saveMkbSettings.bind($container);
|
||||
$container.saveShortcutsSettings = MkbExtraSettings.saveShortcutsSettings.bind($container);
|
||||
|
||||
const $mappingPresets = BxSelectElement.create(CE<HTMLSelectElement>('select', {
|
||||
const $mappingPresets = BxSelectElement.create(CE('select', {
|
||||
autocomplete: 'off',
|
||||
_on: {
|
||||
input: $container.saveMkbSettings,
|
||||
},
|
||||
}));
|
||||
|
||||
const $shortcutsPresets = BxSelectElement.create(CE<HTMLSelectElement>('select', {
|
||||
const $shortcutsPresets = BxSelectElement.create(CE('select', {
|
||||
autocomplete: 'off',
|
||||
_on: {
|
||||
input: $container.saveShortcutsSettings,
|
||||
@@ -54,8 +55,9 @@ export class MkbExtraSettings extends HTMLElement {
|
||||
},
|
||||
$mappingPresets,
|
||||
createButton({
|
||||
label: t('manage'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
title: t('manage'),
|
||||
icon: BxIcon.MANAGE,
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY | ButtonStyle.AUTO_HEIGHT,
|
||||
onClick: () => MkbMappingManagerDialog.getInstance().show({
|
||||
id: parseInt($container.$mappingPresets.value),
|
||||
}),
|
||||
@@ -73,7 +75,7 @@ export class MkbExtraSettings extends HTMLElement {
|
||||
] : []),
|
||||
|
||||
createSettingRow(
|
||||
t('keyboard-shortcuts-in-game'),
|
||||
t('in-game-keyboard-shortcuts'),
|
||||
CE('div', {
|
||||
class: 'bx-preset-row',
|
||||
_nearby: {
|
||||
@@ -82,8 +84,9 @@ export class MkbExtraSettings extends HTMLElement {
|
||||
},
|
||||
$shortcutsPresets,
|
||||
createButton({
|
||||
label: t('manage'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
title: t('manage'),
|
||||
icon: BxIcon.MANAGE,
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY | ButtonStyle.AUTO_HEIGHT,
|
||||
onClick: () => KeyboardShortcutsManagerDialog.getInstance().show({
|
||||
id: parseInt($container.$shortcutsPresets.value),
|
||||
}),
|
||||
@@ -108,23 +111,23 @@ export class MkbExtraSettings extends HTMLElement {
|
||||
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));
|
||||
renderPresetsList(this.$mappingPresets, mappingPresets, getPref(PrefKey.MKB_P1_MAPPING_PRESET_ID));
|
||||
|
||||
// Render shortcut presets
|
||||
const shortcutsPresets = await KeyboardShortcutsTable.getInstance().getPresets();
|
||||
renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getPref<MkbPresetId>(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID), { addOffValue: true });
|
||||
renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getPref(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID), { addOffValue: true });
|
||||
}
|
||||
|
||||
private static async saveMkbSettings(this: MkbExtraSettings) {
|
||||
const presetId = parseInt(this.$mappingPresets.value);
|
||||
setPref<MkbPresetId>(PrefKey.MKB_P1_MAPPING_PRESET_ID, presetId);
|
||||
setPref(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);
|
||||
setPref(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID, presetId);
|
||||
|
||||
StreamSettings.refreshKeyboardShortcuts();
|
||||
}
|
||||
|
@@ -92,7 +92,7 @@ export class SuggestionsSetting {
|
||||
|
||||
// Start rendering
|
||||
const $suggestedSettings = CE('div', { class: 'bx-suggest-wrapper' });
|
||||
const $select = CE<HTMLSelectElement>('select', {},
|
||||
const $select = CE('select', {},
|
||||
hasRecommendedSettings && CE('option', { value: 'recommended' }, t('recommended')),
|
||||
!hasRecommendedSettings && CE('option', { value: 'highest' }, t('highest-quality')),
|
||||
CE('option', { value: 'default' }, t('default')),
|
||||
@@ -126,6 +126,7 @@ export class SuggestionsSetting {
|
||||
suggestedValue = settings[prefKey];
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const currentValue = getPref(prefKey, false);
|
||||
const currentValueText = STORAGE.Global.getValueText(prefKey, currentValue);
|
||||
const isSameValue = currentValue === suggestedValue;
|
||||
@@ -233,7 +234,7 @@ export class SuggestionsSetting {
|
||||
orientation: 'vertical',
|
||||
}
|
||||
},
|
||||
BxSelectElement.create($select, true),
|
||||
BxSelectElement.create($select),
|
||||
$suggestedSettings,
|
||||
$btnApply,
|
||||
|
||||
|
@@ -115,7 +115,7 @@ export class GuideMenu {
|
||||
});
|
||||
|
||||
// Set TV tag
|
||||
if (STATES.userAgent.isTv || getPref<UiLayout>(PrefKey.UI_LAYOUT) === UiLayout.TV) {
|
||||
if (STATES.userAgent.isTv || getPref(PrefKey.UI_LAYOUT) === UiLayout.TV) {
|
||||
document.body.dataset.bxMediaType = 'tv';
|
||||
}
|
||||
|
||||
|
@@ -50,7 +50,7 @@ export class HeaderSection {
|
||||
return;
|
||||
}
|
||||
|
||||
const PREF_LATEST_VERSION = getPref<VersionLatest>(PrefKey.VERSION_LATEST);
|
||||
const PREF_LATEST_VERSION = getPref(PrefKey.VERSION_LATEST);
|
||||
|
||||
// Setup Settings button
|
||||
const $btnSettings = this.$btnSettings;
|
||||
|
@@ -9,7 +9,7 @@ export function localRedirect(path: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $anchor = CE<HTMLAnchorElement>('a', {
|
||||
const $anchor = CE('a', {
|
||||
href: url,
|
||||
class: 'bx-hidden bx-offscreen',
|
||||
}, '');
|
||||
|
Reference in New Issue
Block a user