mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-13 00:19:17 +02:00
6.0
This commit is contained in:
85
src/modules/ui/dialog/navigation-dialog.ts
Normal file → Executable file
85
src/modules/ui/dialog/navigation-dialog.ts
Normal file → Executable file
@@ -1,9 +1,8 @@
|
||||
import { GamepadKey } from "@/enums/mkb";
|
||||
import { GamepadKey } from "@/enums/gamepad";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { BxLogger } from "@/utils/bx-logger";
|
||||
import { STATES } from "@/utils/global";
|
||||
import { CE, isElementVisible } from "@/utils/html";
|
||||
import { setNearby } from "@/utils/navigation-utils";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
@@ -40,13 +39,22 @@ export abstract class NavigationDialog {
|
||||
|
||||
abstract $container: HTMLElement;
|
||||
dialogManager: NavigationDialogManager;
|
||||
onMountedCallbacks: Array<() => void> = [];
|
||||
|
||||
constructor() {
|
||||
this.dialogManager = NavigationDialogManager.getInstance();
|
||||
}
|
||||
|
||||
show() {
|
||||
NavigationDialogManager.getInstance().show(this);
|
||||
isCancellable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
isOverlayVisible(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
show(configs={}, clearStack=false) {
|
||||
NavigationDialogManager.getInstance().show(this, configs, clearStack);
|
||||
|
||||
const $currentFocus = this.getFocusedElement();
|
||||
// If not focusing on any element
|
||||
@@ -73,8 +81,12 @@ export abstract class NavigationDialog {
|
||||
return null;
|
||||
}
|
||||
|
||||
onBeforeMount(): void {}
|
||||
onMounted(): void {}
|
||||
onBeforeMount(configs={}): void {}
|
||||
onMounted(configs={}): void {
|
||||
for (const callback of this.onMountedCallbacks) {
|
||||
callback.call(this);
|
||||
}
|
||||
}
|
||||
onBeforeUnmount(): void {}
|
||||
onUnmounted(): void {}
|
||||
|
||||
@@ -119,12 +131,12 @@ export class NavigationDialogManager {
|
||||
};
|
||||
|
||||
private static readonly SIBLING_PROPERTY_MAP = {
|
||||
'horizontal': {
|
||||
horizontal: {
|
||||
[NavigationDirection.LEFT]: 'previousElementSibling',
|
||||
[NavigationDirection.RIGHT]: 'nextElementSibling',
|
||||
},
|
||||
|
||||
'vertical': {
|
||||
vertical: {
|
||||
[NavigationDirection.UP]: 'previousElementSibling',
|
||||
[NavigationDirection.DOWN]: 'nextElementSibling',
|
||||
},
|
||||
@@ -137,6 +149,7 @@ export class NavigationDialogManager {
|
||||
private $overlay: HTMLElement;
|
||||
private $container: HTMLElement;
|
||||
private dialog: NavigationDialog | null = null;
|
||||
private dialogsStack: Array<NavigationDialog> = [];
|
||||
|
||||
private constructor() {
|
||||
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||
@@ -145,7 +158,8 @@ export class NavigationDialogManager {
|
||||
this.$overlay.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.hide();
|
||||
|
||||
this.dialog?.isCancellable() && this.hide();
|
||||
});
|
||||
|
||||
document.documentElement.appendChild(this.$overlay);
|
||||
@@ -214,9 +228,16 @@ export class NavigationDialogManager {
|
||||
};
|
||||
}
|
||||
|
||||
private updateActiveInput(input: 'keyboard' | 'gamepad' | 'mouse') {
|
||||
// Set <html>'s activeInput
|
||||
document.documentElement.dataset.activeInput = input;
|
||||
}
|
||||
|
||||
handleEvent(event: Event) {
|
||||
switch (event.type) {
|
||||
case 'keydown':
|
||||
this.updateActiveInput('keyboard');
|
||||
|
||||
const $target = event.target as HTMLElement;
|
||||
const keyboardEvent = event as KeyboardEvent;
|
||||
const keyCode = keyboardEvent.code || keyboardEvent.key;
|
||||
@@ -259,7 +280,7 @@ export class NavigationDialogManager {
|
||||
return this.$container && !this.$container.classList.contains('bx-gone');
|
||||
}
|
||||
|
||||
private pollGamepad() {
|
||||
private pollGamepad = () => {
|
||||
const gamepads = window.navigator.getGamepads();
|
||||
|
||||
for (const gamepad of gamepads) {
|
||||
@@ -365,6 +386,12 @@ export class NavigationDialogManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateActiveInput('gamepad');
|
||||
|
||||
if (this.handleGamepad(gamepad, releasedButton)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (releasedButton === GamepadKey.A) {
|
||||
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||
return;
|
||||
@@ -372,10 +399,6 @@ export class NavigationDialogManager {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.handleGamepad(gamepad, releasedButton)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,7 +436,7 @@ export class NavigationDialogManager {
|
||||
this.gamepadHoldingIntervalId = null;
|
||||
}
|
||||
|
||||
show(dialog: NavigationDialog) {
|
||||
show(dialog: NavigationDialog, configs={}, clearStack=false) {
|
||||
this.clearGamepadHoldingInterval();
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN);
|
||||
@@ -424,20 +447,21 @@ export class NavigationDialogManager {
|
||||
// Lock scroll bar
|
||||
document.body.classList.add('bx-no-scroll');
|
||||
|
||||
// Show overlay
|
||||
this.$overlay.classList.remove('bx-gone');
|
||||
if (STATES.isPlaying) {
|
||||
this.$overlay.classList.add('bx-invisible');
|
||||
}
|
||||
|
||||
// Unmount current dialog
|
||||
this.unmountCurrentDialog();
|
||||
|
||||
// Add to dialogs stack
|
||||
this.dialogsStack.push(dialog);
|
||||
|
||||
// Setup new dialog
|
||||
this.dialog = dialog;
|
||||
dialog.onBeforeMount();
|
||||
dialog.onBeforeMount(configs);
|
||||
this.$container.appendChild(dialog.getContent());
|
||||
dialog.onMounted();
|
||||
dialog.onMounted(configs);
|
||||
|
||||
// Show overlay
|
||||
this.$overlay.classList.remove('bx-gone');
|
||||
this.$overlay.classList.toggle('bx-invisible', !dialog.isOverlayVisible());
|
||||
|
||||
// Show content
|
||||
this.$container.classList.remove('bx-gone');
|
||||
@@ -468,11 +492,24 @@ export class NavigationDialogManager {
|
||||
// Stop gamepad polling
|
||||
this.stopGamepadPolling();
|
||||
|
||||
// Remove current dialog and everything after it from dialogs stack
|
||||
if (this.dialog) {
|
||||
const dialogIndex = this.dialogsStack.indexOf(this.dialog);
|
||||
if (dialogIndex > -1) {
|
||||
this.dialogsStack = this.dialogsStack.slice(0, dialogIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Unmount dialog
|
||||
this.unmountCurrentDialog();
|
||||
|
||||
// Enable xCloud's navigation polling
|
||||
(window as any).BX_EXPOSED.disableGamepadPolling = false;
|
||||
|
||||
// Show the last dialog in dialogs stack
|
||||
if (this.dialogsStack.length) {
|
||||
this.dialogsStack[this.dialogsStack.length - 1].show();
|
||||
}
|
||||
}
|
||||
|
||||
focus($elm: NavigationElement | null): boolean {
|
||||
@@ -624,7 +661,7 @@ export class NavigationDialogManager {
|
||||
private startGamepadPolling() {
|
||||
this.stopGamepadPolling();
|
||||
|
||||
this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad.bind(this), NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);
|
||||
this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);
|
||||
}
|
||||
|
||||
private stopGamepadPolling() {
|
||||
|
205
src/modules/ui/dialog/profile-manger/base-profile-manager-dialog.ts
Executable file
205
src/modules/ui/dialog/profile-manger/base-profile-manager-dialog.ts
Executable file
@@ -0,0 +1,205 @@
|
||||
import { ButtonStyle, CE, createButton, renderPresetsList } from "@/utils/html";
|
||||
import { NavigationDialog } from "../navigation-dialog";
|
||||
import { BxIcon } from "@/utils/bx-icon";
|
||||
import { t } from "@/utils/translation";
|
||||
import type { AllPresets, PresetRecord } from "@/types/presets";
|
||||
import type { BasePresetsTable } from "@/utils/local-db/base-presets-table";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
|
||||
export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends NavigationDialog {
|
||||
$container!: HTMLElement;
|
||||
|
||||
private title: string;
|
||||
protected presetsDb: BasePresetsTable<T>;
|
||||
protected allPresets!: AllPresets<T>;
|
||||
protected currentPresetId: number = 0;
|
||||
|
||||
private $presets!: HTMLSelectElement;
|
||||
private $header!: HTMLElement;
|
||||
protected $content!: HTMLElement;
|
||||
|
||||
private $btnRename!: HTMLButtonElement;
|
||||
private $btnDelete!: HTMLButtonElement;
|
||||
|
||||
protected abstract readonly BLANK_PRESET_DATA: T['data'];
|
||||
|
||||
constructor(title: string, presetsDb: BasePresetsTable<T>) {
|
||||
super();
|
||||
|
||||
this.title = title;
|
||||
this.presetsDb = presetsDb;
|
||||
}
|
||||
|
||||
protected abstract switchPreset(id: number): void;
|
||||
|
||||
protected updateButtonStates() {
|
||||
const isDefaultPreset = this.currentPresetId <= 0;
|
||||
this.$btnRename.disabled = isDefaultPreset;
|
||||
this.$btnDelete.disabled = isDefaultPreset;
|
||||
}
|
||||
|
||||
private async renderPresetsList() {
|
||||
this.allPresets = await this.presetsDb.getPresets();
|
||||
if (!this.currentPresetId) {
|
||||
this.currentPresetId = this.allPresets.default[0];
|
||||
}
|
||||
|
||||
renderPresetsList<T>(this.$presets, this.allPresets, this.currentPresetId);
|
||||
}
|
||||
|
||||
private promptNewName(action: string,value='') {
|
||||
let newName: string | null = '';
|
||||
while (!newName) {
|
||||
newName = prompt(`[${action}] ${t('prompt-preset-name')}`, value);
|
||||
if (newName === null) {
|
||||
return false;
|
||||
}
|
||||
newName = newName.trim();
|
||||
}
|
||||
|
||||
return newName ? newName : false;
|
||||
};
|
||||
|
||||
private async renderDialog() {
|
||||
this.$presets = CE<HTMLSelectElement>('select', { tabindex: -1 });
|
||||
|
||||
const $select = BxSelectElement.create(this.$presets);
|
||||
$select.classList.add('bx-full-width');
|
||||
$select.addEventListener('input', e => {
|
||||
this.switchPreset(parseInt(($select as HTMLSelectElement).value));
|
||||
});
|
||||
|
||||
const $header = CE('div', {
|
||||
class: 'bx-dialog-preset-tools',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
focus: $select,
|
||||
},
|
||||
},
|
||||
$select,
|
||||
|
||||
// Rename button
|
||||
this.$btnRename = createButton({
|
||||
title: t('rename'),
|
||||
icon: BxIcon.CURSOR_TEXT,
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: async () => {
|
||||
const preset = this.allPresets.data[this.currentPresetId];
|
||||
|
||||
const newName = this.promptNewName(t('rename'), preset.name);
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update preset with new name
|
||||
preset.name = newName;
|
||||
|
||||
await this.presetsDb.updatePreset(preset);
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
|
||||
// Delete button
|
||||
this.$btnDelete = createButton({
|
||||
icon: BxIcon.TRASH,
|
||||
title: t('delete'),
|
||||
style: ButtonStyle.DANGER | ButtonStyle.FOCUSABLE,
|
||||
onClick: async (e) => {
|
||||
if (!confirm(t('confirm-delete-preset'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.presetsDb.deletePreset(this.currentPresetId);
|
||||
delete this.allPresets.data[this.currentPresetId];
|
||||
this.currentPresetId = parseInt(Object.keys(this.allPresets.data)[0]);
|
||||
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
|
||||
// New button
|
||||
createButton({
|
||||
icon: BxIcon.NEW,
|
||||
title: t('new'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: async (e) => {
|
||||
const newName = this.promptNewName(t('new'));
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new preset selected name
|
||||
const newId = await this.presetsDb.newPreset(newName, this.BLANK_PRESET_DATA);
|
||||
this.currentPresetId = newId;
|
||||
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
|
||||
// Copy button
|
||||
createButton({
|
||||
icon: BxIcon.COPY,
|
||||
title: t('copy'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: async (e) => {
|
||||
const preset = this.allPresets.data[this.currentPresetId];
|
||||
|
||||
const newName = this.promptNewName(t('copy'), `${preset.name} (2)`);
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new preset with selected name
|
||||
const newId = await this.presetsDb.newPreset(newName, preset.data);
|
||||
this.currentPresetId = newId;
|
||||
|
||||
await this.refresh();
|
||||
},
|
||||
}),
|
||||
);
|
||||
this.$header = $header;
|
||||
|
||||
this.$container = CE('div', { class: 'bx-centered-dialog' },
|
||||
CE('div', { class: 'bx-dialog-title' },
|
||||
CE('p', {}, this.title),
|
||||
createButton({
|
||||
icon: BxIcon.CLOSE,
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR | ButtonStyle.GHOST,
|
||||
onClick: e => this.hide(),
|
||||
}),
|
||||
),
|
||||
$header,
|
||||
CE('div', { class: 'bx-dialog-content bx-hide-scroll-bar' }, this.$content),
|
||||
);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.renderPresetsList();
|
||||
this.switchPreset(this.currentPresetId);
|
||||
}
|
||||
|
||||
async onBeforeMount(configs:{ id?: number }={}) {
|
||||
if (configs?.id) {
|
||||
this.currentPresetId = configs.id;
|
||||
}
|
||||
|
||||
// Select first preset
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
getDialog(): NavigationDialog {
|
||||
return this;
|
||||
}
|
||||
|
||||
getContent(): HTMLElement {
|
||||
if (!this.$container) {
|
||||
this.renderDialog();
|
||||
}
|
||||
|
||||
return this.$container;
|
||||
}
|
||||
|
||||
focusIfNeeded(): void {
|
||||
this.dialogManager.focus(this.$header);
|
||||
}
|
||||
}
|
194
src/modules/ui/dialog/profile-manger/controller-shortcuts-manager-dialog.ts
Executable file
194
src/modules/ui/dialog/profile-manger/controller-shortcuts-manager-dialog.ts
Executable file
@@ -0,0 +1,194 @@
|
||||
import { t } from "@/utils/translation";
|
||||
import { BaseProfileManagerDialog } from "./base-profile-manager-dialog";
|
||||
import { CE } from "@/utils/html";
|
||||
import { GamepadKey, GamepadKeyName } from "@/enums/gamepad";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { PrompFont } from "@/enums/prompt-font";
|
||||
import { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { deepClone } from "@/utils/global";
|
||||
import { setNearby } from "@/utils/navigation-utils";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import type { ControllerShortcutPresetData, ControllerShortcutPresetRecord } from "@/types/presets";
|
||||
import { ControllerShortcutsTable } from "@/utils/local-db/controller-shortcuts-table";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { SHORTCUT_ACTIONS } from "@/modules/shortcuts/shortcut-actions";
|
||||
|
||||
export class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog<ControllerShortcutPresetRecord> {
|
||||
private static instance: ControllerShortcutsManagerDialog;
|
||||
public static getInstance = () => ControllerShortcutsManagerDialog.instance ?? (ControllerShortcutsManagerDialog.instance = new ControllerShortcutsManagerDialog(t('controller-shortcuts')));
|
||||
// private readonly LOG_TAG = 'ControllerShortcutsManagerDialog';
|
||||
|
||||
protected $content: HTMLElement;
|
||||
private selectActions: Partial<Record<GamepadKey, [HTMLSelectElement, HTMLSelectElement | null]>> = {};
|
||||
|
||||
protected readonly BLANK_PRESET_DATA = {
|
||||
mapping: {},
|
||||
};
|
||||
|
||||
private readonly BUTTONS_ORDER = [
|
||||
GamepadKey.Y, GamepadKey.A, GamepadKey.X, GamepadKey.B,
|
||||
GamepadKey.UP, GamepadKey.DOWN, GamepadKey.LEFT, GamepadKey.RIGHT,
|
||||
GamepadKey.SELECT, GamepadKey.START,
|
||||
GamepadKey.LB, GamepadKey.RB,
|
||||
GamepadKey.LT, GamepadKey.RT,
|
||||
GamepadKey.L3, GamepadKey.R3,
|
||||
];
|
||||
|
||||
constructor(title: string) {
|
||||
super(title, ControllerShortcutsTable.getInstance());
|
||||
|
||||
const PREF_CONTROLLER_FRIENDLY_UI = getPref(PrefKey.UI_CONTROLLER_FRIENDLY);
|
||||
|
||||
// Read actions from localStorage
|
||||
// ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage();
|
||||
|
||||
const $baseSelect = CE<HTMLSelectElement>('select', { autocomplete: 'off' }, CE('option', { value: '' }, '---'));
|
||||
for (const groupLabel in SHORTCUT_ACTIONS) {
|
||||
const items = SHORTCUT_ACTIONS[groupLabel];
|
||||
if (!items) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $optGroup = CE<HTMLOptGroupElement>('optgroup', { label: groupLabel });
|
||||
for (const action in items) {
|
||||
const crumbs = items[action as keyof typeof items];
|
||||
if (!crumbs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = crumbs.join(' ❯ ');
|
||||
const $option = CE<HTMLOptionElement>('option', { value: action }, label);
|
||||
$optGroup.appendChild($option);
|
||||
}
|
||||
|
||||
$baseSelect.appendChild($optGroup);
|
||||
}
|
||||
|
||||
const $content = CE('div', {
|
||||
class: 'bx-controller-shortcuts-manager-container',
|
||||
});
|
||||
|
||||
const onActionChanged = (e: Event) => {
|
||||
const $target = e.target as HTMLSelectElement;
|
||||
|
||||
// const profile = $selectProfile.value;
|
||||
// const button: unknown = $target.dataset.button;
|
||||
const action = $target.value as ShortcutAction;
|
||||
|
||||
if (!PREF_CONTROLLER_FRIENDLY_UI) {
|
||||
const $fakeSelect = $target.previousElementSibling! as HTMLSelectElement;
|
||||
let fakeText = '---';
|
||||
if (action) {
|
||||
const $selectedOption = $target.options[$target.selectedIndex];
|
||||
const $optGroup = $selectedOption.parentElement as HTMLOptGroupElement;
|
||||
fakeText = $optGroup.label + ' ❯ ' + $selectedOption.text;
|
||||
}
|
||||
($fakeSelect.firstElementChild as HTMLOptionElement).text = fakeText;
|
||||
}
|
||||
|
||||
// Update preset
|
||||
if (!(e as any).ignoreOnChange) {
|
||||
this.updatePreset();
|
||||
}
|
||||
};
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.appendChild(CE('p', {class: 'bx-shortcut-note'},
|
||||
CE('span', {class: 'bx-prompt'}, PrompFont.HOME),
|
||||
': ' + t('controller-shortcuts-xbox-note'),
|
||||
));
|
||||
|
||||
for (const button of this.BUTTONS_ORDER) {
|
||||
const prompt = GamepadKeyName[button][1];
|
||||
|
||||
const $row = CE('div', {
|
||||
class: 'bx-shortcut-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
});
|
||||
const $label = CE('label', {class: 'bx-prompt'}, `${PrompFont.HOME}${prompt}`);
|
||||
const $div = CE('div', {class: 'bx-shortcut-actions'});
|
||||
|
||||
let $fakeSelect: HTMLSelectElement | null = null;
|
||||
if (!PREF_CONTROLLER_FRIENDLY_UI) {
|
||||
$fakeSelect = CE<HTMLSelectElement>('select', { autocomplete: 'off' },
|
||||
CE('option', {}, '---'),
|
||||
);
|
||||
|
||||
$div.appendChild($fakeSelect);
|
||||
}
|
||||
|
||||
const $select = BxSelectElement.create($baseSelect.cloneNode(true) as HTMLSelectElement);
|
||||
$select.dataset.button = button.toString();
|
||||
$select.classList.add('bx-full-width');
|
||||
$select.addEventListener('input', onActionChanged);
|
||||
|
||||
this.selectActions[button] = [$select, $fakeSelect];
|
||||
|
||||
$div.appendChild($select);
|
||||
setNearby($row, {
|
||||
focus: $select,
|
||||
});
|
||||
|
||||
$row.append($label, $div);
|
||||
fragment.appendChild($row);
|
||||
}
|
||||
|
||||
$content.appendChild(fragment);
|
||||
|
||||
this.$content = $content;
|
||||
}
|
||||
|
||||
protected switchPreset(id: number): void {
|
||||
const preset = this.allPresets.data[id];
|
||||
if (!preset) {
|
||||
this.currentPresetId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentPresetId = id;
|
||||
const isDefaultPreset = id <= 0;
|
||||
const actions = preset.data;
|
||||
|
||||
// Reset selects' values
|
||||
let button: unknown;
|
||||
for (button in this.selectActions) {
|
||||
const [$select, $fakeSelect] = this.selectActions[button as GamepadKey]!;
|
||||
$select.value = actions.mapping[button as GamepadKey] || '';
|
||||
$select.disabled = isDefaultPreset;
|
||||
$fakeSelect && ($fakeSelect.disabled = isDefaultPreset);
|
||||
|
||||
BxEvent.dispatch($select, 'input', {
|
||||
ignoreOnChange: true,
|
||||
manualTrigger: true,
|
||||
});
|
||||
}
|
||||
|
||||
super.updateButtonStates();
|
||||
}
|
||||
|
||||
private updatePreset() {
|
||||
const newData: ControllerShortcutPresetData = deepClone(this.BLANK_PRESET_DATA);
|
||||
|
||||
let button: unknown;
|
||||
for (button in this.selectActions) {
|
||||
const [$select, _] = this.selectActions[button as GamepadKey]!;
|
||||
|
||||
const action = $select.value;
|
||||
if (!action) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newData.mapping[button as GamepadKey] = action as ShortcutAction;
|
||||
}
|
||||
|
||||
const preset = this.allPresets.data[this.currentPresetId];
|
||||
preset.data = newData;
|
||||
this.presetsDb.updatePreset(preset);
|
||||
|
||||
StreamSettings.refreshControllerSettings();
|
||||
}
|
||||
}
|
151
src/modules/ui/dialog/profile-manger/keyboard-shortcuts-manager-dialog.ts
Executable file
151
src/modules/ui/dialog/profile-manger/keyboard-shortcuts-manager-dialog.ts
Executable file
@@ -0,0 +1,151 @@
|
||||
import { t } from "@/utils/translation";
|
||||
import { BaseProfileManagerDialog } from "./base-profile-manager-dialog";
|
||||
import type { KeyboardShortcutPresetData, KeyboardShortcutPresetRecord } from "@/types/presets";
|
||||
import { CE, createSettingRow } from "@/utils/html";
|
||||
import { KeyboardShortcutDefaultId, KeyboardShortcutsTable } from "@/utils/local-db/keyboard-shortcuts-table";
|
||||
import { SHORTCUT_ACTIONS } from "@/modules/shortcuts/shortcut-actions";
|
||||
import { BxKeyBindingButton, BxKeyBindingButtonFlag } from "@/web-components/bx-key-binding-button";
|
||||
import type { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { deepClone } from "@/utils/global";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
|
||||
type KeyboardShortcutButtonDataset = {
|
||||
action: ShortcutAction,
|
||||
}
|
||||
|
||||
export class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog<KeyboardShortcutPresetRecord> {
|
||||
private static instance: KeyboardShortcutsManagerDialog;
|
||||
public static getInstance = () => KeyboardShortcutsManagerDialog.instance ?? (KeyboardShortcutsManagerDialog.instance = new KeyboardShortcutsManagerDialog(t('keyboard-shortcuts')));
|
||||
// private readonly LOG_TAG = 'KeyboardShortcutsManagerDialog';
|
||||
|
||||
protected $content: HTMLElement;
|
||||
private readonly allKeyElements: BxKeyBindingButton[] = [];
|
||||
|
||||
protected readonly BLANK_PRESET_DATA: KeyboardShortcutPresetData = {
|
||||
mapping: {},
|
||||
};
|
||||
|
||||
constructor(title: string) {
|
||||
super(title, KeyboardShortcutsTable.getInstance());
|
||||
|
||||
const $rows = CE('div', { class: 'bx-keyboard-shortcuts-manager-container' });
|
||||
|
||||
for (const groupLabel in SHORTCUT_ACTIONS) {
|
||||
const items = SHORTCUT_ACTIONS[groupLabel];
|
||||
if (!items) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $fieldSet = CE<HTMLFieldSetElement>('fieldset', {}, CE('legend', {}, groupLabel));
|
||||
for (const action in items) {
|
||||
const crumbs = items[action as keyof typeof items];
|
||||
if (!crumbs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = crumbs.join(' ❯ ');
|
||||
const $btn = BxKeyBindingButton.create({
|
||||
title: label,
|
||||
isPrompt: false,
|
||||
onChanged: this.onKeyChanged,
|
||||
|
||||
allowedFlags: [BxKeyBindingButtonFlag.KEYBOARD_PRESS, BxKeyBindingButtonFlag.KEYBOARD_MODIFIER],
|
||||
});
|
||||
$btn.classList.add('bx-full-width');
|
||||
$btn.dataset.action = action;
|
||||
this.allKeyElements.push($btn);
|
||||
|
||||
const $row = createSettingRow(label, CE('div', { class: 'bx-binding-button-wrapper' }, $btn));
|
||||
$fieldSet.appendChild($row);
|
||||
}
|
||||
|
||||
// Don't append empty <fieldset>
|
||||
if ($fieldSet.childElementCount > 1) {
|
||||
$rows.appendChild($fieldSet);
|
||||
}
|
||||
}
|
||||
|
||||
this.$content = CE('div', {}, $rows);
|
||||
}
|
||||
|
||||
private onKeyChanged = (e: Event) => {
|
||||
const $current = e.target as BxKeyBindingButton;
|
||||
const keyInfo = $current.keyInfo;
|
||||
|
||||
// Unbind duplicated keys
|
||||
if (keyInfo) {
|
||||
for (const $elm of this.allKeyElements) {
|
||||
if ($elm === $current) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($elm.keyInfo?.code === keyInfo.code && $elm.keyInfo?.modifiers === keyInfo.modifiers) {
|
||||
// Unbind manually
|
||||
$elm.unbindKey(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save preset
|
||||
this.savePreset();
|
||||
}
|
||||
|
||||
private parseDataset($btn: BxKeyBindingButton): KeyboardShortcutButtonDataset {
|
||||
const dataset = $btn.dataset;
|
||||
return {
|
||||
action: dataset.action as ShortcutAction,
|
||||
};
|
||||
}
|
||||
|
||||
protected switchPreset(id: number): void {
|
||||
const preset = this.allPresets.data[id];
|
||||
if (!preset) {
|
||||
this.currentPresetId = KeyboardShortcutDefaultId.OFF;
|
||||
return;
|
||||
}
|
||||
|
||||
const presetData = preset.data;
|
||||
this.currentPresetId = id;
|
||||
const isDefaultPreset = id <= 0;
|
||||
this.updateButtonStates();
|
||||
|
||||
// Update buttons
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const { action } = this.parseDataset($elm);
|
||||
|
||||
const keyInfo = presetData.mapping[action];
|
||||
if (keyInfo) {
|
||||
$elm.bindKey(keyInfo, true)
|
||||
} else {
|
||||
$elm.unbindKey(true);
|
||||
}
|
||||
|
||||
$elm.disabled = isDefaultPreset;
|
||||
}
|
||||
}
|
||||
|
||||
private savePreset() {
|
||||
const presetData = deepClone(this.BLANK_PRESET_DATA) as KeyboardShortcutPresetData;
|
||||
|
||||
// Get mapping
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const { action } = this.parseDataset($elm);
|
||||
|
||||
const mapping = presetData.mapping;
|
||||
if ($elm.keyInfo) {
|
||||
mapping[action] = $elm.keyInfo;
|
||||
}
|
||||
}
|
||||
|
||||
const oldPreset = this.allPresets.data[this.currentPresetId];
|
||||
const newPreset = {
|
||||
id: this.currentPresetId,
|
||||
name: oldPreset.name,
|
||||
data: presetData,
|
||||
};
|
||||
this.presetsDb.updatePreset(newPreset);
|
||||
|
||||
this.allPresets.data[this.currentPresetId] = newPreset;
|
||||
StreamSettings.refreshKeyboardShortcuts();
|
||||
}
|
||||
}
|
254
src/modules/ui/dialog/profile-manger/mkb-mapping-manager-dialog.ts
Executable file
254
src/modules/ui/dialog/profile-manger/mkb-mapping-manager-dialog.ts
Executable file
@@ -0,0 +1,254 @@
|
||||
import type { MkbPresetData, MkbPresetRecord } from "@/types/presets";
|
||||
import { BaseProfileManagerDialog } from "./base-profile-manager-dialog";
|
||||
import { t } from "@/utils/translation";
|
||||
import { MkbMappingPresetsTable } from "@/utils/local-db/mkb-mapping-presets-table";
|
||||
import { GamepadKey, GamepadKeyName } from "@/enums/gamepad";
|
||||
import { CE, createSettingRow } from "@/utils/html";
|
||||
import { MouseMapTo, MkbPresetKey, type KeyCode } from "@/enums/mkb";
|
||||
import { BxKeyBindingButton, BxKeyBindingButtonFlag } from "@/web-components/bx-key-binding-button";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { BxNumberStepper } from "@/web-components/bx-number-stepper";
|
||||
import { deepClone } from "@/utils/global";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
|
||||
type MkbButtonDataset = {
|
||||
keySlot: number,
|
||||
buttonIndex: GamepadKey,
|
||||
}
|
||||
|
||||
export class MkbMappingManagerDialog extends BaseProfileManagerDialog<MkbPresetRecord> {
|
||||
private static instance: MkbMappingManagerDialog;
|
||||
public static getInstance = () => MkbMappingManagerDialog.instance ?? (MkbMappingManagerDialog.instance = new MkbMappingManagerDialog(t('virtual-controller')));
|
||||
|
||||
declare protected $content: HTMLElement;
|
||||
|
||||
private readonly KEYS_PER_BUTTON = 2;
|
||||
private readonly BUTTONS_ORDER = [
|
||||
GamepadKey.HOME,
|
||||
GamepadKey.UP, GamepadKey.DOWN, GamepadKey.LEFT, GamepadKey.RIGHT,
|
||||
GamepadKey.A, GamepadKey.B, GamepadKey.X, GamepadKey.Y,
|
||||
GamepadKey.LB, GamepadKey.RB, GamepadKey.LT, GamepadKey.RT,
|
||||
GamepadKey.SELECT, GamepadKey.START,
|
||||
GamepadKey.L3, GamepadKey.LS_UP, GamepadKey.LS_DOWN, GamepadKey.LS_LEFT, GamepadKey.LS_RIGHT,
|
||||
GamepadKey.R3, GamepadKey.RS_UP, GamepadKey.RS_DOWN, GamepadKey.RS_LEFT, GamepadKey.RS_RIGHT,
|
||||
];
|
||||
|
||||
protected readonly BLANK_PRESET_DATA: MkbPresetData = {
|
||||
mapping: {},
|
||||
mouse: {
|
||||
[MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo.RS,
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_X]: 100,
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 100,
|
||||
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20,
|
||||
},
|
||||
};
|
||||
|
||||
private readonly allKeyElements: BxKeyBindingButton[] = [];
|
||||
private $mouseMapTo!: BxSelectElement;
|
||||
private $mouseSensitivityX!: BxNumberStepper;
|
||||
private $mouseSensitivityY!: BxNumberStepper;
|
||||
private $mouseDeadzone!: BxNumberStepper;
|
||||
|
||||
constructor(title: string) {
|
||||
super(title, MkbMappingPresetsTable.getInstance());
|
||||
this.render();
|
||||
}
|
||||
|
||||
private onBindingKey = (e: MouseEvent) => {
|
||||
const $btn = e.target as HTMLButtonElement;
|
||||
if ($btn.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
private parseDataset($btn: BxKeyBindingButton): MkbButtonDataset {
|
||||
const dataset = $btn.dataset;
|
||||
return {
|
||||
keySlot: parseInt(dataset.keySlot!),
|
||||
buttonIndex: parseInt(dataset.buttonIndex!),
|
||||
};
|
||||
}
|
||||
|
||||
private onKeyChanged = (e: Event) => {
|
||||
const $current = e.target as BxKeyBindingButton;
|
||||
const keyInfo = $current.keyInfo;
|
||||
|
||||
// Unbind duplicated keys
|
||||
if (keyInfo) {
|
||||
for (const $elm of this.allKeyElements) {
|
||||
if ($elm !== $current && $elm.keyInfo?.code === keyInfo.code) {
|
||||
// Unbind manually
|
||||
$elm.unbindKey(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save preset
|
||||
this.savePreset();
|
||||
}
|
||||
|
||||
private render() {
|
||||
const $rows = CE('div', {},
|
||||
CE('i', { class: 'bx-mkb-note' }, t('right-click-to-unbind')),
|
||||
);
|
||||
|
||||
for (const buttonIndex of this.BUTTONS_ORDER) {
|
||||
const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex];
|
||||
|
||||
let $elm;
|
||||
const $fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < this.KEYS_PER_BUTTON; i++) {
|
||||
$elm = BxKeyBindingButton.create({
|
||||
title: buttonPrompt,
|
||||
isPrompt: true,
|
||||
allowedFlags: [BxKeyBindingButtonFlag.KEYBOARD_PRESS, BxKeyBindingButtonFlag.MOUSE_CLICK, BxKeyBindingButtonFlag.MOUSE_WHEEL],
|
||||
onChanged: this.onKeyChanged,
|
||||
});
|
||||
|
||||
$elm.dataset.buttonIndex = buttonIndex.toString();
|
||||
$elm.dataset.keySlot = i.toString();
|
||||
|
||||
$elm.addEventListener('mouseup', this.onBindingKey);
|
||||
|
||||
$fragment.appendChild($elm);
|
||||
this.allKeyElements.push($elm);
|
||||
}
|
||||
|
||||
const $keyRow = CE('div', {
|
||||
class: 'bx-mkb-key-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
},
|
||||
CE('label', { title: buttonName }, buttonPrompt),
|
||||
$fragment,
|
||||
);
|
||||
|
||||
$rows.appendChild($keyRow);
|
||||
}
|
||||
|
||||
const savePreset = () => this.savePreset();
|
||||
const $extraSettings = CE('div', {},
|
||||
createSettingRow(
|
||||
t('map-mouse-to'),
|
||||
this.$mouseMapTo = BxSelectElement.create(CE('select', { _on: { input: savePreset } },
|
||||
CE('option', { value: MouseMapTo.RS }, t('right-stick')),
|
||||
CE('option', { value: MouseMapTo.LS }, t('left-stick')),
|
||||
CE('option', { value: MouseMapTo.OFF }, t('off')),
|
||||
)),
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('horizontal-sensitivity'),
|
||||
this.$mouseSensitivityX = BxNumberStepper.create('hor_sensitivity', 0, 1, 300, {
|
||||
suffix: '%',
|
||||
exactTicks: 50,
|
||||
}, savePreset),
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('vertical-sensitivity'),
|
||||
this.$mouseSensitivityY = BxNumberStepper.create('ver_sensitivity', 0, 1, 300, {
|
||||
suffix: '%',
|
||||
exactTicks: 50,
|
||||
}, savePreset),
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('deadzone-counterweight'),
|
||||
this.$mouseDeadzone = BxNumberStepper.create('deadzone_counterweight', 0, 1, 50, {
|
||||
suffix: '%',
|
||||
exactTicks: 10,
|
||||
}, savePreset),
|
||||
),
|
||||
);
|
||||
|
||||
this.$content = CE('div', {},
|
||||
$rows,
|
||||
$extraSettings,
|
||||
);
|
||||
}
|
||||
|
||||
protected switchPreset(id: number): void {
|
||||
const preset = this.allPresets.data[id];
|
||||
if (!preset) {
|
||||
this.currentPresetId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const presetData = preset.data;
|
||||
this.currentPresetId = id;
|
||||
const isDefaultPreset = id <= 0;
|
||||
this.updateButtonStates();
|
||||
|
||||
// Update buttons
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const { buttonIndex, keySlot } = this.parseDataset($elm);
|
||||
|
||||
const buttonKeys = presetData.mapping[buttonIndex];
|
||||
if (buttonKeys && buttonKeys[keySlot]) {
|
||||
$elm.bindKey({
|
||||
code: buttonKeys[keySlot],
|
||||
}, true)
|
||||
} else {
|
||||
$elm.unbindKey(true);
|
||||
}
|
||||
|
||||
$elm.disabled = isDefaultPreset;
|
||||
}
|
||||
|
||||
// Update mouse settings
|
||||
const mouse = presetData.mouse;
|
||||
this.$mouseMapTo.value = mouse.mapTo.toString();
|
||||
this.$mouseSensitivityX.value = mouse.sensitivityX.toString();
|
||||
this.$mouseSensitivityY.value = mouse.sensitivityY.toString();
|
||||
this.$mouseDeadzone.value = mouse.deadzoneCounterweight.toString();
|
||||
|
||||
this.$mouseMapTo.disabled = isDefaultPreset;
|
||||
this.$mouseSensitivityX.dataset.disabled = isDefaultPreset.toString();
|
||||
this.$mouseSensitivityY.dataset.disabled = isDefaultPreset.toString();
|
||||
this.$mouseDeadzone.dataset.disabled = isDefaultPreset.toString();
|
||||
}
|
||||
|
||||
private savePreset() {
|
||||
const presetData = deepClone(this.BLANK_PRESET_DATA) as MkbPresetData;
|
||||
|
||||
// Get mapping
|
||||
for (const $elm of this.allKeyElements) {
|
||||
const { buttonIndex, keySlot } = this.parseDataset($elm);
|
||||
const mapping = presetData.mapping;
|
||||
if (!mapping[buttonIndex]) {
|
||||
mapping[buttonIndex] = [];
|
||||
}
|
||||
|
||||
if (!$elm.keyInfo) {
|
||||
// Remove empty key from mapping
|
||||
delete mapping[buttonIndex][keySlot];
|
||||
} else {
|
||||
mapping[buttonIndex][keySlot] = $elm.keyInfo.code as KeyCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Get mouse settings
|
||||
const mouse = presetData.mouse;
|
||||
mouse.mapTo = parseInt(this.$mouseMapTo.value) as MouseMapTo;
|
||||
mouse.sensitivityX = parseInt(this.$mouseSensitivityX.value);
|
||||
mouse.sensitivityY = parseInt(this.$mouseSensitivityY.value);
|
||||
mouse.deadzoneCounterweight = parseInt(this.$mouseDeadzone.value);
|
||||
|
||||
const oldPreset = this.allPresets.data[this.currentPresetId];
|
||||
const newPreset = {
|
||||
id: this.currentPresetId,
|
||||
name: oldPreset.name,
|
||||
data: presetData,
|
||||
};
|
||||
this.presetsDb.updatePreset(newPreset);
|
||||
|
||||
this.allPresets.data[this.currentPresetId] = newPreset;
|
||||
StreamSettings.refreshMkbSettings();
|
||||
}
|
||||
}
|
22
src/modules/ui/dialog/remote-play-dialog.ts
Normal file → Executable file
22
src/modules/ui/dialog/remote-play-dialog.ts
Normal file → Executable file
@@ -8,11 +8,12 @@ import { RemotePlayConsoleState, RemotePlayManager } from "@/modules/remote-play
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { BxLogger } from "@/utils/bx-logger";
|
||||
import { StreamResolution } from "@/enums/pref-values";
|
||||
|
||||
|
||||
export class RemotePlayNavigationDialog extends NavigationDialog {
|
||||
private static instance: RemotePlayNavigationDialog;
|
||||
public static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog());
|
||||
export class RemotePlayDialog extends NavigationDialog {
|
||||
private static instance: RemotePlayDialog;
|
||||
public static getInstance = () => RemotePlayDialog.instance ?? (RemotePlayDialog.instance = new RemotePlayDialog());
|
||||
private readonly LOG_TAG = 'RemotePlayNavigationDialog';
|
||||
|
||||
private readonly STATE_LABELS: Record<RemotePlayConsoleState, string> = {
|
||||
@@ -35,21 +36,18 @@ export class RemotePlayNavigationDialog extends NavigationDialog {
|
||||
|
||||
const $settingNote = CE('p', {});
|
||||
|
||||
const currentResolution = getPref(PrefKey.REMOTE_PLAY_RESOLUTION);
|
||||
const currentResolution = getPref(PrefKey.REMOTE_PLAY_STREAM_RESOLUTION);
|
||||
let $resolutions : HTMLSelectElement | NavigationElement = CE<HTMLSelectElement>('select', {},
|
||||
CE('option', {value: '1080p'}, '1080p'),
|
||||
CE('option', {value: '720p'}, '720p'),
|
||||
CE('option', { value: StreamResolution.DIM_720P }, '720p'),
|
||||
CE('option', { value: StreamResolution.DIM_1080P }, '1080p'),
|
||||
);
|
||||
|
||||
if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
|
||||
$resolutions = BxSelectElement.wrap($resolutions as HTMLSelectElement);
|
||||
}
|
||||
|
||||
$resolutions = BxSelectElement.create($resolutions as HTMLSelectElement);
|
||||
$resolutions.addEventListener('input', (e: Event) => {
|
||||
const value = (e.target as HTMLSelectElement).value;
|
||||
|
||||
$settingNote.textContent = value === '1080p' ? '✅ ' + t('can-stream-xbox-360-games') : '❌ ' + t('cant-stream-xbox-360-games');
|
||||
setPref(PrefKey.REMOTE_PLAY_RESOLUTION, value);
|
||||
setPref(PrefKey.REMOTE_PLAY_STREAM_RESOLUTION, value);
|
||||
});
|
||||
|
||||
($resolutions as any).value = currentResolution;
|
||||
@@ -67,7 +65,7 @@ export class RemotePlayNavigationDialog extends NavigationDialog {
|
||||
$fragment.appendChild($qualitySettings);
|
||||
|
||||
// Render consoles list
|
||||
const manager = RemotePlayManager.getInstance();
|
||||
const manager = RemotePlayManager.getInstance()!;
|
||||
const consoles = manager.getConsoles();
|
||||
|
||||
for (let con of consoles) {
|
||||
|
842
src/modules/ui/dialog/settings-dialog.ts
Normal file → Executable file
842
src/modules/ui/dialog/settings-dialog.ts
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
201
src/modules/ui/dialog/settings/controller-extra.ts
Executable file
201
src/modules/ui/dialog/settings/controller-extra.ts
Executable file
@@ -0,0 +1,201 @@
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { getUniqueGamepadNames } from "@/utils/gamepad";
|
||||
import { CE, removeChildElements, createButton, ButtonStyle, createSettingRow, renderPresetsList } from "@/utils/html";
|
||||
import { t } from "@/utils/translation";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import { ControllerShortcutsManagerDialog } from "../profile-manger/controller-shortcuts-manager-dialog";
|
||||
import type { SettingsDialog } from "../settings-dialog";
|
||||
import { ControllerShortcutsTable } from "@/utils/local-db/controller-shortcuts-table";
|
||||
import { BxNumberStepper } from "@/web-components/bx-number-stepper";
|
||||
import { ControllerSettingsTable } from "@/utils/local-db/controller-settings-table";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
|
||||
export class ControllerExtraSettings extends HTMLElement {
|
||||
currentControllerId!: string;
|
||||
controllerIds!: string[];
|
||||
|
||||
$selectControllers!: BxSelectElement;
|
||||
$selectShortcuts!: BxSelectElement;
|
||||
$vibrationIntensity!: BxNumberStepper;
|
||||
|
||||
updateLayout!: () => void;
|
||||
switchController!: (id: string) => void;
|
||||
getCurrentControllerId!: () => string | null;
|
||||
saveSettings!: () => void;
|
||||
|
||||
static renderSettings(this: SettingsDialog): HTMLElement {
|
||||
const $container = CE<ControllerExtraSettings>('label', {
|
||||
class: 'bx-settings-row bx-controller-extra-settings',
|
||||
});
|
||||
|
||||
$container.updateLayout = ControllerExtraSettings.updateLayout.bind($container);
|
||||
$container.switchController = ControllerExtraSettings.switchController.bind($container);
|
||||
$container.getCurrentControllerId = ControllerExtraSettings.getCurrentControllerId.bind($container);
|
||||
$container.saveSettings = ControllerExtraSettings.saveSettings.bind($container);
|
||||
|
||||
const $selectControllers = BxSelectElement.create(CE<HTMLSelectElement>('select', {
|
||||
autocomplete: 'off',
|
||||
_on: {
|
||||
input: (e: Event) => {
|
||||
$container.switchController($selectControllers.value);
|
||||
},
|
||||
},
|
||||
}));
|
||||
$selectControllers.classList.add('bx-full-width');
|
||||
|
||||
const $selectShortcuts = BxSelectElement.create(CE<HTMLSelectElement>('select', {
|
||||
autocomplete: 'off',
|
||||
_on: {
|
||||
input: $container.saveSettings,
|
||||
},
|
||||
}));
|
||||
|
||||
const $vibrationIntensity = BxNumberStepper.create('controller_vibration_intensity', 50, 0, 100, {
|
||||
steps: 10,
|
||||
suffix: '%',
|
||||
exactTicks: 20,
|
||||
customTextValue: (value: any) => {
|
||||
value = parseInt(value);
|
||||
return value === 0 ? t('off') : value + '%';
|
||||
},
|
||||
}, $container.saveSettings);
|
||||
|
||||
$container.append(
|
||||
CE('span', {}, t('no-controllers-connected')),
|
||||
CE('div', { class: 'bx-controller-extra-wrapper' },
|
||||
$selectControllers,
|
||||
|
||||
CE('div', {class: 'bx-sub-content-box'},
|
||||
createSettingRow(
|
||||
t('controller-shortcuts-in-game'),
|
||||
CE('div', {
|
||||
class: 'bx-preset-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
},
|
||||
$selectShortcuts,
|
||||
createButton({
|
||||
label: t('manage'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: () => ControllerShortcutsManagerDialog.getInstance().show({
|
||||
id: parseInt($container.$selectShortcuts.value),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
{ multiLines: true },
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('vibration-intensity'),
|
||||
$vibrationIntensity,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$container.$selectControllers = $selectControllers;
|
||||
$container.$selectShortcuts = $selectShortcuts;
|
||||
$container.$vibrationIntensity = $vibrationIntensity;
|
||||
|
||||
$container.updateLayout();
|
||||
|
||||
// Detect when gamepad connected/disconnect
|
||||
window.addEventListener('gamepadconnected', $container.updateLayout);
|
||||
window.addEventListener('gamepaddisconnected', $container.updateLayout);
|
||||
|
||||
// Refresh layout when parent dialog is shown
|
||||
this.onMountedCallbacks.push(() => {
|
||||
$container.updateLayout();
|
||||
});
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
private static async updateLayout(this: ControllerExtraSettings, e?: GamepadEvent) {
|
||||
this.controllerIds = getUniqueGamepadNames();
|
||||
|
||||
this.dataset.hasGamepad = (this.controllerIds.length > 0).toString();
|
||||
if (this.controllerIds.length === 0) {
|
||||
// No gamepads
|
||||
return;
|
||||
}
|
||||
|
||||
const $fragment = document.createDocumentFragment();
|
||||
|
||||
// Remove old controllers
|
||||
removeChildElements(this.$selectControllers);
|
||||
|
||||
// Render controller list
|
||||
for (const name of this.controllerIds) {
|
||||
const $option = CE<HTMLOptionElement>('option', { value: name }, name);
|
||||
$fragment.appendChild($option);
|
||||
}
|
||||
|
||||
this.$selectControllers.appendChild($fragment);
|
||||
|
||||
// Render shortcut presets
|
||||
const allShortcutPresets = await ControllerShortcutsTable.getInstance().getPresets();
|
||||
renderPresetsList(this.$selectShortcuts, allShortcutPresets, null, true);
|
||||
|
||||
for (const name of this.controllerIds) {
|
||||
const $option = CE<HTMLOptionElement>('option', { value: name }, name);
|
||||
$fragment.appendChild($option);
|
||||
}
|
||||
|
||||
BxEvent.dispatch(this.$selectControllers, 'input');
|
||||
}
|
||||
|
||||
private static async switchController(this: ControllerExtraSettings, id: string) {
|
||||
this.currentControllerId = id;
|
||||
if (!this.getCurrentControllerId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const controllerSettings = await ControllerSettingsTable.getInstance().getControllerData(this.currentControllerId);
|
||||
|
||||
// Update UI
|
||||
this.$selectShortcuts.value = controllerSettings.shortcutPresetId.toString();
|
||||
this.$vibrationIntensity.value = controllerSettings.vibrationIntensity.toString();
|
||||
}
|
||||
|
||||
private static getCurrentControllerId(this: ControllerExtraSettings) {
|
||||
// Validate current ID
|
||||
if (this.currentControllerId) {
|
||||
if (this.controllerIds.includes(this.currentControllerId)) {
|
||||
return this.currentControllerId;
|
||||
}
|
||||
|
||||
this.currentControllerId = '';
|
||||
}
|
||||
|
||||
// Get first ID
|
||||
if (!this.currentControllerId) {
|
||||
this.currentControllerId = this.controllerIds[0];
|
||||
}
|
||||
|
||||
if (this.currentControllerId) {
|
||||
return this.currentControllerId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async saveSettings(this: ControllerExtraSettings) {
|
||||
if (!this.getCurrentControllerId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data: ControllerSettingsRecord = {
|
||||
id: this.currentControllerId,
|
||||
data: {
|
||||
shortcutPresetId: parseInt(this.$selectShortcuts.value),
|
||||
vibrationIntensity: parseInt(this.$vibrationIntensity.value),
|
||||
},
|
||||
};
|
||||
|
||||
await ControllerSettingsTable.getInstance().put(data);
|
||||
|
||||
StreamSettings.refreshControllerSettings();
|
||||
}
|
||||
}
|
131
src/modules/ui/dialog/settings/mkb-extra.ts
Executable file
131
src/modules/ui/dialog/settings/mkb-extra.ts
Executable file
@@ -0,0 +1,131 @@
|
||||
import { ButtonStyle, CE, createButton, createSettingRow, renderPresetsList } from "@/utils/html";
|
||||
import type { SettingsDialog } from "../settings-dialog";
|
||||
import { MkbMappingPresetsTable } from "@/utils/local-db/mkb-mapping-presets-table";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import { t } from "@/utils/translation";
|
||||
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { StreamSettings } from "@/utils/stream-settings";
|
||||
import { MkbMappingManagerDialog } from "../profile-manger/mkb-mapping-manager-dialog";
|
||||
import { KeyboardShortcutsManagerDialog } from "../profile-manger/keyboard-shortcuts-manager-dialog";
|
||||
import { KeyboardShortcutsTable } from "@/utils/local-db/keyboard-shortcuts-table";
|
||||
import { SettingElement } from "@/utils/setting-element";
|
||||
import { STORAGE } from "@/utils/global";
|
||||
import { EmulatedMkbHandler } from "@/modules/mkb/mkb-handler";
|
||||
|
||||
export class MkbExtraSettings extends HTMLElement {
|
||||
private $mappingPresets!: BxSelectElement;
|
||||
private $shortcutsPresets!: BxSelectElement;
|
||||
|
||||
private updateLayout!: typeof MkbExtraSettings['updateLayout'];
|
||||
private saveMkbSettings!: typeof MkbExtraSettings['saveMkbSettings'];
|
||||
private saveShortcutsSettings!: typeof MkbExtraSettings['saveShortcutsSettings'];
|
||||
|
||||
static renderSettings(this: SettingsDialog): HTMLElement {
|
||||
const $container = document.createDocumentFragment() as unknown as MkbExtraSettings;
|
||||
|
||||
$container.updateLayout = MkbExtraSettings.updateLayout.bind($container);
|
||||
$container.saveMkbSettings = MkbExtraSettings.saveMkbSettings.bind($container);
|
||||
$container.saveShortcutsSettings = MkbExtraSettings.saveShortcutsSettings.bind($container);
|
||||
|
||||
const $mappingPresets = BxSelectElement.create(CE<HTMLSelectElement>('select', {
|
||||
autocomplete: 'off',
|
||||
_on: {
|
||||
input: $container.saveMkbSettings,
|
||||
},
|
||||
}));
|
||||
|
||||
const $shortcutsPresets = BxSelectElement.create(CE<HTMLSelectElement>('select', {
|
||||
autocomplete: 'off',
|
||||
_on: {
|
||||
input: $container.saveShortcutsSettings,
|
||||
},
|
||||
}));
|
||||
|
||||
$container.append(
|
||||
...(getPref(PrefKey.MKB_ENABLED) ? [
|
||||
createSettingRow(
|
||||
t('virtual-controller'),
|
||||
CE('div', {
|
||||
class: 'bx-preset-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
},
|
||||
$mappingPresets,
|
||||
createButton({
|
||||
label: t('manage'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: () => MkbMappingManagerDialog.getInstance().show({
|
||||
id: parseInt($container.$mappingPresets.value),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
{ multiLines: true },
|
||||
),
|
||||
|
||||
createSettingRow(
|
||||
t('virtual-controller-slot'),
|
||||
SettingElement.fromPref(PrefKey.MKB_P1_SLOT, STORAGE.Global, () => {
|
||||
EmulatedMkbHandler.getInstance()?.updateGamepadSlots();
|
||||
}),
|
||||
),
|
||||
] : []),
|
||||
|
||||
createSettingRow(
|
||||
t('keyboard-shortcuts-in-game'),
|
||||
CE('div', {
|
||||
class: 'bx-preset-row',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
},
|
||||
$shortcutsPresets,
|
||||
createButton({
|
||||
label: t('manage'),
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: () => KeyboardShortcutsManagerDialog.getInstance().show({
|
||||
id: parseInt($container.$shortcutsPresets.value),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
{ multiLines: true },
|
||||
),
|
||||
);
|
||||
|
||||
$container.$mappingPresets = $mappingPresets;
|
||||
$container.$shortcutsPresets = $shortcutsPresets;
|
||||
|
||||
$container.updateLayout();
|
||||
// Refresh layout when parent dialog is shown
|
||||
this.onMountedCallbacks.push(() => {
|
||||
$container.updateLayout();
|
||||
});
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
private static async updateLayout(this: MkbExtraSettings) {
|
||||
// Render shortcut presets
|
||||
const mappingPresets = await MkbMappingPresetsTable.getInstance().getPresets();
|
||||
renderPresetsList(this.$mappingPresets, mappingPresets, getPref<MkbPresetId>(PrefKey.MKB_P1_MAPPING_PRESET_ID), false);
|
||||
|
||||
// Render shortcut presets
|
||||
const shortcutsPresets = await KeyboardShortcutsTable.getInstance().getPresets();
|
||||
renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getPref<MkbPresetId>(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID), true);
|
||||
}
|
||||
|
||||
private static async saveMkbSettings(this: MkbExtraSettings) {
|
||||
const presetId = parseInt(this.$mappingPresets.value);
|
||||
setPref<MkbPresetId>(PrefKey.MKB_P1_MAPPING_PRESET_ID, presetId);
|
||||
|
||||
StreamSettings.refreshMkbSettings();
|
||||
}
|
||||
|
||||
private static async saveShortcutsSettings(this: MkbExtraSettings) {
|
||||
const presetId = parseInt(this.$shortcutsPresets.value);
|
||||
setPref<KeyboardShortcutsPresetId>(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID, presetId);
|
||||
|
||||
StreamSettings.refreshKeyboardShortcuts();
|
||||
}
|
||||
}
|
337
src/modules/ui/dialog/settings/suggestions.ts
Executable file
337
src/modules/ui/dialog/settings/suggestions.ts
Executable file
@@ -0,0 +1,337 @@
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { BX_FLAGS, NATIVE_FETCH, type BxFlags } from "@/utils/bx-flags";
|
||||
import { STORAGE } from "@/utils/global";
|
||||
import { CE, removeChildElements, createButton, ButtonStyle, escapeCssSelector } from "@/utils/html";
|
||||
import type { BxHtmlSettingElement } from "@/utils/setting-element";
|
||||
import { getPref, setPref, getPrefDefinition } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { t } from "@/utils/translation";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import type { SettingsDialog } from "../settings-dialog";
|
||||
import type { RecommendedSettings, SuggestedSettingProfile } from "@/types/setting-definition";
|
||||
import { DeviceVibrationMode, TouchControllerMode } from "@/enums/pref-values";
|
||||
import { GhPagesUtils } from "@/utils/gh-pages";
|
||||
|
||||
export class SuggestionsSetting {
|
||||
static async renderSuggestions(this: SettingsDialog, e: Event) {
|
||||
const $btnSuggest = (e.target as HTMLElement).closest('div')!;
|
||||
$btnSuggest.toggleAttribute('bx-open');
|
||||
|
||||
let $content = $btnSuggest.nextElementSibling as HTMLElement;
|
||||
if ($content) {
|
||||
BxEvent.dispatch($content.querySelector('select'), 'input');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get labels
|
||||
let settingTabGroup: keyof typeof this.SETTINGS_UI;
|
||||
for (settingTabGroup in this.SETTINGS_UI) {
|
||||
const settingTab = this.SETTINGS_UI[settingTabGroup];
|
||||
|
||||
if (!settingTab || !settingTab.items || typeof settingTab.items === 'function') {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const settingTabContent of settingTab.items) {
|
||||
if (!settingTabContent || settingTabContent instanceof HTMLElement || !settingTabContent.items) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const setting of settingTabContent.items) {
|
||||
let prefKey: PrefKey | undefined;
|
||||
|
||||
if (typeof setting === 'string') {
|
||||
prefKey = setting;
|
||||
} else if (typeof setting === 'object') {
|
||||
prefKey = setting.pref as PrefKey;
|
||||
}
|
||||
|
||||
if (prefKey) {
|
||||
this.suggestedSettingLabels[prefKey] = settingTabContent.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get recommended settings for Android devices
|
||||
let recommendedDevice: string | null = '';
|
||||
|
||||
if (BX_FLAGS.DeviceInfo.deviceType.includes('android')) {
|
||||
if (BX_FLAGS.DeviceInfo.androidInfo) {
|
||||
recommendedDevice = await SuggestionsSetting.getRecommendedSettings.call(this, BX_FLAGS.DeviceInfo.androidInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
recommendedDevice = await this.getRecommendedSettings({
|
||||
manufacturer: 'Lenovo',
|
||||
board: 'kona',
|
||||
model: 'Lenovo TB-9707F',
|
||||
});
|
||||
*/
|
||||
|
||||
const hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0;
|
||||
|
||||
// Add some specific setings based on device type
|
||||
const deviceType = BX_FLAGS.DeviceInfo.deviceType;
|
||||
if (deviceType === 'android-handheld') {
|
||||
// Disable touch
|
||||
SuggestionsSetting.addDefaultSuggestedSetting.call(this, PrefKey.TOUCH_CONTROLLER_MODE, TouchControllerMode.OFF);
|
||||
// Enable device vibration
|
||||
SuggestionsSetting.addDefaultSuggestedSetting.call(this, PrefKey.DEVICE_VIBRATION_MODE, DeviceVibrationMode.ON);
|
||||
} else if (deviceType === 'android') {
|
||||
// Enable device vibration
|
||||
SuggestionsSetting.addDefaultSuggestedSetting.call(this, PrefKey.DEVICE_VIBRATION_MODE, DeviceVibrationMode.AUTO);
|
||||
} else if (deviceType === 'android-tv') {
|
||||
// Disable touch
|
||||
SuggestionsSetting.addDefaultSuggestedSetting.call(this, PrefKey.TOUCH_CONTROLLER_MODE, TouchControllerMode.OFF);
|
||||
}
|
||||
|
||||
// Set value for Default profile
|
||||
SuggestionsSetting.generateDefaultSuggestedSettings.call(this);
|
||||
|
||||
// Start rendering
|
||||
const $suggestedSettings = CE('div', {class: 'bx-suggest-wrapper'});
|
||||
const $select = CE<HTMLSelectElement>('select', {},
|
||||
hasRecommendedSettings && CE('option', {value: 'recommended'}, t('recommended')),
|
||||
!hasRecommendedSettings && CE('option', {value: 'highest'}, t('highest-quality')),
|
||||
CE('option', {value: 'default'}, t('default')),
|
||||
CE('option', {value: 'lowest'}, t('lowest-quality')),
|
||||
);
|
||||
$select.addEventListener('input', e => {
|
||||
const profile = $select.value as SuggestedSettingProfile;
|
||||
|
||||
// Empty children
|
||||
removeChildElements($suggestedSettings);
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
let note: HTMLElement | string | undefined;
|
||||
if (profile === 'recommended') {
|
||||
note = t('recommended-settings-for-device', {device: recommendedDevice});
|
||||
} else if (profile === 'highest') {
|
||||
// Add note for "Highest quality" profile
|
||||
note = '⚠️ ' + t('highest-quality-note');
|
||||
}
|
||||
|
||||
note && fragment.appendChild(CE('div', {class: 'bx-suggest-note'}, note));
|
||||
|
||||
const settings = this.suggestedSettings[profile];
|
||||
let prefKey: PrefKey;
|
||||
for (prefKey in settings) {
|
||||
let suggestedValue;
|
||||
const definition = getPrefDefinition(prefKey);
|
||||
if (definition && definition.transformValue) {
|
||||
suggestedValue = definition.transformValue.get.call(definition, settings[prefKey]);
|
||||
} else {
|
||||
suggestedValue = settings[prefKey];
|
||||
}
|
||||
|
||||
const currentValue = getPref(prefKey, false);
|
||||
const currentValueText = STORAGE.Global.getValueText(prefKey, currentValue);
|
||||
const isSameValue = currentValue === suggestedValue;
|
||||
|
||||
let $child: HTMLElement;
|
||||
let $value: HTMLElement | string;
|
||||
if (isSameValue) {
|
||||
// No changes
|
||||
$value = currentValueText;
|
||||
} else {
|
||||
const suggestedValueText = STORAGE.Global.getValueText(prefKey, suggestedValue);
|
||||
$value = currentValueText + ' ➔ ' + suggestedValueText;
|
||||
}
|
||||
|
||||
let $checkbox: HTMLInputElement;
|
||||
const breadcrumb = this.suggestedSettingLabels[prefKey] + ' ❯ ' + STORAGE.Global.getLabel(prefKey);
|
||||
const id = escapeCssSelector(`bx_suggest_${prefKey}`);
|
||||
|
||||
$child = CE('div', {
|
||||
class: `bx-suggest-row ${isSameValue ? 'bx-suggest-ok' : 'bx-suggest-change'}`,
|
||||
},
|
||||
$checkbox = CE('input', {
|
||||
type: 'checkbox',
|
||||
tabindex: 0,
|
||||
checked: true,
|
||||
id: id,
|
||||
}),
|
||||
CE('label', {
|
||||
for: id,
|
||||
},
|
||||
CE('div', {
|
||||
class: 'bx-suggest-label',
|
||||
}, breadcrumb),
|
||||
CE('div', {
|
||||
class: 'bx-suggest-value',
|
||||
}, $value),
|
||||
),
|
||||
);
|
||||
|
||||
if (isSameValue) {
|
||||
$checkbox.disabled = true;
|
||||
$checkbox.checked = true;
|
||||
}
|
||||
|
||||
fragment.appendChild($child);
|
||||
}
|
||||
|
||||
$suggestedSettings.appendChild(fragment);
|
||||
});
|
||||
|
||||
BxEvent.dispatch($select, 'input');
|
||||
|
||||
const onClickApply = () => {
|
||||
const profile = $select.value as SuggestedSettingProfile;
|
||||
const settings = this.suggestedSettings[profile];
|
||||
|
||||
let prefKey: PrefKey;
|
||||
for (prefKey in settings) {
|
||||
let suggestedValue = settings[prefKey];
|
||||
|
||||
const $checkBox = $content.querySelector<HTMLInputElement>(`#bx_suggest_${escapeCssSelector(prefKey)}`)!;
|
||||
if (!$checkBox.checked || $checkBox.disabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $control = this.settingElements[prefKey] as HTMLElement;
|
||||
|
||||
// Set value directly if the control element is not available
|
||||
if (!$control) {
|
||||
setPref(prefKey, suggestedValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transform value
|
||||
const settingDefinition = getPrefDefinition(prefKey);
|
||||
if (settingDefinition.transformValue) {
|
||||
suggestedValue = settingDefinition.transformValue.get.call(settingDefinition, suggestedValue);
|
||||
}
|
||||
|
||||
if ('setValue' in $control) {
|
||||
($control as BxHtmlSettingElement).setValue(suggestedValue);
|
||||
} else {
|
||||
($control as HTMLInputElement).value = suggestedValue;
|
||||
}
|
||||
|
||||
BxEvent.dispatch($control, 'input', {
|
||||
manualTrigger: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh suggested settings
|
||||
BxEvent.dispatch($select, 'input');
|
||||
};
|
||||
|
||||
// Apply button
|
||||
const $btnApply = createButton({
|
||||
label: t('apply'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
onClick: onClickApply,
|
||||
});
|
||||
|
||||
$content = CE('div', {
|
||||
class: 'bx-sub-content-box bx-suggest-box',
|
||||
_nearby: {
|
||||
orientation: 'vertical',
|
||||
}
|
||||
},
|
||||
BxSelectElement.create($select, true),
|
||||
$suggestedSettings,
|
||||
$btnApply,
|
||||
|
||||
BX_FLAGS.DeviceInfo.deviceType.includes('android') && CE('a', {
|
||||
class: 'bx-suggest-link bx-focusable',
|
||||
href: 'https://better-xcloud.github.io/guide/android-webview-tweaks/',
|
||||
target: '_blank',
|
||||
tabindex: 0,
|
||||
}, '🤓 ' + t('how-to-improve-app-performance')),
|
||||
|
||||
BX_FLAGS.DeviceInfo.deviceType.includes('android') && !hasRecommendedSettings && CE('a', {
|
||||
class: 'bx-suggest-link bx-focusable',
|
||||
href: 'https://github.com/redphx/better-xcloud-devices',
|
||||
target: '_blank',
|
||||
tabindex: 0,
|
||||
}, t('suggest-settings-link')),
|
||||
);
|
||||
|
||||
$btnSuggest.insertAdjacentElement('afterend', $content);
|
||||
}
|
||||
|
||||
private static async getRecommendedSettings(this: SettingsDialog, androidInfo: BxFlags['DeviceInfo']['androidInfo']): Promise<string | null> {
|
||||
function normalize(str: string) {
|
||||
return str.toLowerCase()
|
||||
.trim()
|
||||
.replaceAll(/\s+/g, '-')
|
||||
.replaceAll(/-+/g, '-');
|
||||
}
|
||||
|
||||
// Get recommended settings from GitHub
|
||||
try {
|
||||
let {brand, board, model} = androidInfo!;
|
||||
brand = normalize(brand);
|
||||
board = normalize(board);
|
||||
model = normalize(model);
|
||||
|
||||
const url = GhPagesUtils.getUrl(`devices/${brand}/${board}-${model}.json`);
|
||||
const response = await NATIVE_FETCH(url);
|
||||
const json = (await response.json()) as RecommendedSettings;
|
||||
const recommended: PartialRecord<PrefKey, any> = {};
|
||||
|
||||
// Only supports schema version 2
|
||||
if (json.schema_version !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scriptSettings = json.settings.script;
|
||||
|
||||
// Set base settings
|
||||
if (scriptSettings._base) {
|
||||
let base = typeof scriptSettings._base === 'string' ? [scriptSettings._base] : scriptSettings._base;
|
||||
for (const profile of base) {
|
||||
Object.assign(recommended, this.suggestedSettings[profile]);
|
||||
}
|
||||
|
||||
delete scriptSettings._base;
|
||||
}
|
||||
|
||||
// Override settings
|
||||
let key: Exclude<keyof typeof scriptSettings, '_base'>;
|
||||
// @ts-ignore
|
||||
for (key in scriptSettings) {
|
||||
recommended[key] = scriptSettings[key];
|
||||
}
|
||||
|
||||
// Update device type in BxFlags
|
||||
BX_FLAGS.DeviceInfo.deviceType = json.device_type;
|
||||
|
||||
this.suggestedSettings.recommended = recommended;
|
||||
|
||||
return json.device_name;
|
||||
} catch (e) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static addDefaultSuggestedSetting(this: SettingsDialog, prefKey: PrefKey, value: any) {
|
||||
let key: keyof typeof this.suggestedSettings;
|
||||
for (key in this.suggestedSettings) {
|
||||
if (key !== 'default' && !(prefKey in this.suggestedSettings)) {
|
||||
this.suggestedSettings[key][prefKey] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static generateDefaultSuggestedSettings(this: SettingsDialog) {
|
||||
let key: keyof typeof this.suggestedSettings;
|
||||
for (key in this.suggestedSettings) {
|
||||
if (key === 'default') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let prefKey: PrefKey;
|
||||
for (prefKey in this.suggestedSettings[key]) {
|
||||
if (!(prefKey in this.suggestedSettings.default)) {
|
||||
this.suggestedSettings.default[prefKey] = getPrefDefinition(prefKey).default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user