This commit is contained in:
redphx
2024-12-05 17:10:39 +07:00
parent c836e33f7b
commit 9199351af1
207 changed files with 9833 additions and 6953 deletions

View File

@@ -0,0 +1,84 @@
import type { AllPresets, AllPresetsData, PresetRecord, PresetRecords } from "@/types/presets";
import { deepClone } from "../global";
import { BaseLocalTable } from "./base-table";
export abstract class BasePresetsTable<T extends PresetRecord> extends BaseLocalTable<T> {
protected abstract TABLE_PRESETS: string;
protected abstract DEFAULT_PRESETS: PresetRecords<T>;
protected abstract readonly DEFAULT_PRESET_ID: number;
async newPreset(name: string, data: T['data']) {
const newRecord = { name, data } as T;
return await this.add(newRecord);
}
async updatePreset(preset: T) {
return await this.put(preset);
}
async deletePreset(id: number) {
return this.delete(id);
}
async getPreset(id: number): Promise<T | null> {
if (id === 0) {
return null;
}
if (id < 0) {
return this.DEFAULT_PRESETS[id];
}
let preset = await this.get(id);
if (!preset) {
preset = this.DEFAULT_PRESETS[this.DEFAULT_PRESET_ID];
}
return preset;
}
async getPresets(): Promise<AllPresets<T>> {
let all = deepClone(this.DEFAULT_PRESETS) as Record<string, T>;
const presets: AllPresets<T> = {
default: Object.keys(this.DEFAULT_PRESETS).map(key => parseInt(key)),
custom: [],
data: {},
}
const count = await this.count();
if (count > 0) {
const items = await this.getAll();
let id: keyof typeof items;
for (id in items) {
const item = items[id]!;
presets.custom.push(item.id!);
all[item.id] = item;
}
}
presets.data = all;
return presets;
}
async getPresetsData(): Promise<AllPresetsData<T>> {
const presetsData: AllPresetsData<T> = {}
for (const id in this.DEFAULT_PRESETS) {
const preset = this.DEFAULT_PRESETS[id];
presetsData[id] = deepClone(preset.data);
}
const count = await this.count();
if (count > 0) {
const items = await this.getAll();
let id: keyof typeof items;
for (id in items) {
const item = items[id]!;
presetsData[item.id] = item.data;
}
}
return presetsData;
}
}

View File

@@ -0,0 +1,70 @@
import { LocalDb } from "./local-db";
export class BaseLocalTable<T extends BaseRecord> {
private tableName: string;
constructor(tableName: string) {
this.tableName = tableName;
}
protected async prepareTable(type: IDBTransactionMode = 'readonly'): Promise<IDBObjectStore> {
const db = await LocalDb.getInstance().open();
const transaction = db.transaction(this.tableName, type);
const table = transaction.objectStore(this.tableName);
return table;
}
// Convert IndexDB method to Promise
protected call(method: any) {
return new Promise(resolve => {
const request = method.call(null, ...Array.from(arguments).slice(1));
request.onsuccess = (e: Event) => {
resolve((e.target as any).result);
};
});
}
async count(): Promise<number> {
const table = await this.prepareTable();
// @ts-ignore
return this.call(table.count.bind(table));
}
async add(data: T): Promise<number> {
const table = await this.prepareTable('readwrite');
// @ts-ignore
return this.call(table.add.bind(table), ...arguments);
}
async put(data: T): Promise<number> {
const table = await this.prepareTable('readwrite');
// @ts-ignore
return this.call(table.put.bind(table), ...arguments);
}
async delete(id: T['id']): Promise<number> {
const table = await this.prepareTable('readwrite');
// @ts-ignore
return this.call(table.delete.bind(table), ...arguments);
}
async get(id: T['id']): Promise<T> {
const table = await this.prepareTable();
// @ts-ignore
return this.call(table.get.bind(table), ...arguments);
}
async getAll(): Promise<{[key: string]: T}> {
const table = await this.prepareTable();
// @ts-ignore
const all = await (this.call(table.getAll.bind(table), ...arguments) as Promise<T[]>);
const results: {[key: string]: T} = {};
all.forEach(item => {
results[item.id as T['id']] = item;
});
return results;
}
}

View File

@@ -0,0 +1,38 @@
import { BaseLocalTable } from "./base-table";
import { LocalDb } from "./local-db";
import { ControllerShortcutDefaultId } from "./controller-shortcuts-table";
import { deepClone } from "../global";
export class ControllerSettingsTable extends BaseLocalTable<ControllerSettingsRecord> {
private static instance: ControllerSettingsTable;
public static getInstance = () => ControllerSettingsTable.instance ?? (ControllerSettingsTable.instance = new ControllerSettingsTable(LocalDb.TABLE_CONTROLLER_SETTINGS));
static readonly DEFAULT_DATA: ControllerSettingsRecord['data'] = {
shortcutPresetId: ControllerShortcutDefaultId.DEFAULT,
vibrationIntensity: 50,
};
async getControllerData(id: string): Promise<ControllerSettingsRecord['data']> {
const setting = await this.get(id);
if (!setting) {
return deepClone(ControllerSettingsTable.DEFAULT_DATA);
}
return setting.data;
}
async getControllersData() {
const all = await this.getAll();
const results: {[key: string]: ControllerSettingsRecord['data']} = {};
for (const key in all) {
const settings = all[key].data;
// Pre-calculate virabtionIntensity
settings.vibrationIntensity /= 100;
results[key] = settings;
}
return results;
}
}

View File

@@ -0,0 +1,61 @@
import type { ControllerShortcutPresetRecord, PresetRecords } from "@/types/presets";
import { BxLogger } from "../bx-logger";
import { BasePresetsTable } from "./base-presets-table";
import { LocalDb } from "./local-db";
import { GamepadKey } from "@/enums/gamepad";
import { ShortcutAction } from "@/enums/shortcut-actions";
import { AppInterface } from "../global";
export enum ControllerShortcutDefaultId {
TYPE_A = -1,
TYPE_B = -2,
DEFAULT = TYPE_A,
};
export class ControllerShortcutsTable extends BasePresetsTable<ControllerShortcutPresetRecord> {
private static instance: ControllerShortcutsTable;
public static getInstance = () => ControllerShortcutsTable.instance ?? (ControllerShortcutsTable.instance = new ControllerShortcutsTable());
private readonly LOG_TAG = 'ControllerShortcutsTable';
protected readonly TABLE_PRESETS: string = LocalDb.TABLE_CONTROLLER_SHORTCUTS;
protected readonly DEFAULT_PRESETS: PresetRecords<ControllerShortcutPresetRecord> = {
[ControllerShortcutDefaultId.TYPE_A]: {
id: ControllerShortcutDefaultId.TYPE_A,
name: 'Type A',
data: {
mapping: {
[GamepadKey.Y]: AppInterface ? ShortcutAction.DEVICE_VOLUME_INC : ShortcutAction.STREAM_VOLUME_INC,
[GamepadKey.A]: AppInterface ? ShortcutAction.DEVICE_VOLUME_DEC : ShortcutAction.STREAM_VOLUME_DEC,
[GamepadKey.X]: ShortcutAction.STREAM_STATS_TOGGLE,
[GamepadKey.B]: AppInterface ? ShortcutAction.DEVICE_SOUND_TOGGLE : ShortcutAction.STREAM_SOUND_TOGGLE,
[GamepadKey.RB]: ShortcutAction.STREAM_SCREENSHOT_CAPTURE,
[GamepadKey.START]: ShortcutAction.STREAM_MENU_SHOW,
},
},
},
[ControllerShortcutDefaultId.TYPE_B]: {
id: ControllerShortcutDefaultId.TYPE_B,
name: 'Type B',
data: {
mapping: {
[GamepadKey.UP]: AppInterface ? ShortcutAction.DEVICE_VOLUME_INC : ShortcutAction.STREAM_VOLUME_INC,
[GamepadKey.DOWN]: AppInterface ? ShortcutAction.DEVICE_VOLUME_DEC : ShortcutAction.STREAM_VOLUME_DEC,
[GamepadKey.RIGHT]: ShortcutAction.STREAM_STATS_TOGGLE,
[GamepadKey.LEFT]: AppInterface ? ShortcutAction.DEVICE_SOUND_TOGGLE : ShortcutAction.STREAM_SOUND_TOGGLE,
[GamepadKey.LB]: ShortcutAction.STREAM_SCREENSHOT_CAPTURE,
[GamepadKey.SELECT]: ShortcutAction.STREAM_MENU_SHOW,
},
},
},
};
protected readonly DEFAULT_PRESET_ID = ControllerShortcutDefaultId.DEFAULT;
private constructor() {
super(LocalDb.TABLE_CONTROLLER_SHORTCUTS);
BxLogger.info(this.LOG_TAG, 'constructor()');
}
}

View File

@@ -0,0 +1,45 @@
import type { KeyboardShortcutPresetRecord, PresetRecords } from "@/types/presets";
import { BxLogger } from "../bx-logger";
import { BasePresetsTable } from "./base-presets-table";
import { LocalDb } from "./local-db";
import { ShortcutAction } from "@/enums/shortcut-actions";
import { t } from "../translation";
export enum KeyboardShortcutDefaultId {
OFF = 0,
STANDARD = -1,
DEFAULT = STANDARD,
};
export class KeyboardShortcutsTable extends BasePresetsTable<KeyboardShortcutPresetRecord> {
private static instance: KeyboardShortcutsTable;
public static getInstance = () => KeyboardShortcutsTable.instance ?? (KeyboardShortcutsTable.instance = new KeyboardShortcutsTable());
private readonly LOG_TAG = 'KeyboardShortcutsTable';
protected readonly TABLE_PRESETS: string = LocalDb.TABLE_KEYBOARD_SHORTCUTS;
protected readonly DEFAULT_PRESETS: PresetRecords<KeyboardShortcutPresetRecord> = {
[KeyboardShortcutDefaultId.DEFAULT]: {
id: KeyboardShortcutDefaultId.DEFAULT,
name: t('standard'),
data: {
mapping: {
[ShortcutAction.MKB_TOGGLE]: {
code: 'F8',
},
[ShortcutAction.STREAM_SCREENSHOT_CAPTURE]: {
code: 'Slash',
},
},
},
},
};
protected readonly DEFAULT_PRESET_ID = KeyboardShortcutDefaultId.DEFAULT;
private constructor() {
super(LocalDb.TABLE_KEYBOARD_SHORTCUTS);
BxLogger.info(this.LOG_TAG, 'constructor()');
}
}

113
src/utils/local-db/local-db.ts Normal file → Executable file
View File

@@ -1,18 +1,65 @@
export abstract class LocalDb {
export class LocalDb {
private static instance: LocalDb;
public static getInstance = () => LocalDb.instance ?? (LocalDb.instance = new LocalDb());
// private readonly LOG_TAG = 'LocalDb';
static readonly DB_NAME = 'BetterXcloud';
static readonly DB_VERSION = 2;
static readonly DB_VERSION = 3;
protected db!: IDBDatabase;
static readonly TABLE_VIRTUAL_CONTROLLERS = 'virtual_controllers';
static readonly TABLE_CONTROLLER_SHORTCUTS = 'controller_shortcuts';
static readonly TABLE_CONTROLLER_SETTINGS = 'controller_settings';
static readonly TABLE_KEYBOARD_SHORTCUTS = 'keyboard_shortcuts';
protected open() {
return new Promise<void>((resolve, reject) => {
private db!: IDBDatabase;
open() {
return new Promise<IDBDatabase>((resolve, reject) => {
if (this.db) {
resolve();
resolve(this.db);
return;
}
const request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
request.onupgradeneeded = this.onUpgradeNeeded.bind(this);
request.onupgradeneeded = (e: IDBVersionChangeEvent) => {
const db = (e.target! as any).result as IDBDatabase;
// Delete "undefined" table
if (db.objectStoreNames.contains('undefined')) {
db.deleteObjectStore('undefined');
}
// Virtual controller
if (!db.objectStoreNames.contains(LocalDb.TABLE_VIRTUAL_CONTROLLERS)) {
db.createObjectStore(LocalDb.TABLE_VIRTUAL_CONTROLLERS, {
keyPath: 'id',
autoIncrement: true,
});
}
// Controller shortcuts
if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SHORTCUTS)) {
db.createObjectStore(LocalDb.TABLE_CONTROLLER_SHORTCUTS, {
keyPath: 'id',
autoIncrement: true,
});
}
// Controller settings
if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SETTINGS)) {
db.createObjectStore(LocalDb.TABLE_CONTROLLER_SETTINGS, {
keyPath: 'id',
});
}
// Keyboard shortcuts
if (!db.objectStoreNames.contains(LocalDb.TABLE_KEYBOARD_SHORTCUTS)) {
db.createObjectStore(LocalDb.TABLE_KEYBOARD_SHORTCUTS, {
keyPath: 'id',
autoIncrement: true,
});
}
};
request.onerror = e => {
console.log(e);
@@ -22,58 +69,8 @@ export abstract class LocalDb {
request.onsuccess = e => {
this.db = (e.target as any).result;
resolve();
resolve(this.db);
};
});
}
protected abstract onUpgradeNeeded(e: IDBVersionChangeEvent): void;
protected table(name: string, type: IDBTransactionMode): Promise<IDBObjectStore> {
const transaction = this.db.transaction(name, type || 'readonly');
const table = transaction.objectStore(name);
return new Promise(resolve => resolve(table));
}
// Convert IndexDB method to Promise
protected call(method: any) {
const table = arguments[1];
return new Promise(resolve => {
const request = method.call(table, ...Array.from(arguments).slice(2));
request.onsuccess = (e: Event) => {
resolve([table, (e.target as any).result]);
};
});
}
protected count(table: IDBObjectStore): Promise<[IDBObjectStore, number]> {
// @ts-ignore
return this.call(table.count, ...arguments);
}
protected add(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
// @ts-ignore
return this.call(table.add, ...arguments);
}
protected put(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
// @ts-ignore
return this.call(table.put, ...arguments);
}
protected delete(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
// @ts-ignore
return this.call(table.delete, ...arguments);
}
protected get(table: IDBObjectStore, id: number): Promise<any> {
// @ts-ignore
return this.call(table.get, ...arguments);
}
protected getAll(table: IDBObjectStore): Promise<[IDBObjectStore, any]> {
// @ts-ignore
return this.call(table.getAll, ...arguments);
}
}

View File

@@ -0,0 +1,126 @@
import { LocalDb } from "./local-db";
import { BxLogger } from "../bx-logger";
import { BasePresetsTable } from "./base-presets-table";
import type { MkbPresetRecord, PresetRecords } from "@/types/presets";
import { MkbPresetKey, MouseMapTo, MouseButtonCode } from "@/enums/mkb";
import { GamepadKey } from "@/enums/gamepad";
import { t } from "../translation";
export const enum MkbMappingDefaultPresetId {
OFF = 0,
STANDARD = -1,
SHOOTER = -2,
DEFAULT = STANDARD,
};
export class MkbMappingPresetsTable extends BasePresetsTable<MkbPresetRecord> {
private static instance: MkbMappingPresetsTable;
public static getInstance = () => MkbMappingPresetsTable.instance ?? (MkbMappingPresetsTable.instance = new MkbMappingPresetsTable());
private readonly LOG_TAG = 'MkbMappingPresetsTable';
protected readonly TABLE_PRESETS = LocalDb.TABLE_VIRTUAL_CONTROLLERS;
protected readonly DEFAULT_PRESETS: PresetRecords<MkbPresetRecord> = {
[MkbMappingDefaultPresetId.STANDARD]: {
id: MkbMappingDefaultPresetId.STANDARD,
name: t('standard'),
data: {
mapping: {
[GamepadKey.HOME]: ['Backquote'],
[GamepadKey.UP]: ['ArrowUp', 'Digit1'],
[GamepadKey.DOWN]: ['ArrowDown', 'Digit2'],
[GamepadKey.LEFT]: ['ArrowLeft', 'Digit3'],
[GamepadKey.RIGHT]: ['ArrowRight', 'Digit4'],
[GamepadKey.LS_UP]: ['KeyW'],
[GamepadKey.LS_DOWN]: ['KeyS'],
[GamepadKey.LS_LEFT]: ['KeyA'],
[GamepadKey.LS_RIGHT]: ['KeyD'],
[GamepadKey.RS_UP]: ['KeyU'],
[GamepadKey.RS_DOWN]: ['KeyJ'],
[GamepadKey.RS_LEFT]: ['KeyH'],
[GamepadKey.RS_RIGHT]: ['KeyK'],
[GamepadKey.A]: ['Space', 'KeyE'],
[GamepadKey.X]: ['KeyR'],
[GamepadKey.B]: ['KeyC', 'Backspace'],
[GamepadKey.Y]: ['KeyE'],
[GamepadKey.START]: ['Enter'],
[GamepadKey.SELECT]: ['Tab'],
[GamepadKey.LB]: ['KeyQ'],
[GamepadKey.RB]: ['KeyF'],
[GamepadKey.RT]: [MouseButtonCode.LEFT_CLICK],
[GamepadKey.LT]: [MouseButtonCode.RIGHT_CLICK],
[GamepadKey.L3]: ['KeyX'],
[GamepadKey.R3]: ['KeyZ'],
},
mouse: {
[MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo.RS,
[MkbPresetKey.MOUSE_SENSITIVITY_X]: 100,
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 100,
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20,
},
},
},
[MkbMappingDefaultPresetId.SHOOTER]: {
id: MkbMappingDefaultPresetId.SHOOTER,
name: 'Shooter',
data: {
mapping: {
[GamepadKey.HOME]: ['Backquote'],
[GamepadKey.UP]: ['ArrowUp'],
[GamepadKey.DOWN]: ['ArrowDown'],
[GamepadKey.LEFT]: ['ArrowLeft'],
[GamepadKey.RIGHT]: ['ArrowRight'],
[GamepadKey.LS_UP]: ['KeyW'],
[GamepadKey.LS_DOWN]: ['KeyS'],
[GamepadKey.LS_LEFT]: ['KeyA'],
[GamepadKey.LS_RIGHT]: ['KeyD'],
[GamepadKey.RS_UP]: ['KeyI'],
[GamepadKey.RS_DOWN]: ['KeyK'],
[GamepadKey.RS_LEFT]: ['KeyJ'],
[GamepadKey.RS_RIGHT]: ['KeyL'],
[GamepadKey.A]: ['Space', 'KeyE'],
[GamepadKey.X]: ['KeyR'],
[GamepadKey.B]: ['ControlLeft', 'Backspace'],
[GamepadKey.Y]: ['KeyV'],
[GamepadKey.START]: ['Enter'],
[GamepadKey.SELECT]: ['Tab'],
[GamepadKey.LB]: ['KeyC', 'KeyG'],
[GamepadKey.RB]: ['KeyQ'],
[GamepadKey.RT]: [MouseButtonCode.LEFT_CLICK],
[GamepadKey.LT]: [MouseButtonCode.RIGHT_CLICK],
[GamepadKey.L3]: ['ShiftLeft'],
[GamepadKey.R3]: ['KeyF'],
},
mouse: {
[MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo.RS,
[MkbPresetKey.MOUSE_SENSITIVITY_X]: 100,
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 100,
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20,
},
},
},
};
protected readonly DEFAULT_PRESET_ID = MkbMappingDefaultPresetId.DEFAULT;
private constructor() {
super(LocalDb.TABLE_VIRTUAL_CONTROLLERS);
BxLogger.info(this.LOG_TAG, 'constructor()');
}
}

View File

@@ -1,102 +0,0 @@
import { PrefKey } from "@/enums/pref-keys";
import { MkbPreset } from "@/modules/mkb/mkb-preset";
import type { MkbStoredPreset, MkbStoredPresets } from "@/types/mkb";
import { setPref } from "../settings-storages/global-settings-storage";
import { t } from "../translation";
import { LocalDb } from "./local-db";
import { BxLogger } from "../bx-logger";
export class MkbPresetsDb extends LocalDb {
private static instance: MkbPresetsDb;
public static getInstance = () => MkbPresetsDb.instance ?? (MkbPresetsDb.instance = new MkbPresetsDb());
private readonly LOG_TAG = 'MkbPresetsDb';
private readonly TABLE_PRESETS = 'mkb_presets';
private constructor() {
super();
BxLogger.info(this.LOG_TAG, 'constructor()');
}
private createTable(db: IDBDatabase) {
const presets = db.createObjectStore(this.TABLE_PRESETS, {
keyPath: 'id',
autoIncrement: true,
});
presets.createIndex('name_idx', 'name');
}
protected onUpgradeNeeded(e: IDBVersionChangeEvent): void {
const db = (e.target! as any).result as IDBDatabase;
if (db.objectStoreNames.contains('undefined')) {
db.deleteObjectStore('undefined');
}
if (!db.objectStoreNames.contains(this.TABLE_PRESETS)) {
this.createTable(db);
}
}
private async presetsTable() {
await this.open();
return await this.table(this.TABLE_PRESETS, 'readwrite');
}
async newPreset(name: string, data: any) {
const table = await this.presetsTable();
const [, id] = await this.add(table, { name, data });
return id;
}
async updatePreset(preset: MkbStoredPreset) {
const table = await this.presetsTable();
const [, id] = await this.put(table, preset);
return id;
}
async deletePreset(id: number) {
const table = await this.presetsTable();
await this.delete(table, id);
return id;
}
async getPreset(id: number): Promise<MkbStoredPreset> {
const table = await this.presetsTable();
const [, preset] = await this.get(table, id);
return preset;
}
async getPresets(): Promise<MkbStoredPresets> {
const table = await this.presetsTable();
const [, count] = await this.count(table);
// Return stored presets
if (count > 0) {
const [, items] = await this.getAll(table);
const presets: MkbStoredPresets = {};
items.forEach((item: MkbStoredPreset) => (presets[item.id!] = item));
return presets;
}
// Create "Default" preset when the table is empty
const preset: MkbStoredPreset = {
name: t('default'),
data: MkbPreset.DEFAULT_PRESET,
};
const [, id] = await this.add(table, preset);
preset.id = id;
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, id);
return {
[id]: preset,
};
}
}