mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-09-22 12:09:35 +02:00
6.0
This commit is contained in:
7
src/utils/bx-event.ts
Normal file → Executable file
7
src/utils/bx-event.ts
Normal file → Executable file
@@ -21,6 +21,9 @@ export namespace BxEvent {
|
||||
export const STREAM_WEBRTC_CONNECTED = 'bx-stream-webrtc-connected';
|
||||
export const STREAM_WEBRTC_DISCONNECTED = 'bx-stream-webrtc-disconnected';
|
||||
|
||||
export const MKB_UPDATED = 'bx-mkb-updated';
|
||||
export const KEYBOARD_SHORTCUTS_UPDATED = 'bx-keyboard-shortcuts-updated';
|
||||
|
||||
// export const STREAM_EVENT_TARGET_READY = 'bx-stream-event-target-ready';
|
||||
export const STREAM_SESSION_READY = 'bx-stream-session-ready';
|
||||
|
||||
@@ -34,10 +37,12 @@ export namespace BxEvent {
|
||||
export const XCLOUD_SERVERS_UNAVAILABLE = 'bx-servers-unavailable';
|
||||
|
||||
export const DATA_CHANNEL_CREATED = 'bx-data-channel-created';
|
||||
export const DEVICE_VIBRATION_CHANGED = 'bx-device-vibration-changed';
|
||||
|
||||
export const GAME_BAR_ACTION_ACTIVATED = 'bx-game-bar-action-activated';
|
||||
export const MICROPHONE_STATE_CHANGED = 'bx-microphone-state-changed';
|
||||
export const SPEAKER_STATE_CHANGED = 'bx-speaker-state-changed';
|
||||
export const VIDEO_VISIBILITY_CHANGED = 'bx-video-visibility-changed';
|
||||
|
||||
export const CAPTURE_SCREENSHOT = 'bx-capture-screenshot';
|
||||
|
||||
@@ -46,6 +51,8 @@ export namespace BxEvent {
|
||||
|
||||
export const NAVIGATION_FOCUS_CHANGED = 'bx-nav-focus-changed';
|
||||
|
||||
export const GH_PAGES_FORCE_NATIVE_MKB_UPDATED = 'bx-gh-pages-force-native-mkb-updated';
|
||||
|
||||
// xCloud Dialog events
|
||||
export const XCLOUD_DIALOG_SHOWN = 'bx-xcloud-dialog-shown';
|
||||
export const XCLOUD_DIALOG_DISMISSED = 'bx-xcloud-dialog-dismissed';
|
||||
|
19
src/utils/bx-exposed.ts
Normal file → Executable file
19
src/utils/bx-exposed.ts
Normal file → Executable file
@@ -7,9 +7,10 @@ import { BxLogger } from "./bx-logger";
|
||||
import { BX_FLAGS } from "./bx-flags";
|
||||
import { NavigationDialogManager } from "@/modules/ui/dialog/navigation-dialog";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, StreamTouchController } from "./settings-storages/global-settings-storage";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
import { GamePassCloudGallery } from "@/enums/game-pass-gallery";
|
||||
import { TouchController } from "@/modules/touch-controller";
|
||||
import { NativeMkbMode, TouchControllerMode } from "@/enums/pref-values";
|
||||
|
||||
export enum SupportedInputType {
|
||||
CONTROLLER = 'Controller',
|
||||
@@ -89,17 +90,17 @@ export const BxExposed = {
|
||||
}
|
||||
|
||||
// Remove native MKB support on mobile browsers or by user's choice
|
||||
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === 'off') {
|
||||
if (getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.OFF) {
|
||||
supportedInputTypes = supportedInputTypes.filter(i => i !== SupportedInputType.MKB);
|
||||
}
|
||||
|
||||
titleInfo.details.hasMkbSupport = supportedInputTypes.includes(SupportedInputType.MKB);
|
||||
|
||||
if (STATES.userAgent.capabilities.touch) {
|
||||
let touchControllerAvailability = getPref(PrefKey.STREAM_TOUCH_CONTROLLER);
|
||||
let touchControllerAvailability = getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE);
|
||||
|
||||
// Disable touch control when gamepad found
|
||||
if (touchControllerAvailability !== StreamTouchController.OFF && getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) {
|
||||
if (touchControllerAvailability !== TouchControllerMode.OFF && getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF)) {
|
||||
const gamepads = window.navigator.getGamepads();
|
||||
let gamepadFound = false;
|
||||
|
||||
@@ -110,10 +111,10 @@ export const BxExposed = {
|
||||
}
|
||||
}
|
||||
|
||||
gamepadFound && (touchControllerAvailability = StreamTouchController.OFF);
|
||||
gamepadFound && (touchControllerAvailability = TouchControllerMode.OFF);
|
||||
}
|
||||
|
||||
if (touchControllerAvailability === StreamTouchController.OFF) {
|
||||
if (touchControllerAvailability === TouchControllerMode.OFF) {
|
||||
// Disable touch on all games (not native touch)
|
||||
supportedInputTypes = supportedInputTypes.filter(i => i !== SupportedInputType.CUSTOM_TOUCH_OVERLAY && i !== SupportedInputType.GENERIC_TOUCH);
|
||||
// Empty TABs
|
||||
@@ -126,7 +127,7 @@ export const BxExposed = {
|
||||
supportedInputTypes.includes(SupportedInputType.CUSTOM_TOUCH_OVERLAY) ||
|
||||
supportedInputTypes.includes(SupportedInputType.GENERIC_TOUCH);
|
||||
|
||||
if (!titleInfo.details.hasTouchSupport && touchControllerAvailability === StreamTouchController.ALL) {
|
||||
if (!titleInfo.details.hasTouchSupport && touchControllerAvailability === TouchControllerMode.ALL) {
|
||||
// Add generic touch support for non touch-supported games
|
||||
titleInfo.details.hasFakeTouchSupport = true;
|
||||
supportedInputTypes.push(SupportedInputType.GENERIC_TOUCH);
|
||||
@@ -172,7 +173,7 @@ export const BxExposed = {
|
||||
resetControllerShortcut: isFullVersion() && ControllerShortcut.reset,
|
||||
|
||||
overrideSettings: {
|
||||
'Tv_settings': {
|
||||
Tv_settings: {
|
||||
hasCompletedOnboarding: true,
|
||||
},
|
||||
},
|
||||
@@ -206,4 +207,6 @@ export const BxExposed = {
|
||||
/ {2,}/g,
|
||||
/ /g,
|
||||
],
|
||||
|
||||
toggleLocalCoOp: (enable: boolean) => {},
|
||||
};
|
||||
|
0
src/utils/bx-flags.ts
Normal file → Executable file
0
src/utils/bx-flags.ts
Normal file → Executable file
4
src/utils/bx-icon.ts
Normal file → Executable file
4
src/utils/bx-icon.ts
Normal file → Executable file
@@ -2,7 +2,6 @@
|
||||
import iconBetterXcloud from "@assets/svg/better-xcloud.svg" with { type: "text" };
|
||||
import iconTrueAchievements from "@assets/svg/true-achievements.svg" with { type: "text" };
|
||||
import iconClose from "@assets/svg/close.svg" with { type: "text" };
|
||||
import iconCommand from "@assets/svg/command.svg" with { type: "text" };
|
||||
import iconController from "@assets/svg/controller.svg" with { type: "text" };
|
||||
import iconCopy from "@assets/svg/copy.svg" with { type: "text" };
|
||||
import iconCreateShortcut from "@assets/svg/create-shortcut.svg" with { type: "text" };
|
||||
@@ -23,7 +22,6 @@ import iconStreamStats from "@assets/svg/stream-stats.svg" with { type: "text" }
|
||||
import iconTouchControlDisable from "@assets/svg/touch-control-disable.svg" with { type: "text" };
|
||||
import iconTouchControlEnable from "@assets/svg/touch-control-enable.svg" with { type: "text" };
|
||||
import iconTrash from "@assets/svg/trash.svg" with { type: "text" };
|
||||
import iconVirtualController from "@assets/svg/virtual-controller.svg" with { type: "text" };
|
||||
|
||||
// Game Bar
|
||||
import iconCaretLeft from "@assets/svg/caret-left.svg" with { type: "text" };
|
||||
@@ -47,7 +45,6 @@ export const BxIcon = {
|
||||
STREAM_SETTINGS: iconStreamSettings,
|
||||
STREAM_STATS: iconStreamStats,
|
||||
CLOSE: iconClose,
|
||||
COMMAND: iconCommand,
|
||||
CONTROLLER: iconController,
|
||||
CREATE_SHORTCUT: iconCreateShortcut,
|
||||
DISPLAY: iconDisplay,
|
||||
@@ -62,7 +59,6 @@ export const BxIcon = {
|
||||
POWER: iconPower,
|
||||
QUESTION: iconQuestion,
|
||||
REFRESH: iconRefresh,
|
||||
VIRTUAL_CONTROLLER: iconVirtualController,
|
||||
|
||||
REMOTE_PLAY: iconRemotePlay,
|
||||
|
||||
|
0
src/utils/bx-logger.ts
Normal file → Executable file
0
src/utils/bx-logger.ts
Normal file → Executable file
15
src/utils/css.ts
Normal file → Executable file
15
src/utils/css.ts
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
import { CE } from "@utils/html";
|
||||
import { compressCss, renderStylus } from "@macros/build" with {type: "macro"};
|
||||
import { UiSection } from "@/enums/ui-sections";
|
||||
import { UiSection } from "@/enums/pref-values";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
@@ -9,7 +9,7 @@ export function addCss() {
|
||||
const STYLUS_CSS = renderStylus() as unknown as string;
|
||||
let css = STYLUS_CSS;
|
||||
|
||||
const PREF_HIDE_SECTIONS = getPref(PrefKey.UI_HIDE_SECTIONS);
|
||||
const PREF_HIDE_SECTIONS = getPref<UiSection[]>(PrefKey.UI_HIDE_SECTIONS);
|
||||
const selectorToHide = [];
|
||||
|
||||
// Hide "News" section
|
||||
@@ -17,6 +17,11 @@ export function addCss() {
|
||||
selectorToHide.push('#BodyContent > div[class*=CarouselRow-module]');
|
||||
}
|
||||
|
||||
// Hide BYOG section
|
||||
if (getPref(PrefKey.BYOG_DISABLED)) {
|
||||
selectorToHide.push('#BodyContent > div[class*=ByogRow-module__container___]');
|
||||
}
|
||||
|
||||
// Hide "All games" section
|
||||
if (PREF_HIDE_SECTIONS.includes(UiSection.ALL_GAMES)) {
|
||||
selectorToHide.push('#BodyContent div[class*=AllGamesRow-module__gridContainer]');
|
||||
@@ -43,7 +48,7 @@ export function addCss() {
|
||||
}
|
||||
|
||||
// Reduce animations
|
||||
if (getPref(PrefKey.REDUCE_ANIMATIONS)) {
|
||||
if (getPref(PrefKey.UI_REDUCE_ANIMATIONS)) {
|
||||
css += compressCss(`
|
||||
div[class*=GameCard-module__gameTitleInnerWrapper],
|
||||
div[class*=GameCard-module__card],
|
||||
@@ -54,7 +59,7 @@ div[class*=ScrollArrows-module] {
|
||||
}
|
||||
|
||||
// Hide the top-left dots icon while playing
|
||||
if (getPref(PrefKey.HIDE_DOTS_ICON)) {
|
||||
if (getPref(PrefKey.UI_HIDE_SYSTEM_MENU_ICON)) {
|
||||
css += compressCss(`
|
||||
div[class*=Grip-module__container] {
|
||||
visibility: hidden;
|
||||
@@ -87,7 +92,7 @@ div[class*=StreamMenu-module__menu] {
|
||||
`);
|
||||
|
||||
// Simplify Stream's menu
|
||||
if (getPref(PrefKey.STREAM_SIMPLIFY_MENU)) {
|
||||
if (getPref(PrefKey.UI_SIMPLIFY_STREAM_MENU)) {
|
||||
css += compressCss(`
|
||||
div[class*=Menu-module__scrollable] {
|
||||
--bxStreamMenuItemSize: 80px;
|
||||
|
23
src/utils/feature-gates.ts
Normal file → Executable file
23
src/utils/feature-gates.ts
Normal file → Executable file
@@ -1,17 +1,30 @@
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { BX_FLAGS } from "./bx-flags";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
import { NativeMkbMode } from "@/enums/pref-values";
|
||||
|
||||
export let FeatureGates: {[key: string]: boolean} = {
|
||||
'PwaPrompt': false,
|
||||
'EnableWifiWarnings': false,
|
||||
'EnableUpdateRequiredPage': false,
|
||||
'ShowForcedUpdateScreen': false,
|
||||
PwaPrompt: false,
|
||||
EnableWifiWarnings: false,
|
||||
EnableUpdateRequiredPage: false,
|
||||
ShowForcedUpdateScreen: false,
|
||||
};
|
||||
|
||||
// Enable Native Mouse & Keyboard
|
||||
const nativeMkbMode = getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE);
|
||||
if (nativeMkbMode !== NativeMkbMode.DEFAULT) {
|
||||
FeatureGates.EnableMouseAndKeyboard = nativeMkbMode === NativeMkbMode.ON;
|
||||
}
|
||||
|
||||
// Disable chat feature
|
||||
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES)) {
|
||||
FeatureGates['EnableGuideChatTab'] = false;
|
||||
FeatureGates.EnableGuideChatTab = false;
|
||||
}
|
||||
|
||||
// Disable BYOG feature
|
||||
if (getPref(PrefKey.BYOG_DISABLED)) {
|
||||
FeatureGates.EnableBYOG = false;
|
||||
FeatureGates.EnableBYOGPurchase = false;
|
||||
}
|
||||
|
||||
if (BX_FLAGS.FeatureGates) {
|
||||
|
24
src/utils/gamepad.ts
Normal file → Executable file
24
src/utils/gamepad.ts
Normal file → Executable file
@@ -34,6 +34,26 @@ export function showGamepadToast(gamepad: Gamepad) {
|
||||
Toast.show(text, status, {instant: false});
|
||||
}
|
||||
|
||||
export function updatePollingRate() {
|
||||
window.BX_CONTROLLER_POLLING_RATE = getPref(PrefKey.CONTROLLER_POLLING_RATE);
|
||||
export function getUniqueGamepadNames() {
|
||||
const gamepads = window.navigator.getGamepads();
|
||||
const names: string[] = [];
|
||||
|
||||
for (const gamepad of gamepads) {
|
||||
if (gamepad?.connected && gamepad.id !== VIRTUAL_GAMEPAD_ID) {
|
||||
!names.includes(gamepad.id) && names.push(gamepad.id);
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
export function hasGamepad() {
|
||||
const gamepads = window.navigator.getGamepads();
|
||||
for (const gamepad of gamepads) {
|
||||
if (gamepad?.connected) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
84
src/utils/gh-pages.ts
Executable file
84
src/utils/gh-pages.ts
Executable file
@@ -0,0 +1,84 @@
|
||||
import { StorageKey } from "@/enums/pref-keys";
|
||||
import { NATIVE_FETCH } from "./bx-flags";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
import { BxEvent } from "./bx-event";
|
||||
|
||||
|
||||
export type ForceNativeMkbResponse = {
|
||||
$schemaVersion: number;
|
||||
data: { [key: string]: string };
|
||||
}
|
||||
|
||||
export class GhPagesUtils {
|
||||
static fetchLatestCommit() {
|
||||
const url = 'https://api.github.com/repos/redphx/better-xcloud/branches/gh-pages';
|
||||
|
||||
NATIVE_FETCH(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const latestCommitHash = data.commit.sha;
|
||||
window.localStorage.setItem(StorageKey.GH_PAGES_COMMIT_HASH, latestCommitHash);
|
||||
}).catch(error => {
|
||||
BxLogger.error('GhPagesUtils', 'Error fetching the latest commit:', error);
|
||||
});
|
||||
}
|
||||
|
||||
static getUrl(path: string): string {
|
||||
if (path[0] === '/') {
|
||||
alert('`path` must not starts with "/"');
|
||||
}
|
||||
|
||||
const prefix = 'https://raw.githubusercontent.com/redphx/better-xcloud';
|
||||
const latestCommitHash = window.localStorage.getItem(StorageKey.GH_PAGES_COMMIT_HASH);
|
||||
if (latestCommitHash) {
|
||||
return `${prefix}/${latestCommitHash}/${path}`;
|
||||
} else {
|
||||
return `${prefix}/refs/heads/gh-pages/${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
static getNativeMkbCustomList(update=false): ForceNativeMkbResponse['data'] {
|
||||
const supportedSchema = 1;
|
||||
const key = StorageKey.LIST_FORCE_NATIVE_MKB;
|
||||
|
||||
// Update IDs in the background
|
||||
update && NATIVE_FETCH(GhPagesUtils.getUrl('native-mkb/ids.json'))
|
||||
.then(response => response.json())
|
||||
.then((json: ForceNativeMkbResponse) => {
|
||||
if (json.$schemaVersion === supportedSchema) {
|
||||
// Save to storage
|
||||
window.localStorage.setItem(key, JSON.stringify(json));
|
||||
BxEvent.dispatch(window, BxEvent.GH_PAGES_FORCE_NATIVE_MKB_UPDATED);
|
||||
}
|
||||
});
|
||||
|
||||
const info = JSON.parse(window.localStorage.getItem(key) || '{}');
|
||||
if (info.$schemaVersion !== supportedSchema) {
|
||||
// Delete storage;
|
||||
window.localStorage.removeItem(key);
|
||||
return {};
|
||||
}
|
||||
|
||||
return info.data;
|
||||
}
|
||||
|
||||
static getTouchControlCustomList() {
|
||||
const key = StorageKey.LIST_CUSTOM_TOUCH_LAYOUTS;
|
||||
|
||||
NATIVE_FETCH(GhPagesUtils.getUrl('touch-layouts/ids.json'))
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
if (Array.isArray(json)) {
|
||||
window.localStorage.setItem(key, JSON.stringify(json));
|
||||
}
|
||||
});
|
||||
|
||||
const customList = JSON.parse(window.localStorage.getItem(key) || '[]');
|
||||
return customList;
|
||||
}
|
||||
}
|
16
src/utils/global.ts
Normal file → Executable file
16
src/utils/global.ts
Normal file → Executable file
@@ -13,7 +13,6 @@ const isTv = userAgent.includes('smart-tv') || userAgent.includes('smarttv') ||
|
||||
const isVr = window.navigator.userAgent.includes('VR') && window.navigator.userAgent.includes('OculusBrowser');
|
||||
const browserHasTouchSupport = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
const userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport;
|
||||
const supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/);
|
||||
|
||||
export const STATES: BxStates = {
|
||||
supportedRegion: true,
|
||||
@@ -28,6 +27,9 @@ export const STATES: BxStates = {
|
||||
capabilities: {
|
||||
touch: browserHasTouchSupport,
|
||||
batteryApi: 'getBattery' in window.navigator,
|
||||
deviceVibration: !!window.navigator.vibrate,
|
||||
mkb: AppInterface || !UserAgent.getDefault().toLowerCase().match(/(android|iphone|ipad)/),
|
||||
emulatedNativeMkb: !!AppInterface,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -35,7 +37,7 @@ export const STATES: BxStates = {
|
||||
isTv: isTv,
|
||||
capabilities: {
|
||||
touch: userAgentHasTouchSupport,
|
||||
mkb: supportMkb,
|
||||
mkb: AppInterface || !userAgent.match(/(android|iphone|ipad)/),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -47,14 +49,14 @@ export const STATES: BxStates = {
|
||||
|
||||
export const STORAGE: {[key: string]: BaseSettingsStore} = {};
|
||||
|
||||
export function deepClone(obj: any): any {
|
||||
if ('structuredClone' in window) {
|
||||
return structuredClone(obj);
|
||||
}
|
||||
|
||||
export function deepClone(obj: any): typeof obj | {} {
|
||||
if (!obj) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if ('structuredClone' in window) {
|
||||
return structuredClone(obj);
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
0
src/utils/history.ts
Normal file → Executable file
0
src/utils/history.ts
Normal file → Executable file
207
src/utils/html.ts
Normal file → Executable file
207
src/utils/html.ts
Normal file → Executable file
@@ -1,24 +1,28 @@
|
||||
import type { BxIcon } from "@utils/bx-icon";
|
||||
import { setNearby } from "./navigation-utils";
|
||||
import type { NavigationNearbyElements } from "@/modules/ui/dialog/navigation-dialog";
|
||||
import type { PresetRecord, AllPresets } from "@/types/presets";
|
||||
import { t } from "./translation";
|
||||
|
||||
export enum ButtonStyle {
|
||||
PRIMARY = 1,
|
||||
DANGER = 2,
|
||||
GHOST = 4,
|
||||
FROSTED = 8,
|
||||
DROP_SHADOW = 16,
|
||||
FOCUSABLE = 32,
|
||||
FULL_WIDTH = 64,
|
||||
FULL_HEIGHT = 128,
|
||||
TALL = 256,
|
||||
CIRCULAR = 512,
|
||||
NORMAL_CASE = 1024,
|
||||
NORMAL_LINK = 2048,
|
||||
WARNING = 1 << 1,
|
||||
DANGER = 1 << 2,
|
||||
GHOST = 1 << 3,
|
||||
FROSTED = 1 << 4,
|
||||
DROP_SHADOW = 1 << 5,
|
||||
FOCUSABLE = 1 << 6,
|
||||
FULL_WIDTH = 1 << 7,
|
||||
FULL_HEIGHT = 1 << 8,
|
||||
TALL = 1 << 9,
|
||||
CIRCULAR = 1 << 10,
|
||||
NORMAL_CASE = 1 << 11,
|
||||
NORMAL_LINK = 1 << 12,
|
||||
}
|
||||
|
||||
const ButtonStyleClass = {
|
||||
[ButtonStyle.PRIMARY]: 'bx-primary',
|
||||
[ButtonStyle.WARNING]: 'bx-warning',
|
||||
[ButtonStyle.DANGER]: 'bx-danger',
|
||||
[ButtonStyle.GHOST]: 'bx-ghost',
|
||||
[ButtonStyle.FROSTED]: 'bx-frosted',
|
||||
@@ -32,22 +36,34 @@ const ButtonStyleClass = {
|
||||
[ButtonStyle.NORMAL_LINK]: 'bx-normal-link',
|
||||
}
|
||||
|
||||
export type BxButton = {
|
||||
style?: ButtonStyle;
|
||||
url?: string;
|
||||
classes?: string[];
|
||||
icon?: typeof BxIcon;
|
||||
label?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
onClick?: EventListener;
|
||||
tabIndex?: number;
|
||||
attributes?: {[key: string]: any},
|
||||
}
|
||||
export type BxButtonOptions = Partial<{
|
||||
style: ButtonStyle;
|
||||
url: string;
|
||||
classes: string[];
|
||||
icon: typeof BxIcon;
|
||||
label: string;
|
||||
secondaryText: HTMLElement | string;
|
||||
title: string;
|
||||
disabled: boolean;
|
||||
onClick: EventListener;
|
||||
tabIndex: number;
|
||||
attributes: {[key: string]: any},
|
||||
}>;
|
||||
|
||||
export type SettingsRowOptions = Partial<{
|
||||
multiLines: boolean;
|
||||
$note: HTMLElement;
|
||||
}>;
|
||||
|
||||
// Quickly create a tree of elements without having to use innerHTML
|
||||
type CreateElementOptions = {
|
||||
[index: string]: any;
|
||||
_on?: {
|
||||
[ key: string ]: (e: Event) => void;
|
||||
};
|
||||
_dataset?: {
|
||||
[ key: string ]: string | number;
|
||||
};
|
||||
_nearby?: NavigationNearbyElements;
|
||||
};
|
||||
|
||||
@@ -65,9 +81,23 @@ function createElement<T=HTMLElement>(elmName: string, props: CreateElementOptio
|
||||
$elm = document.createElement(elmName);
|
||||
}
|
||||
|
||||
if (props['_nearby']) {
|
||||
setNearby($elm, props['_nearby']);
|
||||
delete props['_nearby'];
|
||||
if (props._nearby) {
|
||||
setNearby($elm, props._nearby);
|
||||
delete props._nearby;
|
||||
}
|
||||
|
||||
if (props._on) {
|
||||
for (const name in props._on) {
|
||||
$elm.addEventListener(name, props._on[name]);
|
||||
}
|
||||
delete props._on;
|
||||
}
|
||||
|
||||
if (props._dataset) {
|
||||
for (const name in props._dataset) {
|
||||
$elm.dataset[name] = props._dataset[name] as string;
|
||||
}
|
||||
delete props._dataset;
|
||||
}
|
||||
|
||||
for (const key in props) {
|
||||
@@ -75,33 +105,25 @@ function createElement<T=HTMLElement>(elmName: string, props: CreateElementOptio
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = props[key];
|
||||
if (hasNs) {
|
||||
$elm.setAttributeNS(null, key, props[key]);
|
||||
$elm.setAttributeNS(null, key, value);
|
||||
} else {
|
||||
if (key === 'on') {
|
||||
for (const eventName in props[key]) {
|
||||
$elm.addEventListener(eventName, props[key][eventName]);
|
||||
}
|
||||
} else {
|
||||
$elm.setAttribute(key, props[key]);
|
||||
}
|
||||
$elm.setAttribute(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 2, size = arguments.length; i < size; i++) {
|
||||
const arg = arguments[i];
|
||||
|
||||
if (arg instanceof Node) {
|
||||
$elm.appendChild(arg);
|
||||
} else if (arg !== null && arg !== false && typeof arg !== 'undefined') {
|
||||
$elm.appendChild(document.createTextNode(arg));
|
||||
if (arg !== null && arg !== false && typeof arg !== 'undefined') {
|
||||
$elm.append(arg);
|
||||
}
|
||||
}
|
||||
|
||||
return $elm as T;
|
||||
}
|
||||
|
||||
export const CE = createElement;
|
||||
|
||||
const domParser = new DOMParser();
|
||||
export function createSvgIcon(icon: typeof BxIcon) {
|
||||
@@ -110,16 +132,26 @@ export function createSvgIcon(icon: typeof BxIcon) {
|
||||
|
||||
const ButtonStyleIndices = Object.keys(ButtonStyleClass).map(i => parseInt(i));
|
||||
|
||||
export function createButton<T=HTMLButtonElement>(options: BxButton): T {
|
||||
export function createButton<T=HTMLButtonElement>(options: BxButtonOptions): T {
|
||||
let $btn;
|
||||
|
||||
// Create base button element
|
||||
if (options.url) {
|
||||
$btn = CE<HTMLAnchorElement>('a', {'class': 'bx-button'});
|
||||
$btn.href = options.url;
|
||||
$btn.target = '_blank';
|
||||
$btn = CE<HTMLAnchorElement>('a', {
|
||||
class: 'bx-button',
|
||||
href: options.url,
|
||||
target: '_blank',
|
||||
});
|
||||
} else {
|
||||
$btn = CE<HTMLButtonElement>('button', {'class': 'bx-button', type: 'button'});
|
||||
$btn = CE<HTMLButtonElement>('button', {
|
||||
class: 'bx-button',
|
||||
type: 'button',
|
||||
});
|
||||
|
||||
options.disabled && ($btn.disabled = true);
|
||||
}
|
||||
|
||||
// Add button styles
|
||||
const style = (options.style || 0) as number;
|
||||
if (style) {
|
||||
let index: keyof typeof ButtonStyleClass;
|
||||
@@ -133,10 +165,14 @@ export function createButton<T=HTMLButtonElement>(options: BxButton): T {
|
||||
options.icon && $btn.appendChild(createSvgIcon(options.icon));
|
||||
options.label && $btn.appendChild(CE('span', {}, options.label));
|
||||
options.title && $btn.setAttribute('title', options.title);
|
||||
options.disabled && (($btn as HTMLButtonElement).disabled = true);
|
||||
options.onClick && $btn.addEventListener('click', options.onClick);
|
||||
$btn.tabIndex = typeof options.tabIndex === 'number' ? options.tabIndex : 0;
|
||||
|
||||
if (options.secondaryText) {
|
||||
$btn.classList.add('bx-button-multi-lines');
|
||||
$btn.appendChild(CE('span', {}, options.secondaryText));
|
||||
}
|
||||
|
||||
for (const key in options.attributes) {
|
||||
if (!$btn.hasOwnProperty(key)) {
|
||||
$btn.setAttribute(key, options.attributes[key]);
|
||||
@@ -146,6 +182,41 @@ export function createButton<T=HTMLButtonElement>(options: BxButton): T {
|
||||
return $btn as T;
|
||||
}
|
||||
|
||||
export function createSettingRow(label: string, $control: HTMLElement | false | undefined, options: SettingsRowOptions={}) {
|
||||
let $label: HTMLElement;
|
||||
|
||||
const $row = CE<HTMLLabelElement>('label', { class: 'bx-settings-row' },
|
||||
$label = CE('span', {class: 'bx-settings-label'},
|
||||
label,
|
||||
options.$note,
|
||||
),
|
||||
$control,
|
||||
);
|
||||
|
||||
// Make link inside <label> focusable
|
||||
const $link = $label.querySelector('a');
|
||||
if ($link) {
|
||||
$link.classList.add('bx-focusable');
|
||||
setNearby($label, {
|
||||
focus: $link,
|
||||
});
|
||||
}
|
||||
|
||||
setNearby($row, {
|
||||
orientation: options.multiLines ? 'vertical' : 'horizontal',
|
||||
});
|
||||
|
||||
if (options.multiLines) {
|
||||
$row.dataset.multiLines = 'true';
|
||||
}
|
||||
|
||||
if ($control instanceof HTMLElement && $control.id) {
|
||||
$row.htmlFor = $control.id;
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
export function getReactProps($elm: HTMLElement): any | null {
|
||||
for (const key in $elm) {
|
||||
if (key.startsWith('__reactProps')) {
|
||||
@@ -169,10 +240,12 @@ export function isElementVisible($elm: HTMLElement): boolean {
|
||||
return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height;
|
||||
}
|
||||
|
||||
export const CTN = document.createTextNode.bind(document);
|
||||
window.BX_CE = createElement;
|
||||
|
||||
export function removeChildElements($parent: HTMLElement) {
|
||||
if ($parent instanceof HTMLDivElement && $parent.classList.contains('bx-select')) {
|
||||
$parent = $parent.querySelector('select')!;
|
||||
}
|
||||
|
||||
while ($parent.firstElementChild) {
|
||||
$parent.firstElementChild.remove();
|
||||
}
|
||||
@@ -190,6 +263,39 @@ export function clearDataSet($elm: HTMLElement) {
|
||||
});
|
||||
}
|
||||
|
||||
export function renderPresetsList<T extends PresetRecord>($select: HTMLSelectElement, allPresets: AllPresets<T>, selectedValue: number | null, addOffValue=false) {
|
||||
removeChildElements($select);
|
||||
|
||||
if (addOffValue) {
|
||||
const $option = CE<HTMLOptionElement>('option', { value: 0 }, t('off'));
|
||||
$option.selected = selectedValue === 0;
|
||||
|
||||
$select.appendChild($option);
|
||||
}
|
||||
|
||||
// Render options
|
||||
const groups = {
|
||||
default: t('default'),
|
||||
custom: t('custom'),
|
||||
};
|
||||
|
||||
let key: keyof typeof groups;
|
||||
for (key in groups) {
|
||||
const $optGroup = CE('optgroup', { label: groups[key] });
|
||||
for (const id of allPresets[key]) {
|
||||
const record = allPresets.data[id];
|
||||
const $option = CE<HTMLOptionElement>('option', { value: record.id }, record.name);
|
||||
$option.selected = selectedValue === record.id;
|
||||
|
||||
$optGroup.appendChild($option);
|
||||
}
|
||||
|
||||
if ($optGroup.hasChildNodes()) {
|
||||
$select.appendChild($optGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/20732091
|
||||
const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
export function humanFileSize(size: number) {
|
||||
@@ -228,3 +334,10 @@ export function secondsToHms(seconds: number) {
|
||||
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
export function escapeCssSelector(name: string) {
|
||||
return name.replaceAll('.', '-');
|
||||
}
|
||||
|
||||
export const CE = createElement;
|
||||
window.BX_CE = createElement;
|
||||
|
84
src/utils/local-db/base-presets-table.ts
Executable file
84
src/utils/local-db/base-presets-table.ts
Executable 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;
|
||||
}
|
||||
}
|
70
src/utils/local-db/base-table.ts
Executable file
70
src/utils/local-db/base-table.ts
Executable 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;
|
||||
}
|
||||
}
|
38
src/utils/local-db/controller-settings-table.ts
Executable file
38
src/utils/local-db/controller-settings-table.ts
Executable 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;
|
||||
}
|
||||
}
|
61
src/utils/local-db/controller-shortcuts-table.ts
Executable file
61
src/utils/local-db/controller-shortcuts-table.ts
Executable 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()');
|
||||
}
|
||||
}
|
45
src/utils/local-db/keyboard-shortcuts-table.ts
Executable file
45
src/utils/local-db/keyboard-shortcuts-table.ts
Executable 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
113
src/utils/local-db/local-db.ts
Normal file → Executable 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);
|
||||
}
|
||||
}
|
||||
|
126
src/utils/local-db/mkb-mapping-presets-table.ts
Executable file
126
src/utils/local-db/mkb-mapping-presets-table.ts
Executable 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()');
|
||||
}
|
||||
}
|
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
23
src/utils/monkey-patches.ts
Normal file → Executable file
23
src/utils/monkey-patches.ts
Normal file → Executable file
@@ -4,10 +4,12 @@ import { BxLogger } from "@utils/bx-logger";
|
||||
import { patchSdpBitrate, setCodecPreferences } from "./sdp";
|
||||
import { StreamPlayer, type StreamPlayerOptions } from "@/modules/stream-player";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
import { getPref, getPrefDefinition } from "./settings-storages/global-settings-storage";
|
||||
import { CodecProfile } from "@/enums/pref-values";
|
||||
import type { SettingDefinition } from "@/types/setting-definition";
|
||||
|
||||
export function patchVideoApi() {
|
||||
const PREF_SKIP_SPLASH_VIDEO = getPref(PrefKey.SKIP_SPLASH_VIDEO);
|
||||
const PREF_SKIP_SPLASH_VIDEO = getPref(PrefKey.UI_SKIP_SPLASH_VIDEO);
|
||||
|
||||
// Show video player when it's ready
|
||||
const showFunc = function(this: HTMLVideoElement) {
|
||||
@@ -57,7 +59,7 @@ export function patchVideoApi() {
|
||||
|
||||
|
||||
export function patchRtcCodecs() {
|
||||
const codecProfile = getPref(PrefKey.STREAM_CODEC_PROFILE);
|
||||
const codecProfile = getPref<CodecProfile>(PrefKey.STREAM_CODEC_PROFILE);
|
||||
if (codecProfile === 'default') {
|
||||
return;
|
||||
}
|
||||
@@ -80,20 +82,21 @@ export function patchRtcPeerConnection() {
|
||||
return dataChannel;
|
||||
}
|
||||
|
||||
const maxVideoBitrate = getPref(PrefKey.BITRATE_VIDEO_MAX);
|
||||
const codec = getPref(PrefKey.STREAM_CODEC_PROFILE);
|
||||
const maxVideoBitrateDef = getPrefDefinition(PrefKey.STREAM_MAX_VIDEO_BITRATE) as Extract<SettingDefinition, { min: number }>;
|
||||
const maxVideoBitrate = getPref<VideoMaxBitrate>(PrefKey.STREAM_MAX_VIDEO_BITRATE);
|
||||
const codec = getPref<CodecProfile>(PrefKey.STREAM_CODEC_PROFILE);
|
||||
|
||||
if (codec !== 'default' || maxVideoBitrate > 0) {
|
||||
if (codec !== CodecProfile.DEFAULT || maxVideoBitrate < maxVideoBitrateDef.max) {
|
||||
const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription;
|
||||
RTCPeerConnection.prototype.setLocalDescription = function(description?: RTCLocalSessionDescriptionInit): Promise<void> {
|
||||
// Set preferred codec profile
|
||||
if (codec !== 'default') {
|
||||
if (codec !== CodecProfile.DEFAULT) {
|
||||
arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec);
|
||||
}
|
||||
|
||||
// set maximum bitrate
|
||||
// Set maximum bitrate
|
||||
try {
|
||||
if (maxVideoBitrate > 0 && description) {
|
||||
if (maxVideoBitrate < maxVideoBitrateDef.max && description) {
|
||||
arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000));
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -133,7 +136,7 @@ export function patchAudioContext() {
|
||||
|
||||
ctx.createGain = function() {
|
||||
const gainNode = nativeCreateGain.apply(this);
|
||||
gainNode.gain.value = getPref(PrefKey.AUDIO_VOLUME) / 100;
|
||||
gainNode.gain.value = getPref<AudioVolume>(PrefKey.AUDIO_VOLUME) / 100;
|
||||
|
||||
STATES.currentStream.audioGainNode = gainNode;
|
||||
return gainNode;
|
||||
|
0
src/utils/navigation-utils.ts
Normal file → Executable file
0
src/utils/navigation-utils.ts
Normal file → Executable file
10
src/utils/network.ts
Normal file → Executable file
10
src/utils/network.ts
Normal file → Executable file
@@ -66,10 +66,10 @@ function updateIceCandidates(candidates: any, options: {preferIpv6Server: boolea
|
||||
|
||||
const newCandidate = (candidate: string) => {
|
||||
return {
|
||||
'candidate': candidate,
|
||||
'messageType': 'iceCandidate',
|
||||
'sdpMLineIndex': '0',
|
||||
'sdpMid': '0',
|
||||
candidate: candidate,
|
||||
messageType: 'iceCandidate',
|
||||
sdpMLineIndex: '0',
|
||||
sdpMid: '0',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -105,7 +105,7 @@ export async function patchIceCandidates(request: Request, consoleAddrs?: Remote
|
||||
}
|
||||
|
||||
const options = {
|
||||
preferIpv6Server: getPref(PrefKey.PREFER_IPV6_SERVER),
|
||||
preferIpv6Server: getPref(PrefKey.SERVER_PREFER_IPV6),
|
||||
consoleAddrs: consoleAddrs,
|
||||
};
|
||||
|
||||
|
16
src/utils/region.ts
Normal file → Executable file
16
src/utils/region.ts
Normal file → Executable file
@@ -4,17 +4,21 @@ import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
|
||||
export function getPreferredServerRegion(shortName = false): string | null {
|
||||
let preferredRegion = getPref(PrefKey.SERVER_REGION);
|
||||
if (preferredRegion in STATES.serverRegions) {
|
||||
if (shortName && STATES.serverRegions[preferredRegion].shortName) {
|
||||
return STATES.serverRegions[preferredRegion].shortName;
|
||||
let preferredRegion = getPref<ServerRegionName>(PrefKey.SERVER_REGION);
|
||||
const serverRegions = STATES.serverRegions;
|
||||
|
||||
// Return preferred region
|
||||
if (preferredRegion in serverRegions) {
|
||||
if (shortName && serverRegions[preferredRegion].shortName) {
|
||||
return serverRegions[preferredRegion].shortName;
|
||||
} else {
|
||||
return preferredRegion;
|
||||
}
|
||||
}
|
||||
|
||||
for (let regionName in STATES.serverRegions) {
|
||||
const region = STATES.serverRegions[regionName];
|
||||
// Get default region
|
||||
for (let regionName in serverRegions) {
|
||||
const region = serverRegions[regionName];
|
||||
if (!region.isDefault) {
|
||||
continue;
|
||||
}
|
||||
|
2
src/utils/root-dialog-observer.ts
Normal file → Executable file
2
src/utils/root-dialog-observer.ts
Normal file → Executable file
@@ -14,7 +14,6 @@ export class RootDialogObserver {
|
||||
icon: BxIcon.CREATE_SHORTCUT,
|
||||
label: t('create-shortcut'),
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_CASE | ButtonStyle.NORMAL_LINK,
|
||||
tabIndex: 0,
|
||||
onClick: e => {
|
||||
window.BX_EXPOSED.dialogRoutes?.closeAll();
|
||||
|
||||
@@ -27,7 +26,6 @@ export class RootDialogObserver {
|
||||
icon: BxIcon.DOWNLOAD,
|
||||
label: t('wallpaper'),
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_CASE | ButtonStyle.NORMAL_LINK,
|
||||
tabIndex: 0,
|
||||
onClick: e => {
|
||||
window.BX_EXPOSED.dialogRoutes?.closeAll();
|
||||
|
||||
|
2
src/utils/screenshot-manager.ts
Normal file → Executable file
2
src/utils/screenshot-manager.ts
Normal file → Executable file
@@ -1,9 +1,9 @@
|
||||
import { StreamPlayerType } from "@enums/stream-player";
|
||||
import { AppInterface, STATES } from "./global";
|
||||
import { CE } from "./html";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
import { StreamPlayerType } from "@/enums/pref-values";
|
||||
|
||||
|
||||
export class ScreenshotManager {
|
||||
|
0
src/utils/sdp.ts
Normal file → Executable file
0
src/utils/sdp.ts
Normal file → Executable file
277
src/utils/setting-element.ts
Normal file → Executable file
277
src/utils/setting-element.ts
Normal file → Executable file
@@ -1,15 +1,14 @@
|
||||
import type { PreferenceSetting } from "@/types/preferences";
|
||||
import { CE } from "@utils/html";
|
||||
import { setNearby } from "./navigation-utils";
|
||||
import { CE, escapeCssSelector } from "@utils/html";
|
||||
import type { PrefKey } from "@/enums/pref-keys";
|
||||
import type { BaseSettingsStore } from "./settings-storages/base-settings-storage";
|
||||
import { type MultipleOptionsParams, type NumberStepperParams } from "@/types/setting-definition";
|
||||
import { type BaseSettingDefinition, type MultipleOptionsParams, type NumberStepperParams } from "@/types/setting-definition";
|
||||
import { BxEvent } from "./bx-event";
|
||||
import { BxNumberStepper } from "@/web-components/bx-number-stepper";
|
||||
|
||||
export enum SettingElementType {
|
||||
OPTIONS = 'options',
|
||||
MULTIPLE_OPTIONS = 'multiple-options',
|
||||
NUMBER = 'number',
|
||||
NUMBER_STEPPER = 'number-stepper',
|
||||
CHECKBOX = 'checkbox',
|
||||
}
|
||||
@@ -23,7 +22,7 @@ export interface BxHtmlSettingElement extends HTMLElement, BxBaseSettingElement
|
||||
export interface BxSelectSettingElement extends HTMLSelectElement, BxBaseSettingElement {}
|
||||
|
||||
export class SettingElement {
|
||||
static #renderOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any): BxSelectSettingElement {
|
||||
private static renderOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any): BxSelectSettingElement {
|
||||
const $control = CE<BxSelectSettingElement>('select', {
|
||||
// title: setting.label,
|
||||
tabindex: 0,
|
||||
@@ -62,16 +61,15 @@ export class SettingElement {
|
||||
return $control;
|
||||
}
|
||||
|
||||
static #renderMultipleOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any, params: MultipleOptionsParams={}): BxSelectSettingElement {
|
||||
private static renderMultipleOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any, params: MultipleOptionsParams={}): BxSelectSettingElement {
|
||||
const $control = CE<BxSelectSettingElement>('select', {
|
||||
// title: setting.label,
|
||||
multiple: true,
|
||||
tabindex: 0,
|
||||
});
|
||||
|
||||
if (params && params.size) {
|
||||
$control.setAttribute('size', params.size.toString());
|
||||
}
|
||||
const size = params.size ? params.size : Object.keys(setting.multipleOptions!).length;
|
||||
$control.setAttribute('size', size.toString());
|
||||
|
||||
for (let value in setting.multipleOptions) {
|
||||
const label = setting.multipleOptions[value];
|
||||
@@ -111,29 +109,8 @@ export class SettingElement {
|
||||
return $control;
|
||||
}
|
||||
|
||||
static #renderNumber(key: string, setting: PreferenceSetting, currentValue: any, onChange: any) {
|
||||
const $control = CE<HTMLInputElement>('input', {
|
||||
tabindex: 0,
|
||||
type: 'number',
|
||||
min: setting.min,
|
||||
max: setting.max,
|
||||
});
|
||||
|
||||
$control.value = currentValue;
|
||||
onChange && $control.addEventListener('input', (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
const value = Math.max(setting.min!, Math.min(setting.max!, parseInt(target.value)));
|
||||
target.value = value.toString();
|
||||
|
||||
!(e as any).ignoreOnChange && onChange(e, value);
|
||||
});
|
||||
|
||||
return $control;
|
||||
}
|
||||
|
||||
static #renderCheckbox(key: string, setting: PreferenceSetting, currentValue: any, onChange: any) {
|
||||
const $control = CE('input', {'type': 'checkbox', 'tabindex': 0}) as HTMLInputElement;
|
||||
private static renderCheckbox(key: string, setting: PreferenceSetting, currentValue: any, onChange: any) {
|
||||
const $control = CE('input', {type: 'checkbox', tabindex: 0}) as HTMLInputElement;
|
||||
$control.checked = currentValue;
|
||||
|
||||
onChange && $control.addEventListener('input', e => {
|
||||
@@ -147,233 +124,25 @@ export class SettingElement {
|
||||
return $control;
|
||||
}
|
||||
|
||||
static #renderNumberStepper(key: string, setting: PreferenceSetting, value: any, onChange: any, options: NumberStepperParams={}) {
|
||||
options = options || {};
|
||||
options.suffix = options.suffix || '';
|
||||
options.disabled = !!options.disabled;
|
||||
options.hideSlider = !!options.hideSlider;
|
||||
|
||||
let $text: HTMLSpanElement;
|
||||
let $btnDec: HTMLButtonElement;
|
||||
let $btnInc: HTMLButtonElement;
|
||||
let $range: HTMLInputElement | null = null;
|
||||
|
||||
let controlValue = value;
|
||||
|
||||
const MIN = options.reverse ? -setting.max! : setting.min!;
|
||||
const MAX = options.reverse ? -setting.min! : setting.max!;
|
||||
const STEPS = Math.max(setting.steps || 1, 1);
|
||||
|
||||
let intervalId: number | null;
|
||||
let isHolding = false;
|
||||
|
||||
const clearIntervalId = () => {
|
||||
intervalId && clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
|
||||
const renderTextValue = (value: any) => {
|
||||
value = parseInt(value as string);
|
||||
|
||||
let textContent = null;
|
||||
if (options.customTextValue) {
|
||||
textContent = options.customTextValue(value);
|
||||
}
|
||||
|
||||
if (textContent === null) {
|
||||
textContent = value.toString() + options.suffix;
|
||||
}
|
||||
|
||||
return textContent;
|
||||
};
|
||||
|
||||
const updateButtonsVisibility = () => {
|
||||
$btnDec.classList.toggle('bx-inactive', controlValue === MIN);
|
||||
$btnInc.classList.toggle('bx-inactive', controlValue === MAX);
|
||||
|
||||
if (controlValue === MIN || controlValue === MAX) {
|
||||
clearIntervalId();
|
||||
}
|
||||
}
|
||||
|
||||
const $wrapper = CE<BxHtmlSettingElement>('div', {'class': 'bx-number-stepper', id: `bx_setting_${key}`},
|
||||
$btnDec = CE('button', {
|
||||
'data-type': 'dec',
|
||||
type: 'button',
|
||||
class: options.hideSlider ? 'bx-focusable' : '',
|
||||
tabindex: options.hideSlider ? 0 : -1,
|
||||
}, '-') as HTMLButtonElement,
|
||||
$text = CE('span', {}, renderTextValue(value)) as HTMLSpanElement,
|
||||
$btnInc = CE('button', {
|
||||
'data-type': 'inc',
|
||||
type: 'button',
|
||||
class: options.hideSlider ? 'bx-focusable' : '',
|
||||
tabindex: options.hideSlider ? 0 : -1,
|
||||
}, '+') as HTMLButtonElement,
|
||||
);
|
||||
|
||||
if (options.disabled) {
|
||||
$btnInc.disabled = true;
|
||||
$btnInc.classList.add('bx-inactive');
|
||||
|
||||
$btnDec.disabled = true;
|
||||
$btnDec.classList.add('bx-inactive');
|
||||
|
||||
($wrapper as any).disabled = true;
|
||||
return $wrapper;
|
||||
}
|
||||
|
||||
$range = CE<HTMLInputElement>('input', {
|
||||
id: `bx_inp_setting_${key}`,
|
||||
type: 'range',
|
||||
min: MIN,
|
||||
max: MAX,
|
||||
value: options.reverse ? -value : value,
|
||||
step: STEPS,
|
||||
tabindex: 0,
|
||||
});
|
||||
|
||||
options.hideSlider && $range.classList.add('bx-gone');
|
||||
|
||||
$range.addEventListener('input', e => {
|
||||
value = parseInt((e.target as HTMLInputElement).value);
|
||||
if (options.reverse) {
|
||||
value *= -1;
|
||||
}
|
||||
|
||||
const valueChanged = controlValue !== value;
|
||||
if (!valueChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
controlValue = options.reverse ? -value : value;
|
||||
updateButtonsVisibility();
|
||||
$text.textContent = renderTextValue(value);
|
||||
|
||||
!(e as any).ignoreOnChange && onChange && onChange(e, value);
|
||||
});
|
||||
|
||||
$wrapper.addEventListener('input', e => {
|
||||
BxEvent.dispatch($range, 'input');
|
||||
});
|
||||
$wrapper.appendChild($range);
|
||||
|
||||
if (options.ticks || options.exactTicks) {
|
||||
const markersId = `markers-${key}`;
|
||||
const $markers = CE('datalist', {id: markersId});
|
||||
$range.setAttribute('list', markersId);
|
||||
|
||||
if (options.exactTicks) {
|
||||
let start = Math.max(Math.floor(setting.min! / options.exactTicks), 1) * options.exactTicks;
|
||||
|
||||
if (start === setting.min!) {
|
||||
start += options.exactTicks;
|
||||
}
|
||||
|
||||
for (let i = start; i < setting.max!; i += options.exactTicks) {
|
||||
$markers.appendChild(CE<HTMLOptionElement>('option', {
|
||||
value: options.reverse ? -i : i,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
for (let i = MIN + options.ticks!; i < MAX; i += options.ticks!) {
|
||||
$markers.appendChild(CE<HTMLOptionElement>('option', {value: i}));
|
||||
}
|
||||
}
|
||||
$wrapper.appendChild($markers);
|
||||
}
|
||||
|
||||
updateButtonsVisibility();
|
||||
|
||||
const buttonPressed = (e: Event, $btn: HTMLElement) => {
|
||||
let value = parseInt(controlValue);
|
||||
|
||||
const btnType = $btn.dataset.type;
|
||||
if (btnType === 'dec') {
|
||||
value = Math.max(MIN, value - STEPS);
|
||||
} else {
|
||||
value = Math.min(MAX, value + STEPS);
|
||||
}
|
||||
|
||||
controlValue = value;
|
||||
updateButtonsVisibility();
|
||||
|
||||
$text.textContent = renderTextValue(value);
|
||||
$range && ($range.value = value.toString());
|
||||
|
||||
onChange && onChange(e, value);
|
||||
};
|
||||
|
||||
const onClick = (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (isHolding) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $btn = (e.target as HTMLElement).closest('button') as HTMLElement;
|
||||
$btn && buttonPressed(e, $btn);
|
||||
|
||||
clearIntervalId();
|
||||
isHolding = false;
|
||||
};
|
||||
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
clearIntervalId();
|
||||
|
||||
const $btn = (e.target as HTMLElement).closest('button') as HTMLElement;
|
||||
if (!$btn) {
|
||||
return;
|
||||
}
|
||||
|
||||
isHolding = true;
|
||||
e.preventDefault();
|
||||
|
||||
intervalId = window.setInterval((e: Event) => {
|
||||
buttonPressed(e, $btn);
|
||||
}, 200);
|
||||
|
||||
window.addEventListener('pointerup', onPointerUp, {once: true});
|
||||
window.addEventListener('pointercancel', onPointerUp, {once: true});
|
||||
};
|
||||
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
clearIntervalId();
|
||||
isHolding = false;
|
||||
};
|
||||
|
||||
const onContextMenu = (e: Event) => e.preventDefault();
|
||||
|
||||
// Custom method
|
||||
$wrapper.setValue = (value: any) => {
|
||||
$text.textContent = renderTextValue(value);
|
||||
$range.value = options.reverse ? -value : value;
|
||||
};
|
||||
|
||||
$wrapper.addEventListener('click', onClick);
|
||||
$wrapper.addEventListener('pointerdown', onPointerDown);
|
||||
$wrapper.addEventListener('contextmenu', onContextMenu);
|
||||
setNearby($wrapper, {
|
||||
focus: options.hideSlider ? $btnInc : $range,
|
||||
})
|
||||
|
||||
return $wrapper;
|
||||
private static renderNumberStepper(key: string, setting: PreferenceSetting, value: any, onChange: any, options: NumberStepperParams={}) {
|
||||
const $control = BxNumberStepper.create(key, value, setting.min!, setting.max!, options, onChange);
|
||||
return $control;
|
||||
}
|
||||
|
||||
static #METHOD_MAP = {
|
||||
[SettingElementType.OPTIONS]: SettingElement.#renderOptions,
|
||||
[SettingElementType.MULTIPLE_OPTIONS]: SettingElement.#renderMultipleOptions,
|
||||
[SettingElementType.NUMBER]: SettingElement.#renderNumber,
|
||||
[SettingElementType.NUMBER_STEPPER]: SettingElement.#renderNumberStepper,
|
||||
[SettingElementType.CHECKBOX]: SettingElement.#renderCheckbox,
|
||||
private static readonly METHOD_MAP = {
|
||||
[SettingElementType.OPTIONS]: SettingElement.renderOptions,
|
||||
[SettingElementType.MULTIPLE_OPTIONS]: SettingElement.renderMultipleOptions,
|
||||
[SettingElementType.NUMBER_STEPPER]: SettingElement.renderNumberStepper,
|
||||
[SettingElementType.CHECKBOX]: SettingElement.renderCheckbox,
|
||||
};
|
||||
|
||||
static render(type: SettingElementType, key: string, setting: PreferenceSetting, currentValue: any, onChange: any, options: any) {
|
||||
const method = SettingElement.#METHOD_MAP[type];
|
||||
static render(type: SettingElementType, key: string, setting: BaseSettingDefinition, currentValue: any, onChange: any, options: any) {
|
||||
const method = SettingElement.METHOD_MAP[type];
|
||||
// @ts-ignore
|
||||
const $control = method(...Array.from(arguments).slice(1)) as HTMLElement;
|
||||
|
||||
if (type !== SettingElementType.NUMBER_STEPPER) {
|
||||
$control.id = `bx_setting_${key}`;
|
||||
$control.id = `bx_setting_${escapeCssSelector(key)}`;
|
||||
}
|
||||
|
||||
// Add "name" property to "select" elements
|
||||
@@ -389,14 +158,12 @@ export class SettingElement {
|
||||
let currentValue = storage.getSetting(key);
|
||||
|
||||
let type;
|
||||
if ('type' in definition) {
|
||||
type = definition.type;
|
||||
} else if ('options' in definition) {
|
||||
if ('options' in definition) {
|
||||
type = SettingElementType.OPTIONS;
|
||||
} else if ('multipleOptions' in definition) {
|
||||
type = SettingElementType.MULTIPLE_OPTIONS;
|
||||
} else if (typeof definition.default === 'number') {
|
||||
type = SettingElementType.NUMBER;
|
||||
type = SettingElementType.NUMBER_STEPPER;
|
||||
} else {
|
||||
type = SettingElementType.CHECKBOX;
|
||||
}
|
||||
|
59
src/utils/settings-storages/base-settings-storage.ts
Normal file → Executable file
59
src/utils/settings-storages/base-settings-storage.ts
Normal file → Executable file
@@ -1,7 +1,6 @@
|
||||
import type { PrefKey } from "@/enums/pref-keys";
|
||||
import type { NumberStepperParams, SettingDefinitions } from "@/types/setting-definition";
|
||||
import type { NumberStepperParams, SettingAction, SettingDefinitions } from "@/types/setting-definition";
|
||||
import { BxEvent } from "../bx-event";
|
||||
import { SettingElementType } from "../setting-element";
|
||||
import { t } from "../translation";
|
||||
import { SCRIPT_VARIANT } from "../global";
|
||||
|
||||
@@ -43,6 +42,12 @@ export class BaseSettingsStore {
|
||||
}
|
||||
|
||||
const settings = JSON.parse(this.storage.getItem(this.storageKey) || '{}');
|
||||
|
||||
// Validate setting values
|
||||
for (const key in settings) {
|
||||
settings[key] = this.validateValue('get', key as PrefKey, settings[key]);
|
||||
}
|
||||
|
||||
this._settings = settings;
|
||||
|
||||
return settings;
|
||||
@@ -58,35 +63,34 @@ export class BaseSettingsStore {
|
||||
return this.definitions[key];
|
||||
}
|
||||
|
||||
getSetting(key: PrefKey, checkUnsupported = true) {
|
||||
if (typeof key === 'undefined') {
|
||||
debugger;
|
||||
return;
|
||||
}
|
||||
|
||||
getSetting<T=boolean>(key: PrefKey, checkUnsupported = true): T {
|
||||
const definition = this.definitions[key];
|
||||
|
||||
// Return default value if build variant is different
|
||||
if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) {
|
||||
return definition.default;
|
||||
return definition.default as T;
|
||||
}
|
||||
|
||||
// Return default value if the feature is not supported
|
||||
if (checkUnsupported && definition.unsupported) {
|
||||
return definition.default;
|
||||
if ('unsupportedValue' in definition) {
|
||||
return definition.unsupportedValue as T;
|
||||
} else {
|
||||
return definition.default as T;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(key in this.settings)) {
|
||||
this.settings[key] = this.validateValue(key, null);
|
||||
this.settings[key] = this.validateValue('get', key, null);
|
||||
}
|
||||
|
||||
return this.settings[key];
|
||||
return this.settings[key] as T;
|
||||
}
|
||||
|
||||
setSetting(key: PrefKey, value: any, emitEvent = false) {
|
||||
value = this.validateValue(key, value);
|
||||
setSetting<T=any>(key: PrefKey, value: T, emitEvent = false) {
|
||||
value = this.validateValue('set', key, value);
|
||||
|
||||
this.settings[key] = value;
|
||||
this.settings[key] = this.validateValue('get', key, value);
|
||||
this.saveSettings();
|
||||
|
||||
emitEvent && BxEvent.dispatch(window, BxEvent.SETTINGS_CHANGED, {
|
||||
@@ -102,7 +106,7 @@ export class BaseSettingsStore {
|
||||
this.storage.setItem(this.storageKey, JSON.stringify(this.settings));
|
||||
}
|
||||
|
||||
private validateValue(key: PrefKey, value: any) {
|
||||
private validateValue(action: SettingAction, key: PrefKey, value: any) {
|
||||
const def = this.definitions[key];
|
||||
if (!def) {
|
||||
return value;
|
||||
@@ -112,6 +116,11 @@ export class BaseSettingsStore {
|
||||
value = def.default;
|
||||
}
|
||||
|
||||
// Transform value before validating
|
||||
if (def.transformValue && action === 'get') {
|
||||
value = def.transformValue.get.call(def, value);
|
||||
}
|
||||
|
||||
if ('min' in def) {
|
||||
value = Math.max(def.min!, value);
|
||||
}
|
||||
@@ -120,8 +129,10 @@ export class BaseSettingsStore {
|
||||
value = Math.min(def.max!, value);
|
||||
}
|
||||
|
||||
if ('options' in def && !(value in def.options!)) {
|
||||
value = def.default;
|
||||
if ('options' in def) {
|
||||
if (!(value in def.options)) {
|
||||
value = def.default;
|
||||
}
|
||||
} else if ('multipleOptions' in def) {
|
||||
if (value.length) {
|
||||
const validOptions = Object.keys(def.multipleOptions!);
|
||||
@@ -135,6 +146,11 @@ export class BaseSettingsStore {
|
||||
}
|
||||
}
|
||||
|
||||
// Transform value before setting
|
||||
if (def.transformValue && action === 'set') {
|
||||
value = def.transformValue.set.call(def, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -144,10 +160,13 @@ export class BaseSettingsStore {
|
||||
|
||||
getValueText(key: PrefKey, value: any): string {
|
||||
const definition = this.definitions[key];
|
||||
if (definition.type === SettingElementType.NUMBER_STEPPER) {
|
||||
if ('min' in definition) {
|
||||
const params = (definition as any).params as NumberStepperParams;
|
||||
if (params.customTextValue) {
|
||||
const text = params.customTextValue(value);
|
||||
if (definition.transformValue) {
|
||||
value = definition.transformValue.get.call(definition, value);
|
||||
}
|
||||
const text = params.customTextValue(value, definition.min, definition.max);
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
|
363
src/utils/settings-storages/global-settings-storage.ts
Normal file → Executable file
363
src/utils/settings-storages/global-settings-storage.ts
Normal file → Executable file
@@ -1,7 +1,5 @@
|
||||
import { BypassServers } from "@/enums/bypass-servers";
|
||||
import { PrefKey, StorageKey } from "@/enums/pref-keys";
|
||||
import { StreamPlayerType, StreamVideoProcessing } from "@/enums/stream-player";
|
||||
import { UiSection } from "@/enums/ui-sections";
|
||||
import { UserAgentProfile } from "@/enums/user-agent";
|
||||
import { type SettingDefinition, type SettingDefinitions } from "@/types/setting-definition";
|
||||
import { BX_FLAGS } from "../bx-flags";
|
||||
@@ -10,37 +8,11 @@ import { CE } from "../html";
|
||||
import { t, SUPPORTED_LANGUAGES } from "../translation";
|
||||
import { UserAgent } from "../user-agent";
|
||||
import { BaseSettingsStore as BaseSettingsStorage } from "./base-settings-storage";
|
||||
import { SettingElementType } from "../setting-element";
|
||||
import { StreamStat } from "../stream-stats-collector";
|
||||
|
||||
|
||||
export const enum StreamResolution {
|
||||
DIM_720P = '720p',
|
||||
DIM_1080P = '1080p',
|
||||
}
|
||||
|
||||
export const enum CodecProfile {
|
||||
DEFAULT = 'default',
|
||||
LOW = 'low',
|
||||
NORMAL = 'normal',
|
||||
HIGH = 'high',
|
||||
};
|
||||
|
||||
export const enum StreamTouchController {
|
||||
DEFAULT = 'default',
|
||||
ALL = 'all',
|
||||
OFF = 'off',
|
||||
}
|
||||
|
||||
export const enum ControllerDeviceVibration {
|
||||
ON = 'on',
|
||||
AUTO = 'auto',
|
||||
OFF = 'off',
|
||||
}
|
||||
|
||||
|
||||
export type GameBarPosition = 'bottom-left' | 'bottom-right' | 'off';
|
||||
export type GameBarPositionOptions = Record<GameBarPosition, string>;
|
||||
import { CodecProfile, StreamResolution, TouchControllerMode, TouchControllerStyleStandard, TouchControllerStyleCustom, GameBarPosition, DeviceVibrationMode, NativeMkbMode, UiLayout, UiSection, StreamPlayerType, StreamVideoProcessing, VideoRatio, StreamStat } from "@/enums/pref-values";
|
||||
import { MkbMappingDefaultPresetId } from "../local-db/mkb-mapping-presets-table";
|
||||
import { KeyboardShortcutDefaultId } from "../local-db/keyboard-shortcuts-table";
|
||||
import { GhPagesUtils } from "../gh-pages";
|
||||
import { BxEvent } from "../bx-event";
|
||||
|
||||
|
||||
function getSupportedCodecProfiles() {
|
||||
@@ -101,18 +73,18 @@ function getSupportedCodecProfiles() {
|
||||
|
||||
export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
private static readonly DEFINITIONS = {
|
||||
[PrefKey.LAST_UPDATE_CHECK]: {
|
||||
[PrefKey.VERSION_LAST_CHECK]: {
|
||||
default: 0,
|
||||
},
|
||||
[PrefKey.LATEST_VERSION]: {
|
||||
[PrefKey.VERSION_LATEST]: {
|
||||
default: '',
|
||||
},
|
||||
[PrefKey.CURRENT_VERSION]: {
|
||||
[PrefKey.VERSION_CURRENT]: {
|
||||
default: '',
|
||||
},
|
||||
[PrefKey.BETTER_XCLOUD_LOCALE]: {
|
||||
[PrefKey.SCRIPT_LOCALE]: {
|
||||
label: t('language'),
|
||||
default: localStorage.getItem('better_xcloud_locale') || 'en-US',
|
||||
default: localStorage.getItem(StorageKey.LOCALE) || 'en-US',
|
||||
options: SUPPORTED_LANGUAGES,
|
||||
},
|
||||
[PrefKey.SERVER_REGION]: {
|
||||
@@ -167,22 +139,24 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
'zh-TW': '中文 (繁體)',
|
||||
},
|
||||
},
|
||||
[PrefKey.STREAM_TARGET_RESOLUTION]: {
|
||||
[PrefKey.STREAM_RESOLUTION]: {
|
||||
label: t('target-resolution'),
|
||||
default: 'auto',
|
||||
options: {
|
||||
auto: t('default'),
|
||||
[StreamResolution.DIM_720P]: '720p',
|
||||
[StreamResolution.DIM_1080P]: '1080p',
|
||||
[StreamResolution.DIM_1080P_HQ]: '1080p (HQ)',
|
||||
},
|
||||
suggest: {
|
||||
lowest: StreamResolution.DIM_720P,
|
||||
highest: StreamResolution.DIM_1080P,
|
||||
highest: StreamResolution.DIM_1080P_HQ,
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.STREAM_CODEC_PROFILE]: {
|
||||
label: t('visual-quality'),
|
||||
default: 'default',
|
||||
default: CodecProfile.DEFAULT,
|
||||
options: getSupportedCodecProfiles(),
|
||||
ready: (setting: SettingDefinition) => {
|
||||
const options = (setting as any).options;
|
||||
@@ -199,7 +173,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
};
|
||||
},
|
||||
},
|
||||
[PrefKey.PREFER_IPV6_SERVER]: {
|
||||
[PrefKey.SERVER_PREFER_IPV6]: {
|
||||
label: t('prefer-ipv6-server'),
|
||||
default: false,
|
||||
},
|
||||
@@ -210,11 +184,11 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.SKIP_SPLASH_VIDEO]: {
|
||||
[PrefKey.UI_SKIP_SPLASH_VIDEO]: {
|
||||
label: t('skip-splash-video'),
|
||||
default: false,
|
||||
},
|
||||
[PrefKey.HIDE_DOTS_ICON]: {
|
||||
[PrefKey.UI_HIDE_SYSTEM_MENU_ICON]: {
|
||||
label: t('hide-system-menu-icon'),
|
||||
default: false,
|
||||
},
|
||||
@@ -228,66 +202,62 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
note: t('combine-audio-video-streams-summary'),
|
||||
},
|
||||
|
||||
[PrefKey.STREAM_TOUCH_CONTROLLER]: {
|
||||
[PrefKey.TOUCH_CONTROLLER_MODE]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('tc-availability'),
|
||||
default: StreamTouchController.ALL,
|
||||
default: TouchControllerMode.ALL,
|
||||
options: {
|
||||
[StreamTouchController.DEFAULT]: t('default'),
|
||||
[StreamTouchController.ALL]: t('tc-all-games'),
|
||||
[StreamTouchController.OFF]: t('off'),
|
||||
[TouchControllerMode.DEFAULT]: t('default'),
|
||||
[TouchControllerMode.OFF]: t('off'),
|
||||
[TouchControllerMode.ALL]: t('tc-all-games'),
|
||||
},
|
||||
|
||||
unsupported: !STATES.userAgent.capabilities.touch,
|
||||
ready: (setting: SettingDefinition) => {
|
||||
if (setting.unsupported) {
|
||||
setting.default = StreamTouchController.DEFAULT;
|
||||
}
|
||||
},
|
||||
unsupportedValue: TouchControllerMode.DEFAULT,
|
||||
},
|
||||
[PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF]: {
|
||||
[PrefKey.TOUCH_CONTROLLER_AUTO_OFF]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('tc-auto-off'),
|
||||
default: false,
|
||||
unsupported: !STATES.userAgent.capabilities.touch,
|
||||
},
|
||||
[PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY]: {
|
||||
[PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY]: {
|
||||
requiredVariants: 'full',
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
label: t('tc-default-opacity'),
|
||||
default: 100,
|
||||
min: 10,
|
||||
max: 100,
|
||||
steps: 10,
|
||||
params: {
|
||||
steps: 10,
|
||||
suffix: '%',
|
||||
ticks: 10,
|
||||
hideSlider: true,
|
||||
},
|
||||
unsupported: !STATES.userAgent.capabilities.touch,
|
||||
},
|
||||
[PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: {
|
||||
[PrefKey.TOUCH_CONTROLLER_STYLE_STANDARD]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('tc-standard-layout-style'),
|
||||
default: 'default',
|
||||
default: TouchControllerStyleStandard.DEFAULT,
|
||||
options: {
|
||||
default: t('default'),
|
||||
white: t('tc-all-white'),
|
||||
muted: t('tc-muted-colors'),
|
||||
[TouchControllerStyleStandard.DEFAULT]: t('default'),
|
||||
[TouchControllerStyleStandard.WHITE]: t('tc-all-white'),
|
||||
[TouchControllerStyleStandard.MUTED]: t('tc-muted-colors'),
|
||||
},
|
||||
unsupported: !STATES.userAgent.capabilities.touch,
|
||||
},
|
||||
[PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM]: {
|
||||
[PrefKey.TOUCH_CONTROLLER_STYLE_CUSTOM]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('tc-custom-layout-style'),
|
||||
default: 'default',
|
||||
default: TouchControllerStyleCustom.DEFAULT,
|
||||
options: {
|
||||
default: t('default'),
|
||||
muted: t('tc-muted-colors'),
|
||||
[TouchControllerStyleCustom.DEFAULT]: t('default'),
|
||||
[TouchControllerStyleCustom.MUTED]: t('tc-muted-colors'),
|
||||
},
|
||||
unsupported: !STATES.userAgent.capabilities.touch,
|
||||
},
|
||||
|
||||
[PrefKey.STREAM_SIMPLIFY_MENU]: {
|
||||
[PrefKey.UI_SIMPLIFY_STREAM_MENU]: {
|
||||
label: t('simplify-stream-menu'),
|
||||
default: false,
|
||||
},
|
||||
@@ -296,27 +266,35 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
label: t('hide-idle-cursor'),
|
||||
default: false,
|
||||
},
|
||||
[PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG]: {
|
||||
[PrefKey.UI_DISABLE_FEEDBACK_DIALOG]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('disable-post-stream-feedback-dialog'),
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.BITRATE_VIDEO_MAX]: {
|
||||
[PrefKey.STREAM_MAX_VIDEO_BITRATE]: {
|
||||
requiredVariants: 'full',
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
label: t('bitrate-video-maximum'),
|
||||
note: '⚠️ ' + t('unexpected-behavior'),
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 14 * 1024 * 1000,
|
||||
steps: 100 * 1024,
|
||||
min: 1024 * 100,
|
||||
max: 15 * 1024 * 1000,
|
||||
transformValue: {
|
||||
get(value) {
|
||||
return value === 0 ? this.max : value;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
return value === this.max ? 0 : value;
|
||||
},
|
||||
},
|
||||
params: {
|
||||
exactTicks: 5 * 1024 * 1000,
|
||||
customTextValue: (value: any) => {
|
||||
steps: 100 * 1024,
|
||||
exactTicks: 5 * 1024 * 1000,
|
||||
customTextValue: (value: any, min, max) => {
|
||||
value = parseInt(value);
|
||||
|
||||
if (value === 0) {
|
||||
if (value === max) {
|
||||
return t('unlimited');
|
||||
} else {
|
||||
return (value / (1024 * 1000)).toFixed(1) + ' Mb/s';
|
||||
@@ -331,12 +309,12 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
[PrefKey.GAME_BAR_POSITION]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('position'),
|
||||
default: 'bottom-left' satisfies GameBarPosition,
|
||||
default: GameBarPosition.BOTTOM_LEFT,
|
||||
options: {
|
||||
'bottom-left': t('bottom-left'),
|
||||
'bottom-right': t('bottom-right'),
|
||||
'off': t('off'),
|
||||
} satisfies GameBarPositionOptions,
|
||||
[GameBarPosition.OFF]: t('off'),
|
||||
[GameBarPosition.BOTTOM_LEFT]: t('bottom-left'),
|
||||
[GameBarPosition.BOTTOM_RIGHT]: t('bottom-right'),
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.LOCAL_CO_OP_ENABLED]: {
|
||||
@@ -349,58 +327,43 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
}, t('enable-local-co-op-support-note')),
|
||||
},
|
||||
|
||||
/*
|
||||
[Preferences.LOCAL_CO_OP_SEPARATE_TOUCH_CONTROLLER]: {
|
||||
default: false,
|
||||
'note': t('separate-touch-controller-note'),
|
||||
},
|
||||
*/
|
||||
|
||||
[PrefKey.CONTROLLER_SHOW_CONNECTION_STATUS]: {
|
||||
[PrefKey.UI_CONTROLLER_SHOW_STATUS]: {
|
||||
label: t('show-controller-connection-status'),
|
||||
default: true,
|
||||
},
|
||||
|
||||
[PrefKey.CONTROLLER_ENABLE_VIBRATION]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('controller-vibration'),
|
||||
default: true,
|
||||
},
|
||||
|
||||
[PrefKey.CONTROLLER_DEVICE_VIBRATION]: {
|
||||
[PrefKey.DEVICE_VIBRATION_MODE]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('device-vibration'),
|
||||
default: ControllerDeviceVibration.OFF,
|
||||
default: DeviceVibrationMode.OFF,
|
||||
options: {
|
||||
[ControllerDeviceVibration.ON]: t('on'),
|
||||
[ControllerDeviceVibration.AUTO]: t('device-vibration-not-using-gamepad'),
|
||||
[ControllerDeviceVibration.OFF]: t('off'),
|
||||
[DeviceVibrationMode.OFF]: t('off'),
|
||||
[DeviceVibrationMode.ON]: t('on'),
|
||||
[DeviceVibrationMode.AUTO]: t('device-vibration-not-using-gamepad'),
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.CONTROLLER_VIBRATION_INTENSITY]: {
|
||||
[PrefKey.DEVICE_VIBRATION_INTENSITY]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('vibration-intensity'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 100,
|
||||
min: 0,
|
||||
default: 50,
|
||||
min: 10,
|
||||
max: 100,
|
||||
steps: 10,
|
||||
params: {
|
||||
steps: 10,
|
||||
suffix: '%',
|
||||
ticks: 10,
|
||||
exactTicks: 20,
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.CONTROLLER_POLLING_RATE]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('polling-rate'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 4,
|
||||
min: 4,
|
||||
max: 60,
|
||||
steps: 4,
|
||||
params: {
|
||||
steps: 4,
|
||||
exactTicks: 20,
|
||||
reverse: true,
|
||||
customTextValue(value: any) {
|
||||
@@ -420,7 +383,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
requiredVariants: 'full',
|
||||
label: t('enable-mkb'),
|
||||
default: false,
|
||||
unsupported: !STATES.userAgent.capabilities.mkb,
|
||||
unsupported: !STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb,
|
||||
ready: (setting: SettingDefinition) => {
|
||||
let note;
|
||||
let url;
|
||||
@@ -439,24 +402,39 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.NATIVE_MKB_ENABLED]: {
|
||||
[PrefKey.NATIVE_MKB_MODE]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('native-mkb'),
|
||||
default: 'default',
|
||||
default: NativeMkbMode.DEFAULT,
|
||||
options: {
|
||||
default: t('default'),
|
||||
on: t('on'),
|
||||
off: t('off'),
|
||||
[NativeMkbMode.DEFAULT]: t('default'),
|
||||
[NativeMkbMode.OFF]: t('off'),
|
||||
[NativeMkbMode.ON]: t('on'),
|
||||
},
|
||||
ready: (setting: SettingDefinition) => {
|
||||
if (AppInterface) {
|
||||
if (STATES.browser.capabilities.emulatedNativeMkb) {
|
||||
} else if (UserAgent.isMobile()) {
|
||||
setting.unsupported = true;
|
||||
setting.default = 'off';
|
||||
delete (setting as any).options['default'];
|
||||
delete (setting as any).options['on'];
|
||||
setting.unsupportedValue = NativeMkbMode.OFF;
|
||||
delete (setting as any).options[NativeMkbMode.DEFAULT];
|
||||
delete (setting as any).options[NativeMkbMode.ON];
|
||||
} else {
|
||||
delete (setting as any).options['on'];
|
||||
delete (setting as any).options[NativeMkbMode.ON];
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.FORCE_NATIVE_MKB_GAMES]: {
|
||||
label: t('force-native-mkb-games'),
|
||||
default: [],
|
||||
unsupported: !AppInterface && UserAgent.isMobile(),
|
||||
ready: (setting: SettingDefinition) => {
|
||||
if (!setting.unsupported) {
|
||||
(setting as any).multipleOptions = GhPagesUtils.getNativeMkbCustomList(true);
|
||||
|
||||
window.addEventListener(BxEvent.GH_PAGES_FORCE_NATIVE_MKB_UPDATED, e => {
|
||||
(setting as any).multipleOptions = GhPagesUtils.getNativeMkbCustomList();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -464,12 +442,11 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
[PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('horizontal-scroll-sensitivity'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 100 * 100,
|
||||
steps: 10,
|
||||
params: {
|
||||
steps: 10,
|
||||
exactTicks: 20 * 100,
|
||||
customTextValue: (value: any) => {
|
||||
if (!value) {
|
||||
@@ -484,12 +461,11 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
[PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('vertical-scroll-sensitivity'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 100 * 100,
|
||||
steps: 10,
|
||||
params: {
|
||||
steps: 10,
|
||||
exactTicks: 20 * 100,
|
||||
customTextValue: (value: any) => {
|
||||
if (!value) {
|
||||
@@ -501,31 +477,60 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.MKB_DEFAULT_PRESET_ID]: {
|
||||
[PrefKey.MKB_P1_MAPPING_PRESET_ID]: {
|
||||
requiredVariants: 'full',
|
||||
default: MkbMappingDefaultPresetId.DEFAULT,
|
||||
},
|
||||
|
||||
[PrefKey.MKB_P1_SLOT]: {
|
||||
requiredVariants: 'full',
|
||||
default: 1,
|
||||
min: 1,
|
||||
max: 4,
|
||||
params: {
|
||||
hideSlider: true,
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.MKB_P2_MAPPING_PRESET_ID]: {
|
||||
requiredVariants: 'full',
|
||||
default: MkbMappingDefaultPresetId.OFF,
|
||||
},
|
||||
|
||||
[PrefKey.MKB_P2_SLOT]: {
|
||||
requiredVariants: 'full',
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 4,
|
||||
params: {
|
||||
hideSlider: true,
|
||||
customTextValue(value) {
|
||||
value = parseInt(value);
|
||||
return (value === 0) ? t('off') : value.toString();
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.MKB_ABSOLUTE_MOUSE]: {
|
||||
[PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID]: {
|
||||
requiredVariants: 'full',
|
||||
default: false,
|
||||
default: KeyboardShortcutDefaultId.DEFAULT,
|
||||
},
|
||||
|
||||
[PrefKey.REDUCE_ANIMATIONS]: {
|
||||
[PrefKey.UI_REDUCE_ANIMATIONS]: {
|
||||
label: t('reduce-animations'),
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.UI_LOADING_SCREEN_GAME_ART]: {
|
||||
[PrefKey.LOADING_SCREEN_GAME_ART]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('show-game-art'),
|
||||
default: true,
|
||||
},
|
||||
[PrefKey.UI_LOADING_SCREEN_WAIT_TIME]: {
|
||||
[PrefKey.LOADING_SCREEN_SHOW_WAIT_TIME]: {
|
||||
label: t('show-wait-time'),
|
||||
default: true,
|
||||
},
|
||||
[PrefKey.UI_LOADING_SCREEN_ROCKET]: {
|
||||
[PrefKey.LOADING_SCREEN_ROCKET]: {
|
||||
label: t('rocket-animation'),
|
||||
default: 'show',
|
||||
options: {
|
||||
@@ -543,11 +548,11 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
[PrefKey.UI_LAYOUT]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('layout'),
|
||||
default: 'default',
|
||||
default: UiLayout.DEFAULT,
|
||||
options: {
|
||||
default: t('default'),
|
||||
normal: t('normal'),
|
||||
tv: t('smart-tv'),
|
||||
[UiLayout.DEFAULT]: t('default'),
|
||||
[UiLayout.NORMAL]: t('normal'),
|
||||
[UiLayout.TV]: t('smart-tv'),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -565,18 +570,24 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
[UiSection.FRIENDS]: t('section-play-with-friends'),
|
||||
[UiSection.NATIVE_MKB]: t('section-native-mkb'),
|
||||
[UiSection.TOUCH]: t('section-touch'),
|
||||
// [UiSection.BOYG]: t('section-byog'),
|
||||
[UiSection.MOST_POPULAR]: t('section-most-popular'),
|
||||
[UiSection.ALL_GAMES]: t('section-all-games'),
|
||||
},
|
||||
params: {
|
||||
size: 6,
|
||||
size: 0,
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.BYOG_DISABLED]: {
|
||||
label: t('disable-byog-feature'),
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('show-wait-time-in-game-card'),
|
||||
default: false,
|
||||
default: true,
|
||||
},
|
||||
|
||||
[PrefKey.BLOCK_SOCIAL_FEATURES]: {
|
||||
@@ -603,7 +614,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
},
|
||||
[PrefKey.VIDEO_PLAYER_TYPE]: {
|
||||
label: t('renderer'),
|
||||
default: 'default',
|
||||
default: StreamPlayerType.VIDEO,
|
||||
options: {
|
||||
[StreamPlayerType.VIDEO]: t('default'),
|
||||
[StreamPlayerType.WEBGL2]: t('webgl2'),
|
||||
@@ -629,7 +640,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
label: t('renderer-configuration'),
|
||||
default: 'default',
|
||||
options: {
|
||||
'default': t('default'),
|
||||
default: t('default'),
|
||||
'low-power': t('battery-saving'),
|
||||
'high-performance': t('high-performance'),
|
||||
},
|
||||
@@ -639,12 +650,11 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
},
|
||||
[PrefKey.VIDEO_MAX_FPS]: {
|
||||
label: t('max-fps'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 60,
|
||||
min: 10,
|
||||
max: 60,
|
||||
steps: 10,
|
||||
params: {
|
||||
steps: 10,
|
||||
exactTicks: 10,
|
||||
customTextValue: (value: any) => {
|
||||
value = parseInt(value);
|
||||
@@ -654,7 +664,6 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
},
|
||||
[PrefKey.VIDEO_SHARPNESS]: {
|
||||
label: t('sharpness'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 10,
|
||||
@@ -673,21 +682,20 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
[PrefKey.VIDEO_RATIO]: {
|
||||
label: t('aspect-ratio'),
|
||||
note: t('aspect-ratio-note'),
|
||||
default: '16:9',
|
||||
default: VideoRatio['16:9'],
|
||||
options: {
|
||||
'16:9': '16:9',
|
||||
'18:9': '18:9',
|
||||
'21:9': '21:9',
|
||||
'16:10': '16:10',
|
||||
'4:3': '4:3',
|
||||
[VideoRatio['16:9']]: '16:9',
|
||||
[VideoRatio['18:9']]: '18:9',
|
||||
[VideoRatio['21:9']]: '21:9',
|
||||
[VideoRatio['16:10']]: '16:10',
|
||||
[VideoRatio['4:3']]: '4:3',
|
||||
|
||||
fill: t('stretch'),
|
||||
[VideoRatio.FILL]: t('stretch'),
|
||||
//'cover': 'Cover',
|
||||
},
|
||||
},
|
||||
[PrefKey.VIDEO_SATURATION]: {
|
||||
label: t('saturation'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 100,
|
||||
min: 50,
|
||||
max: 150,
|
||||
@@ -698,7 +706,6 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
},
|
||||
[PrefKey.VIDEO_CONTRAST]: {
|
||||
label: t('contrast'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 100,
|
||||
min: 50,
|
||||
max: 150,
|
||||
@@ -709,7 +716,6 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
},
|
||||
[PrefKey.VIDEO_BRIGHTNESS]: {
|
||||
label: t('brightness'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 100,
|
||||
min: 50,
|
||||
max: 150,
|
||||
@@ -723,19 +729,18 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
label: t('enable-mic-on-startup'),
|
||||
default: false,
|
||||
},
|
||||
[PrefKey.AUDIO_ENABLE_VOLUME_CONTROL]: {
|
||||
[PrefKey.AUDIO_VOLUME_CONTROL_ENABLED]: {
|
||||
requiredVariants: 'full',
|
||||
label: t('enable-volume-control'),
|
||||
default: false,
|
||||
},
|
||||
[PrefKey.AUDIO_VOLUME]: {
|
||||
label: t('volume'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 100,
|
||||
min: 0,
|
||||
max: 600,
|
||||
steps: 10,
|
||||
params: {
|
||||
steps: 10,
|
||||
suffix: '%',
|
||||
ticks: 100,
|
||||
},
|
||||
@@ -746,21 +751,21 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
label: t('stats'),
|
||||
default: [StreamStat.PING, StreamStat.FPS, StreamStat.BITRATE, StreamStat.DECODE_TIME, StreamStat.PACKETS_LOST, StreamStat.FRAMES_LOST],
|
||||
multipleOptions: {
|
||||
[StreamStat.CLOCK]: `${StreamStat.CLOCK.toUpperCase()}: ${t('clock')}`,
|
||||
[StreamStat.PLAYTIME]: `${StreamStat.PLAYTIME.toUpperCase()}: ${t('playtime')}`,
|
||||
[StreamStat.BATTERY]: `${StreamStat.BATTERY.toUpperCase()}: ${t('battery')}`,
|
||||
[StreamStat.PING]: `${StreamStat.PING.toUpperCase()}: ${t('stat-ping')}`,
|
||||
[StreamStat.JITTER]: `${StreamStat.JITTER.toUpperCase()}: ${t('jitter')}`,
|
||||
[StreamStat.FPS]: `${StreamStat.FPS.toUpperCase()}: ${t('stat-fps')}`,
|
||||
[StreamStat.BITRATE]: `${StreamStat.BITRATE.toUpperCase()}: ${t('stat-bitrate')}`,
|
||||
[StreamStat.DECODE_TIME]: `${StreamStat.DECODE_TIME.toUpperCase()}: ${t('stat-decode-time')}`,
|
||||
[StreamStat.PACKETS_LOST]: `${StreamStat.PACKETS_LOST.toUpperCase()}: ${t('stat-packets-lost')}`,
|
||||
[StreamStat.FRAMES_LOST]: `${StreamStat.FRAMES_LOST.toUpperCase()}: ${t('stat-frames-lost')}`,
|
||||
[StreamStat.DOWNLOAD]: `${StreamStat.DOWNLOAD.toUpperCase()}: ${t('downloaded')}`,
|
||||
[StreamStat.UPLOAD]: `${StreamStat.UPLOAD.toUpperCase()}: ${t('uploaded')}`,
|
||||
[StreamStat.CLOCK]: t('clock'),
|
||||
[StreamStat.PLAYTIME]: t('playtime'),
|
||||
[StreamStat.BATTERY]: t('battery'),
|
||||
[StreamStat.PING]: t('stat-ping'),
|
||||
[StreamStat.JITTER]: t('jitter'),
|
||||
[StreamStat.FPS]: t('stat-fps'),
|
||||
[StreamStat.BITRATE]: t('stat-bitrate'),
|
||||
[StreamStat.DECODE_TIME]: t('stat-decode-time'),
|
||||
[StreamStat.PACKETS_LOST]: t('stat-packets-lost'),
|
||||
[StreamStat.FRAMES_LOST]: t('stat-frames-lost'),
|
||||
[StreamStat.DOWNLOAD]: t('downloaded'),
|
||||
[StreamStat.UPLOAD]: t('uploaded'),
|
||||
},
|
||||
params: {
|
||||
size: 6,
|
||||
size: 0,
|
||||
},
|
||||
ready: setting => {
|
||||
// Remove Battery option in unsupported browser
|
||||
@@ -768,13 +773,18 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
if (!STATES.browser.capabilities.batteryApi) {
|
||||
delete multipleOptions[StreamStat.BATTERY];
|
||||
}
|
||||
|
||||
// Update texts
|
||||
for (const key in multipleOptions) {
|
||||
multipleOptions[key] = (key as string).toUpperCase() + ': ' + multipleOptions[key];
|
||||
}
|
||||
},
|
||||
},
|
||||
[PrefKey.STATS_SHOW_WHEN_PLAYING]: {
|
||||
label: t('show-stats-on-startup'),
|
||||
default: false,
|
||||
},
|
||||
[PrefKey.STATS_QUICK_GLANCE]: {
|
||||
[PrefKey.STATS_QUICK_GLANCE_ENABLED]: {
|
||||
label: '👀 ' + t('enable-quick-glance-mode'),
|
||||
default: true,
|
||||
},
|
||||
@@ -802,12 +812,11 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
},
|
||||
[PrefKey.STATS_OPACITY]: {
|
||||
label: t('opacity'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 80,
|
||||
min: 50,
|
||||
max: 100,
|
||||
steps: 10,
|
||||
params: {
|
||||
steps: 10,
|
||||
suffix: '%',
|
||||
ticks: 10,
|
||||
},
|
||||
@@ -823,12 +832,13 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.REMOTE_PLAY_RESOLUTION]: {
|
||||
[PrefKey.REMOTE_PLAY_STREAM_RESOLUTION]: {
|
||||
requiredVariants: 'full',
|
||||
default: StreamResolution.DIM_1080P,
|
||||
options: {
|
||||
[StreamResolution.DIM_1080P]: '1080p',
|
||||
[StreamResolution.DIM_720P]: '720p',
|
||||
[StreamResolution.DIM_1080P]: '1080p',
|
||||
[StreamResolution.DIM_1080P_HQ]: '1080p (HQ)',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -838,13 +848,6 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
default: false,
|
||||
note: t('fortnite-allow-stw-mode'),
|
||||
},
|
||||
|
||||
[PrefKey.GAME_MSFS2020_FORCE_NATIVE_MKB]: {
|
||||
requiredVariants: 'full',
|
||||
label: '✈️ ' + t('msfs2020-force-native-mkb'),
|
||||
default: false,
|
||||
note: t('may-not-work-properly'),
|
||||
},
|
||||
} satisfies SettingDefinitions;
|
||||
|
||||
constructor() {
|
||||
|
74
src/utils/shortcut-handler.ts
Executable file
74
src/utils/shortcut-handler.ts
Executable file
@@ -0,0 +1,74 @@
|
||||
import { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { MicrophoneShortcut } from "@/modules/shortcuts/microphone-shortcut";
|
||||
import { SoundShortcut } from "@/modules/shortcuts/sound-shortcut";
|
||||
import { StreamUiShortcut } from "@/modules/shortcuts/stream-ui-shortcut";
|
||||
import { StreamStats } from "@/modules/stream/stream-stats";
|
||||
import { SettingsDialog } from "@/modules/ui/dialog/settings-dialog";
|
||||
import { AppInterface, STATES } from "./global";
|
||||
import { ScreenshotManager } from "./screenshot-manager";
|
||||
import { EmulatedMkbHandler } from "@/modules/mkb/mkb-handler";
|
||||
import { RendererShortcut } from "@/modules/shortcuts/renderer-shortcut";
|
||||
import { TrueAchievements } from "./true-achievements";
|
||||
import { NativeMkbHandler } from "@/modules/mkb/native-mkb-handler";
|
||||
|
||||
export class ShortcutHandler {
|
||||
static runAction(action: ShortcutAction) {
|
||||
switch (action) {
|
||||
case ShortcutAction.BETTER_XCLOUD_SETTINGS_SHOW:
|
||||
SettingsDialog.getInstance().show();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_SCREENSHOT_CAPTURE:
|
||||
ScreenshotManager.getInstance().takeScreenshot();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_VIDEO_TOGGLE:
|
||||
RendererShortcut.toggleVisibility();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_STATS_TOGGLE:
|
||||
StreamStats.getInstance().toggle();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_MICROPHONE_TOGGLE:
|
||||
MicrophoneShortcut.toggle();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_MENU_SHOW:
|
||||
StreamUiShortcut.showHideStreamMenu();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_SOUND_TOGGLE:
|
||||
SoundShortcut.muteUnmute();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_VOLUME_INC:
|
||||
SoundShortcut.adjustGainNodeVolume(10);
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_VOLUME_DEC:
|
||||
SoundShortcut.adjustGainNodeVolume(-10);
|
||||
break;
|
||||
|
||||
case ShortcutAction.DEVICE_BRIGHTNESS_INC:
|
||||
case ShortcutAction.DEVICE_BRIGHTNESS_DEC:
|
||||
case ShortcutAction.DEVICE_SOUND_TOGGLE:
|
||||
case ShortcutAction.DEVICE_VOLUME_INC:
|
||||
case ShortcutAction.DEVICE_VOLUME_DEC:
|
||||
AppInterface && AppInterface.runShortcut && AppInterface.runShortcut(action);
|
||||
break;
|
||||
|
||||
case ShortcutAction.MKB_TOGGLE:
|
||||
if (STATES.currentStream.titleInfo?.details.hasMkbSupport) {
|
||||
NativeMkbHandler.getInstance()?.toggle();
|
||||
} else {
|
||||
EmulatedMkbHandler.getInstance()?.toggle();
|
||||
}
|
||||
break;
|
||||
|
||||
case ShortcutAction.TRUE_ACHIEVEMENTS_OPEN:
|
||||
TrueAchievements.getInstance().open(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
215
src/utils/stream-settings.ts
Executable file
215
src/utils/stream-settings.ts
Executable file
@@ -0,0 +1,215 @@
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { ControllerSettingsTable } from "./local-db/controller-settings-table";
|
||||
import { ControllerShortcutsTable } from "./local-db/controller-shortcuts-table";
|
||||
import { getPref, setPref } from "./settings-storages/global-settings-storage";
|
||||
import type { ControllerShortcutPresetRecord, KeyboardShortcutConvertedPresetData, MkbConvertedPresetData } from "@/types/presets";
|
||||
import { STATES } from "./global";
|
||||
import { DeviceVibrationMode } from "@/enums/pref-values";
|
||||
import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler";
|
||||
import { hasGamepad } from "./gamepad";
|
||||
import { MkbMappingPresetsTable } from "./local-db/mkb-mapping-presets-table";
|
||||
import type { GamepadKey } from "@/enums/gamepad";
|
||||
import { MkbPresetKey, MouseConstant } from "@/enums/mkb";
|
||||
import { BxEvent } from "./bx-event";
|
||||
import { KeyboardShortcutDefaultId, KeyboardShortcutsTable } from "./local-db/keyboard-shortcuts-table";
|
||||
import { ShortcutAction } from "@/enums/shortcut-actions";
|
||||
import { KeyHelper } from "@/modules/mkb/key-helper";
|
||||
|
||||
|
||||
export type StreamSettingsData = {
|
||||
settings: Partial<Record<PrefKey, any>>;
|
||||
xCloudPollingMode: 'none' | 'callbacks' | 'navigation' | 'all';
|
||||
|
||||
deviceVibrationIntensity: DeviceVibrationIntensity;
|
||||
|
||||
controllerPollingRate: ControllerPollingRate;
|
||||
controllers: {
|
||||
[gamepadId: string]: {
|
||||
vibrationIntensity: number;
|
||||
shortcuts: ControllerShortcutPresetRecord['data']['mapping'] | null;
|
||||
};
|
||||
};
|
||||
|
||||
mkbPreset: MkbConvertedPresetData | null;
|
||||
|
||||
keyboardShortcuts: KeyboardShortcutConvertedPresetData['mapping'] | null;
|
||||
}
|
||||
|
||||
export class StreamSettings {
|
||||
static settings: StreamSettingsData = {
|
||||
settings: {},
|
||||
xCloudPollingMode: 'all',
|
||||
|
||||
deviceVibrationIntensity: 0,
|
||||
|
||||
controllerPollingRate: 4,
|
||||
controllers: {},
|
||||
|
||||
mkbPreset: null,
|
||||
|
||||
keyboardShortcuts: {},
|
||||
};
|
||||
|
||||
static getPref<T=boolean>(key: PrefKey) {
|
||||
return getPref<T>(key);
|
||||
}
|
||||
|
||||
static async refreshControllerSettings() {
|
||||
const settings = StreamSettings.settings;
|
||||
const controllers: StreamSettingsData['controllers'] = {};
|
||||
|
||||
const settingsTable = ControllerSettingsTable.getInstance();
|
||||
const shortcutsTable = ControllerShortcutsTable.getInstance();
|
||||
|
||||
const gamepads = window.navigator.getGamepads();
|
||||
for (const gamepad of gamepads) {
|
||||
if (!gamepad?.connected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore virtual controller
|
||||
if (gamepad.id === VIRTUAL_GAMEPAD_ID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const settingsData = await settingsTable.getControllerData(gamepad.id);
|
||||
|
||||
let shortcutsMapping;
|
||||
const preset = await shortcutsTable.getPreset(settingsData.shortcutPresetId);
|
||||
if (!preset) {
|
||||
shortcutsMapping = null;
|
||||
} else {
|
||||
shortcutsMapping = preset.data.mapping;
|
||||
}
|
||||
|
||||
controllers[gamepad.id] = {
|
||||
vibrationIntensity: settingsData.vibrationIntensity,
|
||||
shortcuts: shortcutsMapping,
|
||||
}
|
||||
}
|
||||
settings.controllers = controllers;
|
||||
|
||||
// Controller polling rate
|
||||
settings.controllerPollingRate = StreamSettings.getPref(PrefKey.CONTROLLER_POLLING_RATE);
|
||||
// Device vibration
|
||||
await StreamSettings.refreshDeviceVibration();
|
||||
}
|
||||
|
||||
private static async refreshDeviceVibration() {
|
||||
if (!STATES.browser.capabilities.deviceVibration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = StreamSettings.getPref<DeviceVibrationMode>(PrefKey.DEVICE_VIBRATION_MODE);
|
||||
let intensity = 0; // Disable
|
||||
|
||||
// Enable when no controllers are detected in Auto mode
|
||||
if (mode === DeviceVibrationMode.ON || (mode === DeviceVibrationMode.AUTO && !hasGamepad())) {
|
||||
// Set intensity
|
||||
intensity = StreamSettings.getPref<DeviceVibrationIntensity>(PrefKey.DEVICE_VIBRATION_INTENSITY) / 100;
|
||||
}
|
||||
|
||||
StreamSettings.settings.deviceVibrationIntensity = intensity;
|
||||
BxEvent.dispatch(window, BxEvent.DEVICE_VIBRATION_CHANGED);
|
||||
}
|
||||
|
||||
static async refreshMkbSettings() {
|
||||
const settings = StreamSettings.settings;
|
||||
|
||||
let presetId = StreamSettings.getPref<MkbPresetId>(PrefKey.MKB_P1_MAPPING_PRESET_ID);
|
||||
const orgPreset = (await MkbMappingPresetsTable.getInstance().getPreset(presetId))!;
|
||||
const orgPresetData = orgPreset.data;
|
||||
|
||||
const converted: MkbConvertedPresetData = {
|
||||
mapping: {},
|
||||
mouse: Object.assign({}, orgPresetData.mouse),
|
||||
};
|
||||
|
||||
let key: string;
|
||||
for (key in orgPresetData.mapping) {
|
||||
const buttonIndex = parseInt(key) as GamepadKey;
|
||||
if (!orgPresetData.mapping[buttonIndex]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const keyName of orgPresetData.mapping[buttonIndex]) {
|
||||
if (typeof keyName === 'string') {
|
||||
converted.mapping[keyName!] = buttonIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-calculate mouse's sensitivities
|
||||
const mouse = converted.mouse;
|
||||
mouse[MkbPresetKey.MOUSE_SENSITIVITY_X] *= MouseConstant.DEFAULT_PANNING_SENSITIVITY;
|
||||
mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y] *= MouseConstant.DEFAULT_PANNING_SENSITIVITY;
|
||||
mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT] *= MouseConstant.DEFAULT_DEADZONE_COUNTERWEIGHT;
|
||||
|
||||
settings.mkbPreset = converted;
|
||||
|
||||
setPref(PrefKey.MKB_P1_MAPPING_PRESET_ID, orgPreset.id);
|
||||
BxEvent.dispatch(window, BxEvent.MKB_UPDATED);
|
||||
}
|
||||
|
||||
static async refreshKeyboardShortcuts() {
|
||||
const settings = StreamSettings.settings;
|
||||
|
||||
let presetId = StreamSettings.getPref<KeyboardShortcutsPresetId>(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID);
|
||||
if (presetId === KeyboardShortcutDefaultId.OFF) {
|
||||
settings.keyboardShortcuts = null;
|
||||
|
||||
setPref(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID, presetId);
|
||||
BxEvent.dispatch(window, BxEvent.KEYBOARD_SHORTCUTS_UPDATED);
|
||||
return;
|
||||
}
|
||||
|
||||
const orgPreset = (await KeyboardShortcutsTable.getInstance().getPreset(presetId))!;
|
||||
const orgPresetData = orgPreset.data.mapping;
|
||||
|
||||
const converted: KeyboardShortcutConvertedPresetData['mapping'] = {};
|
||||
|
||||
let action: keyof typeof orgPresetData;
|
||||
for (action in orgPresetData) {
|
||||
const info = orgPresetData[action]!;
|
||||
const key = `${info.code}:${info.modifiers || 0}`;
|
||||
|
||||
converted[key] = action;
|
||||
}
|
||||
|
||||
settings.keyboardShortcuts = converted;
|
||||
|
||||
setPref(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID, orgPreset.id);
|
||||
BxEvent.dispatch(window, BxEvent.KEYBOARD_SHORTCUTS_UPDATED);
|
||||
}
|
||||
|
||||
static async refreshAllSettings() {
|
||||
window.BX_STREAM_SETTINGS = StreamSettings.settings;
|
||||
|
||||
await StreamSettings.refreshControllerSettings();
|
||||
await StreamSettings.refreshMkbSettings();
|
||||
await StreamSettings.refreshKeyboardShortcuts();
|
||||
}
|
||||
|
||||
static findKeyboardShortcut(targetAction: ShortcutAction) {
|
||||
const shortcuts = StreamSettings.settings.keyboardShortcuts
|
||||
for (const codeStr in shortcuts) {
|
||||
const action = shortcuts[codeStr];
|
||||
if (action === targetAction) {
|
||||
return KeyHelper.parseFullKeyCode(codeStr);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static setup() {
|
||||
const listener = () => {
|
||||
StreamSettings.refreshControllerSettings();
|
||||
}
|
||||
|
||||
window.addEventListener('gamepadconnected', listener);
|
||||
window.addEventListener('gamepaddisconnected', listener);
|
||||
|
||||
StreamSettings.refreshAllSettings();
|
||||
}
|
||||
}
|
18
src/utils/stream-stats-collector.ts
Normal file → Executable file
18
src/utils/stream-stats-collector.ts
Normal file → Executable file
@@ -4,21 +4,7 @@ import { STATES } from "./global";
|
||||
import { humanFileSize, secondsToHm } from "./html";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
|
||||
export enum StreamStat {
|
||||
PING = 'ping',
|
||||
JITTER = 'jit',
|
||||
FPS = 'fps',
|
||||
BITRATE = 'btr',
|
||||
DECODE_TIME = 'dt',
|
||||
PACKETS_LOST = 'pl',
|
||||
FRAMES_LOST = 'fl',
|
||||
DOWNLOAD = 'dl',
|
||||
UPLOAD = 'ul',
|
||||
PLAYTIME = 'play',
|
||||
BATTERY = 'batt',
|
||||
CLOCK = 'time',
|
||||
};
|
||||
import { StreamStat } from "@/enums/pref-values";
|
||||
|
||||
export type StreamStatGrade = '' | 'bad' | 'ok' | 'good';
|
||||
|
||||
@@ -125,7 +111,7 @@ export class StreamStatsCollector {
|
||||
[StreamStat.FPS]: {
|
||||
current: 0,
|
||||
toString() {
|
||||
const maxFps = getPref(PrefKey.VIDEO_MAX_FPS);
|
||||
const maxFps = getPref<VideoMaxFps>(PrefKey.VIDEO_MAX_FPS);
|
||||
return maxFps < 60 ? `${maxFps}/${this.current}` : this.current.toString();
|
||||
},
|
||||
},
|
||||
|
4
src/utils/toast.ts
Normal file → Executable file
4
src/utils/toast.ts
Normal file → Executable file
@@ -65,7 +65,7 @@ export class Toast {
|
||||
this.isShowing = true;
|
||||
|
||||
this.timeoutId && clearTimeout(this.timeoutId);
|
||||
this.timeoutId = window.setTimeout(this.hide.bind(this), this.DURATION);
|
||||
this.timeoutId = window.setTimeout(this.hide, this.DURATION);
|
||||
|
||||
// Get values from item
|
||||
const [msg, status, options] = this.stack.shift()!;
|
||||
@@ -88,7 +88,7 @@ export class Toast {
|
||||
classList.add('bx-show');
|
||||
}
|
||||
|
||||
private hide() {
|
||||
private hide = () => {
|
||||
this.timeoutId = null;
|
||||
|
||||
const classList = this.$wrapper.classList;
|
||||
|
103
src/utils/translation.ts
Normal file → Executable file
103
src/utils/translation.ts
Normal file → Executable file
@@ -1,5 +1,7 @@
|
||||
import { StorageKey } from "@/enums/pref-keys";
|
||||
import { NATIVE_FETCH } from "./bx-flags";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
import { GhPagesUtils } from "./gh-pages";
|
||||
|
||||
export const SUPPORTED_LANGUAGES = {
|
||||
'en-US': 'English (US)',
|
||||
@@ -57,6 +59,9 @@ const Texts = {
|
||||
"clarity-boost": "Clarity boost",
|
||||
"clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",
|
||||
"clear": "Clear",
|
||||
"clear-data": "Clear data",
|
||||
"clear-data-confirm": "Do you want to clear all Better xCloud settings and data?",
|
||||
"clear-data-success": "Data cleared! Refresh the page to apply the changes.",
|
||||
"clock": "Clock",
|
||||
"close": "Close",
|
||||
"close-app": "Close app",
|
||||
@@ -77,6 +82,7 @@ const Texts = {
|
||||
"controller-friendly-ui": "Controller-friendly UI",
|
||||
"controller-shortcuts": "Controller shortcuts",
|
||||
"controller-shortcuts-connect-note": "Connect a controller to use this feature",
|
||||
"controller-shortcuts-in-game": "In-game controller shortcuts",
|
||||
"controller-shortcuts-xbox-note": "Button to open the Guide menu",
|
||||
"controller-vibration": "Controller vibration",
|
||||
"copy": "Copy",
|
||||
@@ -91,6 +97,7 @@ const Texts = {
|
||||
"device-vibration": "Device vibration",
|
||||
"device-vibration-not-using-gamepad": "On when not using gamepad",
|
||||
"disable": "Disable",
|
||||
"disable-byog-feature": "Disable \"Stream your own game\" feature",
|
||||
"disable-home-context-menu": "Disable context menu in Home page",
|
||||
"disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog",
|
||||
"disable-social-features": "Disable social features",
|
||||
@@ -112,6 +119,7 @@ const Texts = {
|
||||
"experimental": "Experimental",
|
||||
"export": "Export",
|
||||
"fast": "Fast",
|
||||
"force-native-mkb-games": "Force native Mouse & Keyboard for these games",
|
||||
"fortnite-allow-stw-mode": "Allows playing \"Save the World\" mode on mobile",
|
||||
"fortnite-force-console-version": "Fortnite: force console version",
|
||||
"game-bar": "Game Bar",
|
||||
@@ -137,7 +145,9 @@ const Texts = {
|
||||
"install-android": "Better xCloud app for Android",
|
||||
"japan": "Japan",
|
||||
"jitter": "Jitter",
|
||||
"keyboard-key": "Keyboard key",
|
||||
"keyboard-shortcuts": "Keyboard shortcuts",
|
||||
"keyboard-shortcuts-in-game": "In-game keyboard shortcuts",
|
||||
"korea": "Korea",
|
||||
"language": "Language",
|
||||
"large": "Large",
|
||||
@@ -147,6 +157,7 @@ const Texts = {
|
||||
"loading-screen": "Loading screen",
|
||||
"local-co-op": "Local co-op",
|
||||
"lowest-quality": "Lowest quality",
|
||||
"manage": "Manage",
|
||||
"map-mouse-to": "Map mouse to",
|
||||
"max-fps": "Max FPS",
|
||||
"may-not-work-properly": "May not work properly!",
|
||||
@@ -154,17 +165,18 @@ const Texts = {
|
||||
"microphone": "Microphone",
|
||||
"mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings",
|
||||
"mkb-click-to-activate": "Click to activate",
|
||||
"mkb-disclaimer": "Using this feature when playing online could be viewed as cheating",
|
||||
"mkb-disclaimer": "This could be viewed as cheating when playing online",
|
||||
"modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.",
|
||||
"mouse-and-keyboard": "Mouse & Keyboard",
|
||||
"mouse-click": "Mouse click",
|
||||
"mouse-wheel": "Mouse wheel",
|
||||
"msfs2020-force-native-mkb": "MSFS2020: force native M&KB support",
|
||||
"muted": "Muted",
|
||||
"name": "Name",
|
||||
"native-mkb": "Native Mouse & Keyboard",
|
||||
"new": "New",
|
||||
"new-version-available": [
|
||||
(e: any) => `Version ${e.version} available`,
|
||||
,
|
||||
(e: any) => `Versió ${e.version} disponible`,
|
||||
,
|
||||
(e: any) => `Version ${e.version} verfügbar`,
|
||||
(e: any) => `Versi ${e.version} tersedia`,
|
||||
@@ -184,8 +196,10 @@ const Texts = {
|
||||
(e: any) => `已可更新為 ${e.version} 版`,
|
||||
],
|
||||
"no-consoles-found": "No consoles found",
|
||||
"no-controllers-connected": "No controllers connected",
|
||||
"normal": "Normal",
|
||||
"off": "Off",
|
||||
"official": "Official",
|
||||
"on": "On",
|
||||
"only-supports-some-games": "Only supports some games",
|
||||
"opacity": "Opacity",
|
||||
@@ -264,6 +278,7 @@ const Texts = {
|
||||
"screen": "Screen",
|
||||
"screenshot-apply-filters": "Apply video filters to screenshots",
|
||||
"section-all-games": "All games",
|
||||
"section-byog": "Stream your own game",
|
||||
"section-most-popular": "Most popular",
|
||||
"section-native-mkb": "Play with mouse & keyboard",
|
||||
"section-news": "News",
|
||||
@@ -293,6 +308,7 @@ const Texts = {
|
||||
"small": "Small",
|
||||
"smart-tv": "Smart TV",
|
||||
"sound": "Sound",
|
||||
"standard": "Standard",
|
||||
"standby": "Standby",
|
||||
"stat-bitrate": "Bitrate",
|
||||
"stat-decode-time": "Decode time",
|
||||
@@ -357,6 +373,8 @@ const Texts = {
|
||||
"unknown": "Unknown",
|
||||
"unlimited": "Unlimited",
|
||||
"unmuted": "Unmuted",
|
||||
"unofficial": "Unofficial",
|
||||
"unofficial-game-list": "Unofficial game list",
|
||||
"unsharp-masking": "Unsharp masking",
|
||||
"upload": "Upload",
|
||||
"uploaded": "Uploaded",
|
||||
@@ -369,6 +387,7 @@ const Texts = {
|
||||
"vibration-status": "Vibration",
|
||||
"video": "Video",
|
||||
"virtual-controller": "Virtual controller",
|
||||
"virtual-controller-slot": "Virtual controller slot",
|
||||
"visual-quality": "Visual quality",
|
||||
"visual-quality-high": "High",
|
||||
"visual-quality-low": "Low",
|
||||
@@ -376,57 +395,57 @@ const Texts = {
|
||||
"volume": "Volume",
|
||||
"wait-time-countdown": "Countdown",
|
||||
"wait-time-estimated": "Estimated finish time",
|
||||
"waiting-for-input": "Waiting for input...",
|
||||
"wallpaper": "Wallpaper",
|
||||
"webgl2": "WebGL2",
|
||||
};
|
||||
|
||||
export class Translations {
|
||||
static readonly #EN_US = 'en-US';
|
||||
static readonly #KEY_LOCALE = 'better_xcloud_locale';
|
||||
static readonly #KEY_TRANSLATIONS = 'better_xcloud_translations';
|
||||
private static readonly EN_US = 'en-US';
|
||||
private static readonly KEY_LOCALE = StorageKey.LOCALE;
|
||||
private static readonly KEY_TRANSLATIONS = StorageKey.LOCALE_TRANSLATIONS;
|
||||
|
||||
static #enUsIndex = -1;
|
||||
static #selectedLocaleIndex = -1;
|
||||
static #selectedLocale: keyof typeof SUPPORTED_LANGUAGES = 'en-US';
|
||||
private static selectedLocaleIndex = -1;
|
||||
private static selectedLocale: keyof typeof SUPPORTED_LANGUAGES = 'en-US';
|
||||
|
||||
static #supportedLocales = Object.keys(SUPPORTED_LANGUAGES);
|
||||
static #foreignTranslations: any = {};
|
||||
private static supportedLocales = Object.keys(SUPPORTED_LANGUAGES);
|
||||
private static foreignTranslations: any = {};
|
||||
|
||||
private static enUsIndex = Translations.supportedLocales.indexOf(Translations.EN_US);
|
||||
|
||||
static async init() {
|
||||
Translations.#enUsIndex = Translations.#supportedLocales.indexOf(Translations.#EN_US);
|
||||
|
||||
Translations.refreshLocale();
|
||||
await Translations.#loadTranslations();
|
||||
await Translations.loadTranslations();
|
||||
}
|
||||
|
||||
static refreshLocale(newLocale?: string) {
|
||||
let locale;
|
||||
if (newLocale) {
|
||||
localStorage.setItem(Translations.#KEY_LOCALE, newLocale);
|
||||
localStorage.setItem(Translations.KEY_LOCALE, newLocale);
|
||||
locale = newLocale;
|
||||
} else {
|
||||
locale = localStorage.getItem(Translations.#KEY_LOCALE);
|
||||
locale = localStorage.getItem(Translations.KEY_LOCALE);
|
||||
}
|
||||
const supportedLocales = Translations.#supportedLocales;
|
||||
const supportedLocales = Translations.supportedLocales;
|
||||
|
||||
if (!locale) {
|
||||
// Get browser's locale
|
||||
locale = window.navigator.language || Translations.#EN_US;
|
||||
locale = window.navigator.language || Translations.EN_US;
|
||||
if (supportedLocales.indexOf(locale) === -1) {
|
||||
locale = Translations.#EN_US;
|
||||
locale = Translations.EN_US;
|
||||
}
|
||||
localStorage.setItem(Translations.#KEY_LOCALE, locale);
|
||||
localStorage.setItem(Translations.KEY_LOCALE, locale);
|
||||
}
|
||||
|
||||
Translations.#selectedLocale = locale as keyof typeof SUPPORTED_LANGUAGES;
|
||||
Translations.#selectedLocaleIndex = supportedLocales.indexOf(locale);
|
||||
Translations.selectedLocale = locale as keyof typeof SUPPORTED_LANGUAGES;
|
||||
Translations.selectedLocaleIndex = supportedLocales.indexOf(locale);
|
||||
}
|
||||
|
||||
static get<T=string>(key: keyof typeof Texts, values?: any): T {
|
||||
let text = null;
|
||||
|
||||
if (Translations.#foreignTranslations && Translations.#selectedLocale !== Translations.#EN_US) {
|
||||
text = Translations.#foreignTranslations[key];
|
||||
if (Translations.foreignTranslations && Translations.selectedLocale !== Translations.EN_US) {
|
||||
text = Translations.foreignTranslations[key];
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
@@ -435,7 +454,7 @@ export class Translations {
|
||||
|
||||
let translation: unknown;
|
||||
if (Array.isArray(text)) {
|
||||
translation = text[Translations.#selectedLocaleIndex] || text[Translations.#enUsIndex];
|
||||
translation = text[Translations.selectedLocaleIndex] || text[Translations.enUsIndex];
|
||||
return (translation as any)(values);
|
||||
}
|
||||
|
||||
@@ -443,44 +462,44 @@ export class Translations {
|
||||
return translation as T;
|
||||
}
|
||||
|
||||
static async #loadTranslations() {
|
||||
if (Translations.#selectedLocale === Translations.#EN_US) {
|
||||
private static async loadTranslations() {
|
||||
if (Translations.selectedLocale === Translations.EN_US) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Translations.#foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.#KEY_TRANSLATIONS)!);
|
||||
Translations.foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.KEY_TRANSLATIONS)!);
|
||||
} catch(e) {}
|
||||
|
||||
if (!Translations.#foreignTranslations) {
|
||||
await this.downloadTranslations(Translations.#selectedLocale);
|
||||
if (!Translations.foreignTranslations) {
|
||||
await this.downloadTranslations(Translations.selectedLocale);
|
||||
}
|
||||
}
|
||||
|
||||
static async updateTranslations(async=false) {
|
||||
// Don't have to download en-US
|
||||
if (Translations.#selectedLocale === Translations.#EN_US) {
|
||||
localStorage.removeItem(Translations.#KEY_TRANSLATIONS);
|
||||
if (Translations.selectedLocale === Translations.EN_US) {
|
||||
localStorage.removeItem(Translations.KEY_TRANSLATIONS);
|
||||
return;
|
||||
}
|
||||
|
||||
if (async) {
|
||||
Translations.downloadTranslationsAsync(Translations.#selectedLocale);
|
||||
Translations.downloadTranslationsAsync(Translations.selectedLocale);
|
||||
} else {
|
||||
await Translations.downloadTranslations(Translations.#selectedLocale);
|
||||
await Translations.downloadTranslations(Translations.selectedLocale);
|
||||
}
|
||||
}
|
||||
|
||||
static async downloadTranslations(locale: string) {
|
||||
try {
|
||||
const resp = await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/translations/${locale}.json`);
|
||||
const resp = await NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`));
|
||||
const translations = await resp.json();
|
||||
|
||||
// Prevent saving incorrect translations
|
||||
let currentLocale = localStorage.getItem(Translations.#KEY_LOCALE);
|
||||
let currentLocale = localStorage.getItem(Translations.KEY_LOCALE);
|
||||
if (currentLocale === locale) {
|
||||
window.localStorage.setItem(Translations.#KEY_TRANSLATIONS, JSON.stringify(translations));
|
||||
Translations.#foreignTranslations = translations;
|
||||
window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations));
|
||||
Translations.foreignTranslations = translations;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -491,16 +510,16 @@ export class Translations {
|
||||
}
|
||||
|
||||
static downloadTranslationsAsync(locale: string) {
|
||||
NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/translations/${locale}.json`)
|
||||
NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`))
|
||||
.then(resp => resp.json())
|
||||
.then(translations => {
|
||||
window.localStorage.setItem(Translations.#KEY_TRANSLATIONS, JSON.stringify(translations));
|
||||
Translations.#foreignTranslations = translations;
|
||||
window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations));
|
||||
Translations.foreignTranslations = translations;
|
||||
});
|
||||
}
|
||||
|
||||
static switchLocale(locale: string) {
|
||||
localStorage.setItem(Translations.#KEY_LOCALE, locale);
|
||||
localStorage.setItem(Translations.KEY_LOCALE, locale);
|
||||
}
|
||||
}
|
||||
|
||||
|
6
src/utils/true-achievements.ts
Normal file → Executable file
6
src/utils/true-achievements.ts
Normal file → Executable file
@@ -21,7 +21,7 @@ export class TrueAchievements {
|
||||
url: '#',
|
||||
icon: BxIcon.TRUE_ACHIEVEMENTS,
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_LINK,
|
||||
onClick: this.onClick.bind(this),
|
||||
onClick: this.onClick,
|
||||
});
|
||||
|
||||
this.$button = createButton<HTMLAnchorElement>({
|
||||
@@ -29,7 +29,7 @@ export class TrueAchievements {
|
||||
title: t('true-achievements'),
|
||||
icon: BxIcon.TRUE_ACHIEVEMENTS,
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: this.onClick.bind(this),
|
||||
onClick: this.onClick,
|
||||
});
|
||||
|
||||
this.$hiddenLink = CE<HTMLAnchorElement>('a', {
|
||||
@@ -37,7 +37,7 @@ export class TrueAchievements {
|
||||
});
|
||||
}
|
||||
|
||||
private onClick(e: Event) {
|
||||
private onClick = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Close all xCloud's dialogs
|
||||
|
3
src/utils/user-agent.ts
Normal file → Executable file
3
src/utils/user-agent.ts
Normal file → Executable file
@@ -1,5 +1,6 @@
|
||||
import { UserAgentProfile } from "@enums/user-agent";
|
||||
import { BX_FLAGS } from "./bx-flags";
|
||||
import { StorageKey } from "@/enums/pref-keys";
|
||||
|
||||
type UserAgentConfig = {
|
||||
profile: UserAgentProfile,
|
||||
@@ -18,7 +19,7 @@ if (!!(window as any).chrome || window.navigator.userAgent.includes('Chrome')) {
|
||||
}
|
||||
|
||||
export class UserAgent {
|
||||
static readonly STORAGE_KEY = 'better_xcloud_user_agent';
|
||||
static readonly STORAGE_KEY = StorageKey.USER_AGENT;
|
||||
static #config: UserAgentConfig;
|
||||
|
||||
static #isMobile: boolean | null = null;
|
||||
|
35
src/utils/utils.ts
Normal file → Executable file
35
src/utils/utils.ts
Normal file → Executable file
@@ -1,9 +1,10 @@
|
||||
import { AppInterface, SCRIPT_VERSION } from "@utils/global";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
import { Translations } from "./translation";
|
||||
import { t, Translations } from "./translation";
|
||||
import { Toast } from "./toast";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, setPref } from "./settings-storages/global-settings-storage";
|
||||
import { LocalDb } from "./local-db/local-db";
|
||||
|
||||
/**
|
||||
* Check for update
|
||||
@@ -16,8 +17,8 @@ export function checkForUpdate() {
|
||||
|
||||
const CHECK_INTERVAL_SECONDS = 2 * 3600; // check every 2 hours
|
||||
|
||||
const currentVersion = getPref(PrefKey.CURRENT_VERSION);
|
||||
const lastCheck = getPref(PrefKey.LAST_UPDATE_CHECK);
|
||||
const currentVersion = getPref<VersionCurrent>(PrefKey.VERSION_CURRENT);
|
||||
const lastCheck = getPref<VersionLastCheck>(PrefKey.VERSION_LAST_CHECK);
|
||||
const now = Math.round((+new Date) / 1000);
|
||||
|
||||
if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) {
|
||||
@@ -25,13 +26,13 @@ export function checkForUpdate() {
|
||||
}
|
||||
|
||||
// Start checking
|
||||
setPref(PrefKey.LAST_UPDATE_CHECK, now);
|
||||
setPref(PrefKey.VERSION_LAST_CHECK, now);
|
||||
fetch('https://api.github.com/repos/redphx/better-xcloud/releases/latest')
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
// Store the latest version
|
||||
setPref(PrefKey.LATEST_VERSION, json.tag_name.substring(1));
|
||||
setPref(PrefKey.CURRENT_VERSION, SCRIPT_VERSION);
|
||||
setPref(PrefKey.VERSION_LATEST, json.tag_name.substring(1));
|
||||
setPref(PrefKey.VERSION_CURRENT, SCRIPT_VERSION);
|
||||
});
|
||||
|
||||
// Update translations
|
||||
@@ -132,3 +133,25 @@ export function parseDetailsPath(path: string) {
|
||||
|
||||
return {titleSlug, productId};
|
||||
}
|
||||
|
||||
export function clearAllData() {
|
||||
// Delete localStorage items
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Delete key
|
||||
if (key.startsWith('BetterXcloud') || key.startsWith('better_xcloud')) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete IndexedDB database
|
||||
try {
|
||||
indexedDB.deleteDatabase(LocalDb.DB_NAME);
|
||||
} catch (e) {};
|
||||
|
||||
alert(t('clear-data-success'));
|
||||
}
|
||||
|
0
src/utils/xbox-api.ts
Normal file → Executable file
0
src/utils/xbox-api.ts
Normal file → Executable file
4
src/utils/xcloud-api.ts
Normal file → Executable file
4
src/utils/xcloud-api.ts
Normal file → Executable file
@@ -29,7 +29,7 @@ export class XcloudApi {
|
||||
const response = await NATIVE_FETCH(`${baseUri}/v2/titles`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${STATES.gsToken}`,
|
||||
Authorization: `Bearer ${STATES.gsToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
@@ -63,7 +63,7 @@ export class XcloudApi {
|
||||
const response = await NATIVE_FETCH(`${baseUri}/v1/waittime/${id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${STATES.gsToken}`,
|
||||
Authorization: `Bearer ${STATES.gsToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
84
src/utils/xcloud-interceptor.ts
Normal file → Executable file
84
src/utils/xcloud-interceptor.ts
Normal file → Executable file
@@ -11,7 +11,8 @@ import { patchIceCandidates } from "./network";
|
||||
import { getPreferredServerRegion } from "./region";
|
||||
import { BypassServerIps } from "@/enums/bypass-servers";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, StreamResolution, StreamTouchController } from "./settings-storages/global-settings-storage";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
import { NativeMkbMode, StreamResolution, TouchControllerMode } from "@/enums/pref-values";
|
||||
|
||||
export class XcloudInterceptor {
|
||||
private static readonly SERVER_EXTRA_INFO: Record<string, [string, ServerContinent]> = {
|
||||
@@ -41,8 +42,48 @@ export class XcloudInterceptor {
|
||||
WestEurope: ['🇪🇺', 'europe'],
|
||||
};
|
||||
|
||||
private static readonly BASE_DEVICE_INFO = {
|
||||
appInfo: {
|
||||
env: {
|
||||
clientAppId: window.location.host,
|
||||
clientAppType: 'browser',
|
||||
clientAppVersion: '24.17.36',
|
||||
clientSdkVersion: '10.1.14',
|
||||
httpEnvironment: 'prod',
|
||||
sdkInstallId: '',
|
||||
},
|
||||
},
|
||||
|
||||
dev: {
|
||||
displayInfo: {
|
||||
dimensions: {
|
||||
widthInPixels: 1920,
|
||||
heightInPixels: 1080,
|
||||
},
|
||||
pixelDensity: {
|
||||
dpiX: 1,
|
||||
dpiY: 1,
|
||||
},
|
||||
},
|
||||
hw: {
|
||||
make: 'Microsoft',
|
||||
model: 'unknown',
|
||||
sdktype: 'web',
|
||||
},
|
||||
os: {
|
||||
name: 'windows',
|
||||
ver: '22631.2715',
|
||||
platform: 'desktop',
|
||||
},
|
||||
browser: {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '125.0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
private static async handleLogin(request: RequestInfo | URL, init?: RequestInit) {
|
||||
const bypassServer = getPref(PrefKey.SERVER_BYPASS_RESTRICTION);
|
||||
const bypassServer = getPref<string>(PrefKey.SERVER_BYPASS_RESTRICTION);
|
||||
if (bypassServer !== 'off') {
|
||||
const ip = BypassServerIps[bypassServer as keyof typeof BypassServerIps];
|
||||
ip && (request as Request).headers.set('X-Forwarded-For', ip);
|
||||
@@ -58,7 +99,7 @@ export class XcloudInterceptor {
|
||||
const obj = await response.clone().json();
|
||||
|
||||
// Store xCloud token
|
||||
RemotePlayManager.getInstance().xcloudToken = obj.gsToken;
|
||||
RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken);
|
||||
|
||||
// Get server list
|
||||
const serverRegex = /\/\/(\w+)\./;
|
||||
@@ -108,8 +149,8 @@ export class XcloudInterceptor {
|
||||
private static async handlePlay(request: RequestInfo | URL, init?: RequestInit) {
|
||||
BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
|
||||
|
||||
const PREF_STREAM_TARGET_RESOLUTION = getPref(PrefKey.STREAM_TARGET_RESOLUTION);
|
||||
const PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE);
|
||||
const PREF_STREAM_TARGET_RESOLUTION = getPref<StreamResolution>(PrefKey.STREAM_RESOLUTION);
|
||||
const PREF_STREAM_PREFERRED_LOCALE = getPref<StreamPreferredLocale>(PrefKey.STREAM_PREFERRED_LOCALE);
|
||||
|
||||
const url = (typeof request === 'string') ? request : (request as Request).url;
|
||||
const parsedUrl = new URL(url);
|
||||
@@ -127,9 +168,31 @@ export class XcloudInterceptor {
|
||||
const clone = (request as Request).clone();
|
||||
const body = await clone.json();
|
||||
|
||||
const headers: {[index: string]: string} = {};
|
||||
for (const pair of (clone.headers as any).entries()) {
|
||||
headers[pair[0]] = pair[1];
|
||||
}
|
||||
|
||||
// Force stream's resolution
|
||||
if (PREF_STREAM_TARGET_RESOLUTION !== 'auto') {
|
||||
const osName = (PREF_STREAM_TARGET_RESOLUTION === StreamResolution.DIM_720P) ? 'android' : 'windows';
|
||||
let osName;
|
||||
switch (PREF_STREAM_TARGET_RESOLUTION) {
|
||||
case StreamResolution.DIM_1080P_HQ:
|
||||
osName = 'tizen';
|
||||
|
||||
const deviceInfo = XcloudInterceptor.BASE_DEVICE_INFO;
|
||||
deviceInfo.dev.os.name = 'tizen';
|
||||
headers['x-ms-device-info'] = JSON.stringify(deviceInfo);
|
||||
|
||||
break;
|
||||
case StreamResolution.DIM_1080P:
|
||||
osName = 'windows';
|
||||
break;
|
||||
default:
|
||||
osName = 'android';
|
||||
break
|
||||
}
|
||||
|
||||
body.settings.osName = osName;
|
||||
}
|
||||
|
||||
@@ -140,6 +203,7 @@ export class XcloudInterceptor {
|
||||
|
||||
const newRequest = new Request(request, {
|
||||
body: JSON.stringify(body),
|
||||
headers: headers,
|
||||
});
|
||||
|
||||
return NATIVE_FETCH(newRequest);
|
||||
@@ -148,7 +212,7 @@ export class XcloudInterceptor {
|
||||
private static async handleWaitTime(request: RequestInfo | URL, init?: RequestInit) {
|
||||
const response = await NATIVE_FETCH(request, init);
|
||||
|
||||
if (getPref(PrefKey.UI_LOADING_SCREEN_WAIT_TIME)) {
|
||||
if (getPref(PrefKey.LOADING_SCREEN_SHOW_WAIT_TIME)) {
|
||||
const json = await response.clone().json();
|
||||
if (json.estimatedAllocationTimeInSeconds > 0) {
|
||||
// Setup wait time overlay
|
||||
@@ -165,7 +229,7 @@ export class XcloudInterceptor {
|
||||
}
|
||||
|
||||
// Touch controller for all games
|
||||
if (isFullVersion() && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.ALL) {
|
||||
if (isFullVersion() && getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL) {
|
||||
const titleInfo = STATES.currentStream.titleInfo;
|
||||
if (titleInfo?.details.hasTouchSupport) {
|
||||
TouchController.disable();
|
||||
@@ -191,11 +255,11 @@ export class XcloudInterceptor {
|
||||
|
||||
let overrideMkb: boolean | null = null;
|
||||
|
||||
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' || (STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId))) {
|
||||
if (getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON || (STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId))) {
|
||||
overrideMkb = true;
|
||||
}
|
||||
|
||||
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === 'off') {
|
||||
if (getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.OFF) {
|
||||
overrideMkb = false;
|
||||
}
|
||||
|
||||
|
22
src/utils/xhome-interceptor.ts
Normal file → Executable file
22
src/utils/xhome-interceptor.ts
Normal file → Executable file
@@ -5,9 +5,10 @@ import { NATIVE_FETCH } from "./bx-flags";
|
||||
import { STATES } from "./global";
|
||||
import { patchIceCandidates } from "./network";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, StreamResolution, StreamTouchController } from "./settings-storages/global-settings-storage";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
import type { RemotePlayConsoleAddresses } from "@/types/network";
|
||||
import { RemotePlayManager } from "@/modules/remote-play-manager";
|
||||
import { StreamResolution, TouchControllerMode } from "@/enums/pref-values";
|
||||
|
||||
export class XhomeInterceptor {
|
||||
private static consoleAddrs: RemotePlayConsoleAddresses = {};
|
||||
@@ -109,7 +110,7 @@ export class XhomeInterceptor {
|
||||
private static async handleInputConfigs(request: Request | URL, opts: {[index: string]: any}) {
|
||||
const response = await NATIVE_FETCH(request);
|
||||
|
||||
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== StreamTouchController.ALL) {
|
||||
if (getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) !== TouchControllerMode.ALL) {
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -150,7 +151,7 @@ export class XhomeInterceptor {
|
||||
for (const pair of (clone.headers as any).entries()) {
|
||||
headers[pair[0]] = pair[1];
|
||||
}
|
||||
headers.authorization = `Bearer ${RemotePlayManager.getInstance().xcloudToken}`;
|
||||
headers.authorization = `Bearer ${RemotePlayManager.getInstance()!.getXcloudToken()}`;
|
||||
|
||||
const index = request.url.indexOf('.xboxlive.com');
|
||||
request = new Request('https://wus.core.gssv-play-prod' + request.url.substring(index), {
|
||||
@@ -187,12 +188,21 @@ export class XhomeInterceptor {
|
||||
headers[pair[0]] = pair[1];
|
||||
}
|
||||
// Add xHome token to headers
|
||||
headers.authorization = `Bearer ${RemotePlayManager.getInstance().xhomeToken}`;
|
||||
headers.authorization = `Bearer ${RemotePlayManager.getInstance()!.getXhomeToken()}`;
|
||||
|
||||
// Patch resolution
|
||||
const deviceInfo = XhomeInterceptor.BASE_DEVICE_INFO;
|
||||
if (getPref(PrefKey.REMOTE_PLAY_RESOLUTION) === StreamResolution.DIM_720P) {
|
||||
deviceInfo.dev.os.name = 'android';
|
||||
const resolution = getPref<StreamResolution>(PrefKey.REMOTE_PLAY_STREAM_RESOLUTION);
|
||||
switch (resolution) {
|
||||
case StreamResolution.DIM_1080P_HQ:
|
||||
deviceInfo.dev.os.name = 'tizen';
|
||||
break;
|
||||
case StreamResolution.DIM_720P:
|
||||
deviceInfo.dev.os.name = 'android';
|
||||
break;
|
||||
default:
|
||||
deviceInfo.dev.os.name = 'windows';
|
||||
break;
|
||||
}
|
||||
|
||||
headers['x-ms-device-info'] = JSON.stringify(deviceInfo);
|
||||
|
Reference in New Issue
Block a user