mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-14 00:49:15 +02:00
6.0
This commit is contained in:
205
src/modules/ui/dialog/profile-manger/base-profile-manager-dialog.ts
Executable file
205
src/modules/ui/dialog/profile-manger/base-profile-manager-dialog.ts
Executable file
@@ -0,0 +1,205 @@
|
||||
import { ButtonStyle, CE, createButton, renderPresetsList } from "@/utils/html";
|
||||
import { NavigationDialog } from "../navigation-dialog";
|
||||
import { BxIcon } from "@/utils/bx-icon";
|
||||
import { t } from "@/utils/translation";
|
||||
import type { AllPresets, PresetRecord } from "@/types/presets";
|
||||
import type { BasePresetsTable } from "@/utils/local-db/base-presets-table";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
|
||||
export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends NavigationDialog {
|
||||
$container!: HTMLElement;
|
||||
|
||||
private title: string;
|
||||
protected presetsDb: BasePresetsTable<T>;
|
||||
protected allPresets!: AllPresets<T>;
|
||||
protected currentPresetId: number = 0;
|
||||
|
||||
private $presets!: HTMLSelectElement;
|
||||
private $header!: HTMLElement;
|
||||
protected $content!: HTMLElement;
|
||||
|
||||
private $btnRename!: HTMLButtonElement;
|
||||
private $btnDelete!: HTMLButtonElement;
|
||||
|
||||
protected abstract readonly BLANK_PRESET_DATA: T['data'];
|
||||
|
||||
constructor(title: string, presetsDb: BasePresetsTable<T>) {
|
||||
super();
|
||||
|
||||
this.title = title;
|
||||
this.presetsDb = presetsDb;
|
||||
}
|
||||
|
||||
protected abstract switchPreset(id: number): void;
|
||||
|
||||
protected updateButtonStates() {
|
||||
const isDefaultPreset = this.currentPresetId <= 0;
|
||||
this.$btnRename.disabled = isDefaultPreset;
|
||||
this.$btnDelete.disabled = isDefaultPreset;
|
||||
}
|
||||
|
||||
private async renderPresetsList() {
|
||||
this.allPresets = await this.presetsDb.getPresets();
|
||||
if (!this.currentPresetId) {
|
||||
this.currentPresetId = this.allPresets.default[0];
|
||||
}
|
||||
|
||||
renderPresetsList<T>(this.$presets, this.allPresets, this.currentPresetId);
|
||||
}
|
||||
|
||||
private promptNewName(action: string,value='') {
|
||||
let newName: string | null = '';
|
||||
while (!newName) {
|
||||
newName = prompt(`[${action}] ${t('prompt-preset-name')}`, value);
|
||||
if (newName === null) {
|
||||
return false;
|
||||
}
|
||||
newName = newName.trim();
|
||||
}
|
||||
|
||||
return newName ? newName : false;
|
||||
};
|
||||
|
||||
private async renderDialog() {
|
||||
this.$presets = CE<HTMLSelectElement>('select', { tabindex: -1 });
|
||||
|
||||
const $select = BxSelectElement.create(this.$presets);
|
||||
$select.classList.add('bx-full-width');
|
||||
$select.addEventListener('input', e => {
|
||||
this.switchPreset(parseInt(($select as HTMLSelectElement).value));
|
||||
});
|
||||
|
||||
const $header = CE('div', {
|
||||
class: 'bx-dialog-preset-tools',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
focus: $select,
|
||||
},
|
||||
},
|
||||
$select,
|
||||
|
||||
// Rename button
|
||||
this.$btnRename = createButton({
|
||||
title: t('rename'),
|
||||
icon: BxIcon.CURSOR_TEXT,
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: async () => {
|
||||
const preset = this.allPresets.data[this.currentPresetId];
|
||||
|
||||
const newName = this.promptNewName(t('rename'), preset.name);
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update preset with new name
|
||||
preset.name = newName;
|
||||
|
||||
await this.presetsDb.updatePreset(preset);
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
|
||||
// Delete button
|
||||
this.$btnDelete = createButton({
|
||||
icon: BxIcon.TRASH,
|
||||
title: t('delete'),
|
||||
style: ButtonStyle.DANGER | ButtonStyle.FOCUSABLE,
|
||||
onClick: async (e) => {
|
||||
if (!confirm(t('confirm-delete-preset'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.presetsDb.deletePreset(this.currentPresetId);
|
||||
delete this.allPresets.data[this.currentPresetId];
|
||||
this.currentPresetId = parseInt(Object.keys(this.allPresets.data)[0]);
|
||||
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
|
||||
// New button
|
||||
createButton({
|
||||
icon: BxIcon.NEW,
|
||||
title: t('new'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: async (e) => {
|
||||
const newName = this.promptNewName(t('new'));
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new preset selected name
|
||||
const newId = await this.presetsDb.newPreset(newName, this.BLANK_PRESET_DATA);
|
||||
this.currentPresetId = newId;
|
||||
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
|
||||
// Copy button
|
||||
createButton({
|
||||
icon: BxIcon.COPY,
|
||||
title: t('copy'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: async (e) => {
|
||||
const preset = this.allPresets.data[this.currentPresetId];
|
||||
|
||||
const newName = this.promptNewName(t('copy'), `${preset.name} (2)`);
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new preset with selected name
|
||||
const newId = await this.presetsDb.newPreset(newName, preset.data);
|
||||
this.currentPresetId = newId;
|
||||
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
);
|
||||
this.$header = $header;
|
||||
|
||||
this.$container = CE('div', { class: 'bx-centered-dialog' },
|
||||
CE('div', { class: 'bx-dialog-title' },
|
||||
CE('p', {}, this.title),
|
||||
createButton({
|
||||
icon: BxIcon.CLOSE,
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR | ButtonStyle.GHOST,
|
||||
onClick: e => this.hide(),
|
||||
}),
|
||||
),
|
||||
$header,
|
||||
CE('div', { class: 'bx-dialog-content bx-hide-scroll-bar' }, this.$content),
|
||||
);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.renderPresetsList();
|
||||
this.switchPreset(this.currentPresetId);
|
||||
}
|
||||
|
||||
async onBeforeMount(configs:{ id?: number }={}) {
|
||||
if (configs?.id) {
|
||||
this.currentPresetId = configs.id;
|
||||
}
|
||||
|
||||
// Select first preset
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
getDialog(): NavigationDialog {
|
||||
return this;
|
||||
}
|
||||
|
||||
getContent(): HTMLElement {
|
||||
if (!this.$container) {
|
||||
this.renderDialog();
|
||||
}
|
||||
|
||||
return this.$container;
|
||||
}
|
||||
|
||||
focusIfNeeded(): void {
|
||||
this.dialogManager.focus(this.$header);
|
||||
}
|
||||
}
|
194
src/modules/ui/dialog/profile-manger/controller-shortcuts-manager-dialog.ts
Executable file
194
src/modules/ui/dialog/profile-manger/controller-shortcuts-manager-dialog.ts
Executable file
@@ -0,0 +1,194 @@
|
||||
import { t } from "@/utils/translation";
|
||||
import { BaseProfileManagerDialog } from "./base-profile-manager-dialog";
|
||||
import { CE } from "@/utils/html";
|
||||
import { GamepadKey, GamepadKeyName } from "@/enums/gamepad";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { PrompFont } from "@/enums/prompt-font";
|
||||
import { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { deepClone } from "@/utils/global";
|
||||
import { setNearby } from "@/utils/navigation-utils";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import type { ControllerShortcutPresetData, ControllerShortcutPresetRecord } from "@/types/presets";
|
||||
import { ControllerShortcutsTable } from "@/utils/local-db/controller-shortcuts-table";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { SHORTCUT_ACTIONS } from "@/modules/shortcuts/shortcut-actions";
|
||||
|
||||
export class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog<ControllerShortcutPresetRecord> {
|
||||
private static instance: ControllerShortcutsManagerDialog;
|
||||
public static getInstance = () => ControllerShortcutsManagerDialog.instance ?? (ControllerShortcutsManagerDialog.instance = new ControllerShortcutsManagerDialog(t('controller-shortcuts')));
|
||||
// private readonly LOG_TAG = 'ControllerShortcutsManagerDialog';
|
||||
|
||||
protected $content: HTMLElement;
|
||||
private selectActions: Partial<Record<GamepadKey, [HTMLSelectElement, HTMLSelectElement | null]>> = {};
|
||||
|
||||
protected readonly BLANK_PRESET_DATA = {
|
||||
mapping: {},
|
||||
};
|
||||
|
||||
private readonly BUTTONS_ORDER = [
|
||||
GamepadKey.Y, GamepadKey.A, GamepadKey.X, GamepadKey.B,
|
||||
GamepadKey.UP, GamepadKey.DOWN, GamepadKey.LEFT, GamepadKey.RIGHT,
|
||||
GamepadKey.SELECT, GamepadKey.START,
|
||||
GamepadKey.LB, GamepadKey.RB,
|
||||
GamepadKey.LT, GamepadKey.RT,
|
||||
GamepadKey.L3, GamepadKey.R3,
|
||||
];
|
||||
|
||||
constructor(title: string) {
|
||||
super(title, ControllerShortcutsTable.getInstance());
|
||||
|
||||
const PREF_CONTROLLER_FRIENDLY_UI = getPref(PrefKey.UI_CONTROLLER_FRIENDLY);
|
||||
|
||||
// Read actions from localStorage
|
||||
// ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage();
|
||||
|
||||
const $baseSelect = CE<HTMLSelectElement>('select', { autocomplete: 'off' }, CE('option', { value: '' }, '---'));
|
||||
for (const groupLabel in SHORTCUT_ACTIONS) {
|
||||
const items = SHORTCUT_ACTIONS[groupLabel];
|
||||
if (!items) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $optGroup = CE<HTMLOptGroupElement>('optgroup', { label: groupLabel });
|
||||
for (const action in items) {
|
||||
const crumbs = items[action as keyof typeof items];
|
||||
if (!crumbs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = crumbs.join(' ❯ ');
|
||||
const $option = CE<HTMLOptionElement>('option', { value: action }, label);
|
||||
$optGroup.appendChild($option);
|
||||
}
|
||||
|
||||
$baseSelect.appendChild($optGroup);
|
||||
}
|
||||
|
||||
const $content = CE('div', {
|
||||
class: 'bx-controller-shortcuts-manager-container',
|
||||
});
|
||||
|
||||
const onActionChanged = (e: Event) => {
|
||||
const $target = e.target as HTMLSelectElement;
|
||||
|
||||
// const profile = $selectProfile.value;
|
||||
// const button: unknown = $target.dataset.button;
|
||||
const action = $target.value as ShortcutAction;
|
||||
|
||||
if (!PREF_CONTROLLER_FRIENDLY_UI) {
|
||||
const $fakeSelect = $target.previousElementSibling! as HTMLSelectElement;
|
||||
let fakeText = '---';
|
||||
if (action) {
|
||||
const $selectedOption = $target.options[$target.selectedIndex];
|
||||
const $optGroup = $selectedOption.parentElement as HTMLOptGroupElement;
|
||||
fakeText = $optGroup.label + ' ❯ ' + $selectedOption.text;
|
||||
}
|
||||
($fakeSelect.firstElementChild as HTMLOptionElement).text = fakeText;
|
||||
}
|
||||
|
||||
// Update preset
|
||||
if (!(e as any).ignoreOnChange) {
|
||||
this.updatePreset();
|
||||
}
|
||||
};
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.appendChild(CE('p', {class: 'bx-shortcut-note'},
|
||||
CE('span', {class: 'bx-prompt'}, PrompFont.HOME),
|
||||
': ' + t('controller-shortcuts-xbox-note'),
|
||||
));
|
||||
|
||||
for (const button of this.BUTTONS_ORDER) {
|
||||
const prompt = GamepadKeyName[button][1];
|
||||
|
||||
const $row = CE('div', {
|
||||
class: 'bx-shortcut-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
});
|
||||
const $label = CE('label', {class: 'bx-prompt'}, `${PrompFont.HOME}${prompt}`);
|
||||
const $div = CE('div', {class: 'bx-shortcut-actions'});
|
||||
|
||||
let $fakeSelect: HTMLSelectElement | null = null;
|
||||
if (!PREF_CONTROLLER_FRIENDLY_UI) {
|
||||
$fakeSelect = CE<HTMLSelectElement>('select', { autocomplete: 'off' },
|
||||
CE('option', {}, '---'),
|
||||
);
|
||||
|
||||
$div.appendChild($fakeSelect);
|
||||
}
|
||||
|
||||
const $select = BxSelectElement.create($baseSelect.cloneNode(true) as HTMLSelectElement);
|
||||
$select.dataset.button = button.toString();
|
||||
$select.classList.add('bx-full-width');
|
||||
$select.addEventListener('input', onActionChanged);
|
||||
|
||||
this.selectActions[button] = [$select, $fakeSelect];
|
||||
|
||||
$div.appendChild($select);
|
||||
setNearby($row, {
|
||||
focus: $select,
|
||||
});
|
||||
|
||||
$row.append($label, $div);
|
||||
fragment.appendChild($row);
|
||||
}
|
||||
|
||||
$content.appendChild(fragment);
|
||||
|
||||
this.$content = $content;
|
||||
}
|
||||
|
||||
protected switchPreset(id: number): void {
|
||||
const preset = this.allPresets.data[id];
|
||||
if (!preset) {
|
||||
this.currentPresetId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentPresetId = id;
|
||||
const isDefaultPreset = id <= 0;
|
||||
const actions = preset.data;
|
||||
|
||||
// Reset selects' values
|
||||
let button: unknown;
|
||||
for (button in this.selectActions) {
|
||||
const [$select, $fakeSelect] = this.selectActions[button as GamepadKey]!;
|
||||
$select.value = actions.mapping[button as GamepadKey] || '';
|
||||
$select.disabled = isDefaultPreset;
|
||||
$fakeSelect && ($fakeSelect.disabled = isDefaultPreset);
|
||||
|
||||
BxEvent.dispatch($select, 'input', {
|
||||
ignoreOnChange: true,
|
||||
manualTrigger: true,
|
||||
});
|
||||
}
|
||||
|
||||
super.updateButtonStates();
|
||||
}
|
||||
|
||||
private updatePreset() {
|
||||
const newData: ControllerShortcutPresetData = deepClone(this.BLANK_PRESET_DATA);
|
||||
|
||||
let button: unknown;
|
||||
for (button in this.selectActions) {
|
||||
const [$select, _] = this.selectActions[button as GamepadKey]!;
|
||||
|
||||
const action = $select.value;
|
||||
if (!action) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newData.mapping[button as GamepadKey] = action as ShortcutAction;
|
||||
}
|
||||
|
||||
const preset = this.allPresets.data[this.currentPresetId];
|
||||
preset.data = newData;
|
||||
this.presetsDb.updatePreset(preset);
|
||||
|
||||
StreamSettings.refreshControllerSettings();
|
||||
}
|
||||
}
|
151
src/modules/ui/dialog/profile-manger/keyboard-shortcuts-manager-dialog.ts
Executable file
151
src/modules/ui/dialog/profile-manger/keyboard-shortcuts-manager-dialog.ts
Executable file
@@ -0,0 +1,151 @@
|
||||
import { t } from "@/utils/translation";
|
||||
import { BaseProfileManagerDialog } from "./base-profile-manager-dialog";
|
||||
import type { KeyboardShortcutPresetData, KeyboardShortcutPresetRecord } from "@/types/presets";
|
||||
import { CE, createSettingRow } from "@/utils/html";
|
||||
import { KeyboardShortcutDefaultId, KeyboardShortcutsTable } from "@/utils/local-db/keyboard-shortcuts-table";
|
||||
import { SHORTCUT_ACTIONS } from "@/modules/shortcuts/shortcut-actions";
|
||||
import { BxKeyBindingButton, BxKeyBindingButtonFlag } from "@/web-components/bx-key-binding-button";
|
||||
import type { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { deepClone } from "@/utils/global";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
|
||||
type KeyboardShortcutButtonDataset = {
|
||||
action: ShortcutAction,
|
||||
}
|
||||
|
||||
export class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog<KeyboardShortcutPresetRecord> {
|
||||
private static instance: KeyboardShortcutsManagerDialog;
|
||||
public static getInstance = () => KeyboardShortcutsManagerDialog.instance ?? (KeyboardShortcutsManagerDialog.instance = new KeyboardShortcutsManagerDialog(t('keyboard-shortcuts')));
|
||||
// private readonly LOG_TAG = 'KeyboardShortcutsManagerDialog';
|
||||
|
||||
protected $content: HTMLElement;
|
||||
private readonly allKeyElements: BxKeyBindingButton[] = [];
|
||||
|
||||
protected readonly BLANK_PRESET_DATA: KeyboardShortcutPresetData = {
|
||||
mapping: {},
|
||||
};
|
||||
|
||||
constructor(title: string) {
|
||||
super(title, KeyboardShortcutsTable.getInstance());
|
||||
|
||||
const $rows = CE('div', { class: 'bx-keyboard-shortcuts-manager-container' });
|
||||
|
||||
for (const groupLabel in SHORTCUT_ACTIONS) {
|
||||
const items = SHORTCUT_ACTIONS[groupLabel];
|
||||
if (!items) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $fieldSet = CE<HTMLFieldSetElement>('fieldset', {}, CE('legend', {}, groupLabel));
|
||||
for (const action in items) {
|
||||
const crumbs = items[action as keyof typeof items];
|
||||
if (!crumbs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = crumbs.join(' ❯ ');
|
||||
const $btn = BxKeyBindingButton.create({
|
||||
title: label,
|
||||
isPrompt: false,
|
||||
onChanged: this.onKeyChanged,
|
||||
|
||||
allowedFlags: [BxKeyBindingButtonFlag.KEYBOARD_PRESS, BxKeyBindingButtonFlag.KEYBOARD_MODIFIER],
|
||||
});
|
||||
$btn.classList.add('bx-full-width');
|
||||
$btn.dataset.action = action;
|
||||
this.allKeyElements.push($btn);
|
||||
|
||||
const $row = createSettingRow(label, CE('div', { class: 'bx-binding-button-wrapper' }, $btn));
|
||||
$fieldSet.appendChild($row);
|
||||
}
|
||||
|
||||
// Don't append empty <fieldset>
|
||||
if ($fieldSet.childElementCount > 1) {
|
||||
$rows.appendChild($fieldSet);
|
||||
}
|
||||
}
|
||||
|
||||
this.$content = CE('div', {}, $rows);
|
||||
}
|
||||
|
||||
private onKeyChanged = (e: Event) => {
|
||||
const $current = e.target as BxKeyBindingButton;
|
||||
const keyInfo = $current.keyInfo;
|
||||
|
||||
// Unbind duplicated keys
|
||||
if (keyInfo) {
|
||||
for (const $elm of this.allKeyElements) {
|
||||
if ($elm === $current) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($elm.keyInfo?.code === keyInfo.code && $elm.keyInfo?.modifiers === keyInfo.modifiers) {
|
||||
// Unbind manually
|
||||
$elm.unbindKey(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save preset
|
||||
this.savePreset();
|
||||
}
|
||||
|
||||
private parseDataset($btn: BxKeyBindingButton): KeyboardShortcutButtonDataset {
|
||||
const dataset = $btn.dataset;
|
||||
return {
|
||||
action: dataset.action as ShortcutAction,
|
||||
};
|
||||
}
|
||||
|
||||
protected switchPreset(id: number): void {
|
||||
const preset = this.allPresets.data[id];
|
||||
if (!preset) {
|
||||
this.currentPresetId = KeyboardShortcutDefaultId.OFF;
|
||||
return;
|
||||
}
|
||||
|
||||
const presetData = preset.data;
|
||||
this.currentPresetId = id;
|
||||
const isDefaultPreset = id <= 0;
|
||||
this.updateButtonStates();
|
||||
|
||||
// Update buttons
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const { action } = this.parseDataset($elm);
|
||||
|
||||
const keyInfo = presetData.mapping[action];
|
||||
if (keyInfo) {
|
||||
$elm.bindKey(keyInfo, true)
|
||||
} else {
|
||||
$elm.unbindKey(true);
|
||||
}
|
||||
|
||||
$elm.disabled = isDefaultPreset;
|
||||
}
|
||||
}
|
||||
|
||||
private savePreset() {
|
||||
const presetData = deepClone(this.BLANK_PRESET_DATA) as KeyboardShortcutPresetData;
|
||||
|
||||
// Get mapping
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const { action } = this.parseDataset($elm);
|
||||
|
||||
const mapping = presetData.mapping;
|
||||
if ($elm.keyInfo) {
|
||||
mapping[action] = $elm.keyInfo;
|
||||
}
|
||||
}
|
||||
|
||||
const oldPreset = this.allPresets.data[this.currentPresetId];
|
||||
const newPreset = {
|
||||
id: this.currentPresetId,
|
||||
name: oldPreset.name,
|
||||
data: presetData,
|
||||
};
|
||||
this.presetsDb.updatePreset(newPreset);
|
||||
|
||||
this.allPresets.data[this.currentPresetId] = newPreset;
|
||||
StreamSettings.refreshKeyboardShortcuts();
|
||||
}
|
||||
}
|
254
src/modules/ui/dialog/profile-manger/mkb-mapping-manager-dialog.ts
Executable file
254
src/modules/ui/dialog/profile-manger/mkb-mapping-manager-dialog.ts
Executable file
@@ -0,0 +1,254 @@
|
||||
import type { MkbPresetData, MkbPresetRecord } from "@/types/presets";
|
||||
import { BaseProfileManagerDialog } from "./base-profile-manager-dialog";
|
||||
import { t } from "@/utils/translation";
|
||||
import { MkbMappingPresetsTable } from "@/utils/local-db/mkb-mapping-presets-table";
|
||||
import { GamepadKey, GamepadKeyName } from "@/enums/gamepad";
|
||||
import { CE, createSettingRow } from "@/utils/html";
|
||||
import { MouseMapTo, MkbPresetKey, type KeyCode } from "@/enums/mkb";
|
||||
import { BxKeyBindingButton, BxKeyBindingButtonFlag } from "@/web-components/bx-key-binding-button";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { BxNumberStepper } from "@/web-components/bx-number-stepper";
|
||||
import { deepClone } from "@/utils/global";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
|
||||
type MkbButtonDataset = {
|
||||
keySlot: number,
|
||||
buttonIndex: GamepadKey,
|
||||
}
|
||||
|
||||
export class MkbMappingManagerDialog extends BaseProfileManagerDialog<MkbPresetRecord> {
|
||||
private static instance: MkbMappingManagerDialog;
|
||||
public static getInstance = () => MkbMappingManagerDialog.instance ?? (MkbMappingManagerDialog.instance = new MkbMappingManagerDialog(t('virtual-controller')));
|
||||
|
||||
declare protected $content: HTMLElement;
|
||||
|
||||
private readonly KEYS_PER_BUTTON = 2;
|
||||
private readonly BUTTONS_ORDER = [
|
||||
GamepadKey.HOME,
|
||||
GamepadKey.UP, GamepadKey.DOWN, GamepadKey.LEFT, GamepadKey.RIGHT,
|
||||
GamepadKey.A, GamepadKey.B, GamepadKey.X, GamepadKey.Y,
|
||||
GamepadKey.LB, GamepadKey.RB, GamepadKey.LT, GamepadKey.RT,
|
||||
GamepadKey.SELECT, GamepadKey.START,
|
||||
GamepadKey.L3, GamepadKey.LS_UP, GamepadKey.LS_DOWN, GamepadKey.LS_LEFT, GamepadKey.LS_RIGHT,
|
||||
GamepadKey.R3, GamepadKey.RS_UP, GamepadKey.RS_DOWN, GamepadKey.RS_LEFT, GamepadKey.RS_RIGHT,
|
||||
];
|
||||
|
||||
protected readonly BLANK_PRESET_DATA: MkbPresetData = {
|
||||
mapping: {},
|
||||
mouse: {
|
||||
[MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo.RS,
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_X]: 100,
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 100,
|
||||
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20,
|
||||
},
|
||||
};
|
||||
|
||||
private readonly allKeyElements: BxKeyBindingButton[] = [];
|
||||
private $mouseMapTo!: BxSelectElement;
|
||||
private $mouseSensitivityX!: BxNumberStepper;
|
||||
private $mouseSensitivityY!: BxNumberStepper;
|
||||
private $mouseDeadzone!: BxNumberStepper;
|
||||
|
||||
constructor(title: string) {
|
||||
super(title, MkbMappingPresetsTable.getInstance());
|
||||
this.render();
|
||||
}
|
||||
|
||||
private onBindingKey = (e: MouseEvent) => {
|
||||
const $btn = e.target as HTMLButtonElement;
|
||||
if ($btn.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
private parseDataset($btn: BxKeyBindingButton): MkbButtonDataset {
|
||||
const dataset = $btn.dataset;
|
||||
return {
|
||||
keySlot: parseInt(dataset.keySlot!),
|
||||
buttonIndex: parseInt(dataset.buttonIndex!),
|
||||
};
|
||||
}
|
||||
|
||||
private onKeyChanged = (e: Event) => {
|
||||
const $current = e.target as BxKeyBindingButton;
|
||||
const keyInfo = $current.keyInfo;
|
||||
|
||||
// Unbind duplicated keys
|
||||
if (keyInfo) {
|
||||
for (const $elm of this.allKeyElements) {
|
||||
if ($elm !== $current && $elm.keyInfo?.code === keyInfo.code) {
|
||||
// Unbind manually
|
||||
$elm.unbindKey(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save preset
|
||||
this.savePreset();
|
||||
}
|
||||
|
||||
private render() {
|
||||
const $rows = CE('div', {},
|
||||
CE('i', { class: 'bx-mkb-note' }, t('right-click-to-unbind')),
|
||||
);
|
||||
|
||||
for (const buttonIndex of this.BUTTONS_ORDER) {
|
||||
const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex];
|
||||
|
||||
let $elm;
|
||||
const $fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < this.KEYS_PER_BUTTON; i++) {
|
||||
$elm = BxKeyBindingButton.create({
|
||||
title: buttonPrompt,
|
||||
isPrompt: true,
|
||||
allowedFlags: [BxKeyBindingButtonFlag.KEYBOARD_PRESS, BxKeyBindingButtonFlag.MOUSE_CLICK, BxKeyBindingButtonFlag.MOUSE_WHEEL],
|
||||
onChanged: this.onKeyChanged,
|
||||
});
|
||||
|
||||
$elm.dataset.buttonIndex = buttonIndex.toString();
|
||||
$elm.dataset.keySlot = i.toString();
|
||||
|
||||
$elm.addEventListener('mouseup', this.onBindingKey);
|
||||
|
||||
$fragment.appendChild($elm);
|
||||
this.allKeyElements.push($elm);
|
||||
}
|
||||
|
||||
const $keyRow = CE('div', {
|
||||
class: 'bx-mkb-key-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
},
|
||||
CE('label', { title: buttonName }, buttonPrompt),
|
||||
$fragment,
|
||||
);
|
||||
|
||||
$rows.appendChild($keyRow);
|
||||
}
|
||||
|
||||
const savePreset = () => this.savePreset();
|
||||
const $extraSettings = CE('div', {},
|
||||
createSettingRow(
|
||||
t('map-mouse-to'),
|
||||
this.$mouseMapTo = BxSelectElement.create(CE('select', { _on: { input: savePreset } },
|
||||
CE('option', { value: MouseMapTo.RS }, t('right-stick')),
|
||||
CE('option', { value: MouseMapTo.LS }, t('left-stick')),
|
||||
CE('option', { value: MouseMapTo.OFF }, t('off')),
|
||||
)),
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('horizontal-sensitivity'),
|
||||
this.$mouseSensitivityX = BxNumberStepper.create('hor_sensitivity', 0, 1, 300, {
|
||||
suffix: '%',
|
||||
exactTicks: 50,
|
||||
}, savePreset),
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('vertical-sensitivity'),
|
||||
this.$mouseSensitivityY = BxNumberStepper.create('ver_sensitivity', 0, 1, 300, {
|
||||
suffix: '%',
|
||||
exactTicks: 50,
|
||||
}, savePreset),
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('deadzone-counterweight'),
|
||||
this.$mouseDeadzone = BxNumberStepper.create('deadzone_counterweight', 0, 1, 50, {
|
||||
suffix: '%',
|
||||
exactTicks: 10,
|
||||
}, savePreset),
|
||||
),
|
||||
);
|
||||
|
||||
this.$content = CE('div', {},
|
||||
$rows,
|
||||
$extraSettings,
|
||||
);
|
||||
}
|
||||
|
||||
protected switchPreset(id: number): void {
|
||||
const preset = this.allPresets.data[id];
|
||||
if (!preset) {
|
||||
this.currentPresetId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const presetData = preset.data;
|
||||
this.currentPresetId = id;
|
||||
const isDefaultPreset = id <= 0;
|
||||
this.updateButtonStates();
|
||||
|
||||
// Update buttons
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const { buttonIndex, keySlot } = this.parseDataset($elm);
|
||||
|
||||
const buttonKeys = presetData.mapping[buttonIndex];
|
||||
if (buttonKeys && buttonKeys[keySlot]) {
|
||||
$elm.bindKey({
|
||||
code: buttonKeys[keySlot],
|
||||
}, true)
|
||||
} else {
|
||||
$elm.unbindKey(true);
|
||||
}
|
||||
|
||||
$elm.disabled = isDefaultPreset;
|
||||
}
|
||||
|
||||
// Update mouse settings
|
||||
const mouse = presetData.mouse;
|
||||
this.$mouseMapTo.value = mouse.mapTo.toString();
|
||||
this.$mouseSensitivityX.value = mouse.sensitivityX.toString();
|
||||
this.$mouseSensitivityY.value = mouse.sensitivityY.toString();
|
||||
this.$mouseDeadzone.value = mouse.deadzoneCounterweight.toString();
|
||||
|
||||
this.$mouseMapTo.disabled = isDefaultPreset;
|
||||
this.$mouseSensitivityX.dataset.disabled = isDefaultPreset.toString();
|
||||
this.$mouseSensitivityY.dataset.disabled = isDefaultPreset.toString();
|
||||
this.$mouseDeadzone.dataset.disabled = isDefaultPreset.toString();
|
||||
}
|
||||
|
||||
private savePreset() {
|
||||
const presetData = deepClone(this.BLANK_PRESET_DATA) as MkbPresetData;
|
||||
|
||||
// Get mapping
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const { buttonIndex, keySlot } = this.parseDataset($elm);
|
||||
const mapping = presetData.mapping;
|
||||
if (!mapping[buttonIndex]) {
|
||||
mapping[buttonIndex] = [];
|
||||
}
|
||||
|
||||
if (!$elm.keyInfo) {
|
||||
// Remove empty key from mapping
|
||||
delete mapping[buttonIndex][keySlot];
|
||||
} else {
|
||||
mapping[buttonIndex][keySlot] = $elm.keyInfo.code as KeyCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Get mouse settings
|
||||
const mouse = presetData.mouse;
|
||||
mouse.mapTo = parseInt(this.$mouseMapTo.value) as MouseMapTo;
|
||||
mouse.sensitivityX = parseInt(this.$mouseSensitivityX.value);
|
||||
mouse.sensitivityY = parseInt(this.$mouseSensitivityY.value);
|
||||
mouse.deadzoneCounterweight = parseInt(this.$mouseDeadzone.value);
|
||||
|
||||
const oldPreset = this.allPresets.data[this.currentPresetId];
|
||||
const newPreset = {
|
||||
id: this.currentPresetId,
|
||||
name: oldPreset.name,
|
||||
data: presetData,
|
||||
};
|
||||
this.presetsDb.updatePreset(newPreset);
|
||||
|
||||
this.allPresets.data[this.currentPresetId] = newPreset;
|
||||
StreamSettings.refreshMkbSettings();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user