mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-06 05:08:26 +02:00
Move preferences.ts & settings.ts to utils/
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { CE } from "./html";
|
||||
import { PrefKey, getPref } from "../modules/preferences";
|
||||
import { PrefKey, getPref } from "./preferences";
|
||||
import { renderStylus } from "../macros/build" with {type: "macro"};
|
||||
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { MkbHandler } from "../modules/mkb/mkb-handler";
|
||||
import { PrefKey, getPref } from "../modules/preferences";
|
||||
import { PrefKey, getPref } from "./preferences";
|
||||
import { t } from "./translation";
|
||||
import { Toast } from "./toast";
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { MkbPreset } from "../modules/mkb/mkb-preset";
|
||||
import { PrefKey, setPref } from "../modules/preferences";
|
||||
import { PrefKey, setPref } from "./preferences";
|
||||
import { t } from "./translation";
|
||||
import type { MkbStoredPreset, MkbStoredPresets } from "../types/mkb";
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { BxEvent } from "./bx-event";
|
||||
import { getPref, PrefKey } from "../modules/preferences";
|
||||
import { getPref, PrefKey } from "./preferences";
|
||||
import { STATES } from "./global";
|
||||
import { UserAgent } from "./user-agent";
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { BxEvent } from "./bx-event";
|
||||
import { BX_FLAGS } from "./bx-flags";
|
||||
import { LoadingScreen } from "../modules/loading-screen";
|
||||
import { PrefKey, getPref } from "../modules/preferences";
|
||||
import { PrefKey, getPref } from "./preferences";
|
||||
import { RemotePlay } from "../modules/remote-play";
|
||||
import { StreamBadges } from "../modules/stream/stream-badges";
|
||||
import { TouchController } from "../modules/touch-controller";
|
||||
|
759
src/utils/preferences.ts
Normal file
759
src/utils/preferences.ts
Normal file
@@ -0,0 +1,759 @@
|
||||
import { CE } from "./html";
|
||||
import { SUPPORTED_LANGUAGES, t } from "./translation";
|
||||
import { SettingElement, SettingElementType } from "./settings";
|
||||
import { UserAgentProfile } from "./user-agent";
|
||||
import { StreamStat } from "../modules/stream/stream-stats";
|
||||
import type { PreferenceSettings } from "../types/preferences";
|
||||
import { STATES } from "./global";
|
||||
|
||||
export enum PrefKey {
|
||||
LAST_UPDATE_CHECK = 'version_last_check',
|
||||
LATEST_VERSION = 'version_latest',
|
||||
CURRENT_VERSION = 'version_current',
|
||||
|
||||
BETTER_XCLOUD_LOCALE = 'bx_locale',
|
||||
|
||||
SERVER_REGION = 'server_region',
|
||||
PREFER_IPV6_SERVER = 'prefer_ipv6_server',
|
||||
STREAM_TARGET_RESOLUTION = 'stream_target_resolution',
|
||||
STREAM_PREFERRED_LOCALE = 'stream_preferred_locale',
|
||||
STREAM_CODEC_PROFILE = 'stream_codec_profile',
|
||||
|
||||
USER_AGENT_PROFILE = 'user_agent_profile',
|
||||
USER_AGENT_CUSTOM = 'user_agent_custom',
|
||||
STREAM_SIMPLIFY_MENU = 'stream_simplify_menu',
|
||||
|
||||
STREAM_COMBINE_SOURCES = 'stream_combine_sources',
|
||||
|
||||
STREAM_TOUCH_CONTROLLER = 'stream_touch_controller',
|
||||
STREAM_TOUCH_CONTROLLER_AUTO_OFF = 'stream_touch_controller_auto_off',
|
||||
STREAM_TOUCH_CONTROLLER_STYLE_STANDARD = 'stream_touch_controller_style_standard',
|
||||
STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM = 'stream_touch_controller_style_custom',
|
||||
|
||||
STREAM_DISABLE_FEEDBACK_DIALOG = 'stream_disable_feedback_dialog',
|
||||
|
||||
LOCAL_CO_OP_ENABLED = 'local_co_op_enabled',
|
||||
// LOCAL_CO_OP_SEPARATE_TOUCH_CONTROLLER = 'local_co_op_separate_touch_controller',
|
||||
|
||||
CONTROLLER_ENABLE_SHORTCUTS = 'controller_enable_shortcuts',
|
||||
CONTROLLER_ENABLE_VIBRATION = 'controller_enable_vibration',
|
||||
CONTROLLER_DEVICE_VIBRATION = 'controller_device_vibration',
|
||||
CONTROLLER_VIBRATION_INTENSITY = 'controller_vibration_intensity',
|
||||
|
||||
MKB_ENABLED = 'mkb_enabled',
|
||||
MKB_HIDE_IDLE_CURSOR = 'mkb_hide_idle_cursor',
|
||||
MKB_ABSOLUTE_MOUSE = 'mkb_absolute_mouse',
|
||||
MKB_DEFAULT_PRESET_ID = 'mkb_default_preset_id',
|
||||
|
||||
SCREENSHOT_BUTTON_POSITION = 'screenshot_button_position',
|
||||
SCREENSHOT_APPLY_FILTERS = 'screenshot_apply_filters',
|
||||
|
||||
BLOCK_TRACKING = 'block_tracking',
|
||||
BLOCK_SOCIAL_FEATURES = 'block_social_features',
|
||||
SKIP_SPLASH_VIDEO = 'skip_splash_video',
|
||||
HIDE_DOTS_ICON = 'hide_dots_icon',
|
||||
REDUCE_ANIMATIONS = 'reduce_animations',
|
||||
|
||||
UI_LOADING_SCREEN_GAME_ART = 'ui_loading_screen_game_art',
|
||||
UI_LOADING_SCREEN_WAIT_TIME = 'ui_loading_screen_wait_time',
|
||||
UI_LOADING_SCREEN_ROCKET = 'ui_loading_screen_rocket',
|
||||
|
||||
UI_LAYOUT = 'ui_layout',
|
||||
UI_SCROLLBAR_HIDE = 'ui_scrollbar_hide',
|
||||
|
||||
VIDEO_CLARITY = 'video_clarity',
|
||||
VIDEO_RATIO = 'video_ratio',
|
||||
VIDEO_BRIGHTNESS = 'video_brightness',
|
||||
VIDEO_CONTRAST = 'video_contrast',
|
||||
VIDEO_SATURATION = 'video_saturation',
|
||||
|
||||
AUDIO_MIC_ON_PLAYING = 'audio_mic_on_playing',
|
||||
AUDIO_ENABLE_VOLUME_CONTROL = 'audio_enable_volume_control',
|
||||
AUDIO_VOLUME = 'audio_volume',
|
||||
|
||||
STATS_ITEMS = 'stats_items',
|
||||
STATS_SHOW_WHEN_PLAYING = 'stats_show_when_playing',
|
||||
STATS_QUICK_GLANCE = 'stats_quick_glance',
|
||||
STATS_POSITION = 'stats_position',
|
||||
STATS_TEXT_SIZE = 'stats_text_size',
|
||||
STATS_TRANSPARENT = 'stats_transparent',
|
||||
STATS_OPACITY = 'stats_opacity',
|
||||
STATS_CONDITIONAL_FORMATTING = 'stats_conditional_formatting',
|
||||
|
||||
REMOTE_PLAY_ENABLED = 'xhome_enabled',
|
||||
REMOTE_PLAY_RESOLUTION = 'xhome_resolution',
|
||||
|
||||
GAME_FORTNITE_FORCE_CONSOLE = 'game_fortnite_force_console',
|
||||
}
|
||||
|
||||
export class Preferences {
|
||||
static SETTINGS: PreferenceSettings = {
|
||||
[PrefKey.LAST_UPDATE_CHECK]: {
|
||||
default: 0,
|
||||
},
|
||||
[PrefKey.LATEST_VERSION]: {
|
||||
default: '',
|
||||
},
|
||||
[PrefKey.CURRENT_VERSION]: {
|
||||
default: '',
|
||||
},
|
||||
[PrefKey.BETTER_XCLOUD_LOCALE]: {
|
||||
label: t('language'),
|
||||
default: localStorage.getItem('better_xcloud_locale') || 'en-US',
|
||||
options: SUPPORTED_LANGUAGES,
|
||||
},
|
||||
[PrefKey.SERVER_REGION]: {
|
||||
label: t('region'),
|
||||
default: 'default',
|
||||
},
|
||||
[PrefKey.STREAM_PREFERRED_LOCALE]: {
|
||||
label: t('preferred-game-language'),
|
||||
default: 'default',
|
||||
options: {
|
||||
default: t('default'),
|
||||
'ar-SA': 'العربية',
|
||||
'cs-CZ': 'čeština',
|
||||
'da-DK': 'dansk',
|
||||
'de-DE': 'Deutsch',
|
||||
'el-GR': 'Ελληνικά',
|
||||
'en-GB': 'English (United Kingdom)',
|
||||
'en-US': 'English (United States)',
|
||||
'es-ES': 'español (España)',
|
||||
'es-MX': 'español (Latinoamérica)',
|
||||
'fi-FI': 'suomi',
|
||||
'fr-FR': 'français',
|
||||
'he-IL': 'עברית',
|
||||
'hu-HU': 'magyar',
|
||||
'it-IT': 'italiano',
|
||||
'ja-JP': '日本語',
|
||||
'ko-KR': '한국어',
|
||||
'nb-NO': 'norsk bokmål',
|
||||
'nl-NL': 'Nederlands',
|
||||
'pl-PL': 'polski',
|
||||
'pt-BR': 'português (Brasil)',
|
||||
'pt-PT': 'português (Portugal)',
|
||||
'ru-RU': 'русский',
|
||||
'sk-SK': 'slovenčina',
|
||||
'sv-SE': 'svenska',
|
||||
'tr-TR': 'Türkçe',
|
||||
'zh-CN': '中文(简体)',
|
||||
'zh-TW': '中文 (繁體)',
|
||||
},
|
||||
},
|
||||
[PrefKey.STREAM_TARGET_RESOLUTION]: {
|
||||
label: t('target-resolution'),
|
||||
default: 'auto',
|
||||
options: {
|
||||
auto: t('default'),
|
||||
'1080p': '1080p',
|
||||
'720p': '720p',
|
||||
},
|
||||
},
|
||||
[PrefKey.STREAM_CODEC_PROFILE]: {
|
||||
label: t('visual-quality'),
|
||||
default: 'default',
|
||||
options: (() => {
|
||||
const options: {[index: string]: string} = {
|
||||
default: t('default'),
|
||||
};
|
||||
|
||||
if (!('getCapabilities' in RTCRtpReceiver) || typeof RTCRtpTransceiver === 'undefined' || !('setCodecPreferences' in RTCRtpTransceiver.prototype)) {
|
||||
return options;
|
||||
}
|
||||
|
||||
let hasLowCodec = false;
|
||||
let hasNormalCodec = false;
|
||||
let hasHighCodec = false;
|
||||
|
||||
const codecs = RTCRtpReceiver.getCapabilities('video')!.codecs;
|
||||
for (let codec of codecs) {
|
||||
if (codec.mimeType.toLowerCase() !== 'video/h264' || !codec.sdpFmtpLine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fmtp = codec.sdpFmtpLine.toLowerCase();
|
||||
if (!hasHighCodec && fmtp.includes('profile-level-id=4d')) {
|
||||
hasHighCodec = true;
|
||||
} else if (!hasNormalCodec && fmtp.includes('profile-level-id=42e')) {
|
||||
hasNormalCodec = true;
|
||||
} else if (!hasLowCodec && fmtp.includes('profile-level-id=420')) {
|
||||
hasLowCodec = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHighCodec) {
|
||||
if (!hasLowCodec && !hasNormalCodec) {
|
||||
options.default = `${t('visual-quality-high')} (${t('default')})`;
|
||||
} else {
|
||||
options.high = t('visual-quality-high');
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNormalCodec) {
|
||||
if (!hasLowCodec && !hasHighCodec) {
|
||||
options.default = `${t('visual-quality-normal')} (${t('default')})`;
|
||||
} else {
|
||||
options.normal = t('visual-quality-normal');
|
||||
}
|
||||
}
|
||||
|
||||
if (hasLowCodec) {
|
||||
if (!hasNormalCodec && !hasHighCodec) {
|
||||
options.default = `${t('visual-quality-low')} (${t('default')})`;
|
||||
} else {
|
||||
options.low = t('visual-quality-low');
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
})(),
|
||||
ready: () => {
|
||||
const setting = Preferences.SETTINGS[PrefKey.STREAM_CODEC_PROFILE]
|
||||
const options: any = setting.options;
|
||||
const keys = Object.keys(options);
|
||||
|
||||
if (keys.length <= 1) { // Unsupported
|
||||
setting.unsupported = true;
|
||||
setting.note = '⚠️ ' + t('browser-unsupported-feature');
|
||||
} else {
|
||||
// Set default value to the best codec profile
|
||||
// setting.default = keys[keys.length - 1];
|
||||
}
|
||||
},
|
||||
},
|
||||
[PrefKey.PREFER_IPV6_SERVER]: {
|
||||
label: t('prefer-ipv6-server'),
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.SCREENSHOT_BUTTON_POSITION]: {
|
||||
label: t('screenshot-button-position'),
|
||||
default: 'bottom-left',
|
||||
options: {
|
||||
'bottom-left': t('bottom-left'),
|
||||
'bottom-right': t('bottom-right'),
|
||||
'none': t('disable'),
|
||||
},
|
||||
},
|
||||
[PrefKey.SCREENSHOT_APPLY_FILTERS]: {
|
||||
label: t('screenshot-apply-filters'),
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.SKIP_SPLASH_VIDEO]: {
|
||||
label: t('skip-splash-video'),
|
||||
default: false,
|
||||
},
|
||||
[PrefKey.HIDE_DOTS_ICON]: {
|
||||
label: t('hide-system-menu-icon'),
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.STREAM_COMBINE_SOURCES]: {
|
||||
label: t('combine-audio-video-streams'),
|
||||
default: false,
|
||||
experimental: true,
|
||||
note: t('combine-audio-video-streams-summary'),
|
||||
},
|
||||
|
||||
[PrefKey.STREAM_TOUCH_CONTROLLER]: {
|
||||
label: t('tc-availability'),
|
||||
default: 'all',
|
||||
options: {
|
||||
default: t('default'),
|
||||
all: t('tc-all-games'),
|
||||
off: t('off'),
|
||||
},
|
||||
unsupported: !STATES.hasTouchSupport,
|
||||
ready: () => {
|
||||
const setting = Preferences.SETTINGS[PrefKey.STREAM_TOUCH_CONTROLLER];
|
||||
if (setting.unsupported) {
|
||||
setting.default = 'default';
|
||||
}
|
||||
},
|
||||
},
|
||||
[PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF]: {
|
||||
label: t('tc-auto-off'),
|
||||
default: false,
|
||||
unsupported: !STATES.hasTouchSupport,
|
||||
},
|
||||
[PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: {
|
||||
label: t('tc-standard-layout-style'),
|
||||
default: 'default',
|
||||
options: {
|
||||
default: t('default'),
|
||||
white: t('tc-all-white'),
|
||||
muted: t('tc-muted-colors'),
|
||||
},
|
||||
unsupported: !STATES.hasTouchSupport,
|
||||
},
|
||||
[PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM]: {
|
||||
label: t('tc-custom-layout-style'),
|
||||
default: 'default',
|
||||
options: {
|
||||
default: t('default'),
|
||||
muted: t('tc-muted-colors'),
|
||||
},
|
||||
unsupported: !STATES.hasTouchSupport,
|
||||
},
|
||||
|
||||
[PrefKey.STREAM_SIMPLIFY_MENU]: {
|
||||
label: t('simplify-stream-menu'),
|
||||
default: false,
|
||||
},
|
||||
[PrefKey.MKB_HIDE_IDLE_CURSOR]: {
|
||||
label: t('hide-idle-cursor'),
|
||||
default: false,
|
||||
},
|
||||
[PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG]: {
|
||||
label: t('disable-post-stream-feedback-dialog'),
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.LOCAL_CO_OP_ENABLED]: {
|
||||
label: t('enable-local-co-op-support'),
|
||||
default: false,
|
||||
note: CE<HTMLAnchorElement>('a', {
|
||||
href: 'https://github.com/redphx/better-xcloud/discussions/275',
|
||||
target: '_blank',
|
||||
}, t('enable-local-co-op-support-note')),
|
||||
},
|
||||
|
||||
/*
|
||||
[Preferences.LOCAL_CO_OP_SEPARATE_TOUCH_CONTROLLER]: {
|
||||
default: false,
|
||||
'note': t('separate-touch-controller-note'),
|
||||
},
|
||||
*/
|
||||
|
||||
[PrefKey.CONTROLLER_ENABLE_SHORTCUTS]: {
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.CONTROLLER_ENABLE_VIBRATION]: {
|
||||
default: true,
|
||||
},
|
||||
|
||||
[PrefKey.CONTROLLER_DEVICE_VIBRATION]: {
|
||||
default: 'off',
|
||||
options: {
|
||||
on: t('on'),
|
||||
auto: t('device-vibration-not-using-gamepad'),
|
||||
off: t('off'),
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.CONTROLLER_VIBRATION_INTENSITY]: {
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 100,
|
||||
min: 0,
|
||||
max: 100,
|
||||
steps: 10,
|
||||
params: {
|
||||
suffix: '%',
|
||||
ticks: 10,
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.MKB_ENABLED]: {
|
||||
label: t('enable-mkb'),
|
||||
default: false,
|
||||
unsupported: ((): string | boolean => {
|
||||
const userAgent = ((window.navigator as any).orgUserAgent || window.navigator.userAgent || '').toLowerCase();
|
||||
return userAgent.match(/(android|iphone|ipad)/) ? t('browser-unsupported-feature') : false;
|
||||
})(),
|
||||
ready: () => {
|
||||
const pref = Preferences.SETTINGS[PrefKey.MKB_ENABLED];
|
||||
|
||||
let note;
|
||||
let url;
|
||||
if (pref.unsupported) {
|
||||
note = t('browser-unsupported-feature');
|
||||
url = 'https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657';
|
||||
} else {
|
||||
note = t('mkb-disclaimer');
|
||||
url = 'https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer';
|
||||
}
|
||||
|
||||
Preferences.SETTINGS[PrefKey.MKB_ENABLED].note = CE('a', {
|
||||
href: url,
|
||||
target: '_blank',
|
||||
}, '⚠️ ' + note);
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.MKB_DEFAULT_PRESET_ID]: {
|
||||
default: 0,
|
||||
},
|
||||
|
||||
[PrefKey.MKB_ABSOLUTE_MOUSE]: {
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.REDUCE_ANIMATIONS]: {
|
||||
label: t('reduce-animations'),
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.UI_LOADING_SCREEN_GAME_ART]: {
|
||||
label: t('show-game-art'),
|
||||
default: true,
|
||||
},
|
||||
[PrefKey.UI_LOADING_SCREEN_WAIT_TIME]: {
|
||||
label: t('show-wait-time'),
|
||||
default: true,
|
||||
},
|
||||
[PrefKey.UI_LOADING_SCREEN_ROCKET]: {
|
||||
label: t('rocket-animation'),
|
||||
default: 'show',
|
||||
options: {
|
||||
show: t('rocket-always-show'),
|
||||
'hide-queue': t('rocket-hide-queue'),
|
||||
hide: t('rocket-always-hide'),
|
||||
},
|
||||
},
|
||||
[PrefKey.UI_LAYOUT]: {
|
||||
label: t('layout'),
|
||||
default: 'default',
|
||||
options: {
|
||||
default: t('default'),
|
||||
tv: t('smart-tv'),
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.UI_SCROLLBAR_HIDE]: {
|
||||
label: t('hide-scrollbar'),
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.BLOCK_SOCIAL_FEATURES]: {
|
||||
label: t('disable-social-features'),
|
||||
default: false,
|
||||
},
|
||||
[PrefKey.BLOCK_TRACKING]: {
|
||||
label: t('disable-xcloud-analytics'),
|
||||
default: false,
|
||||
},
|
||||
[PrefKey.USER_AGENT_PROFILE]: {
|
||||
label: t('user-agent-profile'),
|
||||
default: 'default',
|
||||
options: {
|
||||
[UserAgentProfile.DEFAULT]: t('default'),
|
||||
[UserAgentProfile.EDGE_WINDOWS]: 'Edge + Windows',
|
||||
[UserAgentProfile.SAFARI_MACOS]: 'Safari + macOS',
|
||||
[UserAgentProfile.SMARTTV_TIZEN]: 'Samsung Smart TV',
|
||||
[UserAgentProfile.CUSTOM]: t('custom'),
|
||||
},
|
||||
},
|
||||
[PrefKey.USER_AGENT_CUSTOM]: {
|
||||
default: '',
|
||||
},
|
||||
[PrefKey.VIDEO_CLARITY]: {
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 5,
|
||||
params: {
|
||||
hideSlider: true,
|
||||
},
|
||||
},
|
||||
[PrefKey.VIDEO_RATIO]: {
|
||||
default: '16:9',
|
||||
options: {
|
||||
'16:9': '16:9',
|
||||
'18:9': '18:9',
|
||||
'21:9': '21:9',
|
||||
'16:10': '16:10',
|
||||
'4:3': '4:3',
|
||||
|
||||
fill: t('stretch'),
|
||||
//'cover': 'Cover',
|
||||
},
|
||||
},
|
||||
[PrefKey.VIDEO_SATURATION]: {
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 100,
|
||||
min: 50,
|
||||
max: 150,
|
||||
params: {
|
||||
suffix: '%',
|
||||
ticks: 25,
|
||||
},
|
||||
},
|
||||
[PrefKey.VIDEO_CONTRAST]: {
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 100,
|
||||
min: 50,
|
||||
max: 150,
|
||||
params: {
|
||||
suffix: '%',
|
||||
ticks: 25,
|
||||
},
|
||||
},
|
||||
[PrefKey.VIDEO_BRIGHTNESS]: {
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 100,
|
||||
min: 50,
|
||||
max: 150,
|
||||
params: {
|
||||
suffix: '%',
|
||||
ticks: 25,
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.AUDIO_MIC_ON_PLAYING]: {
|
||||
label: t('enable-mic-on-startup'),
|
||||
default: false,
|
||||
},
|
||||
[PrefKey.AUDIO_ENABLE_VOLUME_CONTROL]: {
|
||||
label: t('enable-volume-control'),
|
||||
default: false,
|
||||
experimental: true,
|
||||
},
|
||||
[PrefKey.AUDIO_VOLUME]: {
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 100,
|
||||
min: 0,
|
||||
max: 600,
|
||||
params: {
|
||||
suffix: '%',
|
||||
ticks: 100,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
[PrefKey.STATS_ITEMS]: {
|
||||
default: [StreamStat.PING, StreamStat.FPS, StreamStat.BITRATE, StreamStat.DECODE_TIME, StreamStat.PACKETS_LOST, StreamStat.FRAMES_LOST],
|
||||
multipleOptions: {
|
||||
[StreamStat.PING]: `${StreamStat.PING.toUpperCase()}: ${t('stat-ping')}`,
|
||||
[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')}`,
|
||||
},
|
||||
params: {
|
||||
size: 6,
|
||||
},
|
||||
},
|
||||
[PrefKey.STATS_SHOW_WHEN_PLAYING]: {
|
||||
default: false,
|
||||
},
|
||||
[PrefKey.STATS_QUICK_GLANCE]: {
|
||||
default: true,
|
||||
},
|
||||
[PrefKey.STATS_POSITION]: {
|
||||
default: 'top-right',
|
||||
options: {
|
||||
'top-left': t('top-left'),
|
||||
'top-center': t('top-center'),
|
||||
'top-right': t('top-right'),
|
||||
},
|
||||
},
|
||||
[PrefKey.STATS_TEXT_SIZE]: {
|
||||
default: '0.9rem',
|
||||
options: {
|
||||
'0.9rem': t('small'),
|
||||
'1.0rem': t('normal'),
|
||||
'1.1rem': t('large'),
|
||||
},
|
||||
},
|
||||
[PrefKey.STATS_TRANSPARENT]: {
|
||||
default: false,
|
||||
},
|
||||
[PrefKey.STATS_OPACITY]: {
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 80,
|
||||
min: 50,
|
||||
max: 100,
|
||||
params: {
|
||||
suffix: '%',
|
||||
ticks: 10,
|
||||
},
|
||||
},
|
||||
[PrefKey.STATS_CONDITIONAL_FORMATTING]: {
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.REMOTE_PLAY_ENABLED]: {
|
||||
label: t('enable-remote-play-feature'),
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.REMOTE_PLAY_RESOLUTION]: {
|
||||
default: '1080p',
|
||||
options: {
|
||||
'1080p': '1080p',
|
||||
'720p': '720p',
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.GAME_FORTNITE_FORCE_CONSOLE]: {
|
||||
label: '🎮 ' + t('fortnite-force-console-version'),
|
||||
default: false,
|
||||
note: t('fortnite-allow-stw-mode'),
|
||||
},
|
||||
|
||||
// Deprecated
|
||||
/*
|
||||
[Preferences.DEPRECATED_CONTROLLER_SUPPORT_LOCAL_CO_OP]: {
|
||||
default: false,
|
||||
'migrate': function(savedPrefs, value) {
|
||||
this.set(Preferences.LOCAL_CO_OP_ENABLED, value);
|
||||
savedPrefs[Preferences.LOCAL_CO_OP_ENABLED] = value;
|
||||
},
|
||||
},
|
||||
*/
|
||||
}
|
||||
|
||||
#storage = localStorage;
|
||||
#key = 'better_xcloud';
|
||||
#prefs: {[index: string]: any} = {};
|
||||
|
||||
constructor() {
|
||||
let savedPrefsStr = this.#storage.getItem(this.#key);
|
||||
if (savedPrefsStr == null) {
|
||||
savedPrefsStr = '{}';
|
||||
}
|
||||
|
||||
const savedPrefs = JSON.parse(savedPrefsStr);
|
||||
|
||||
for (let settingId in Preferences.SETTINGS) {
|
||||
const setting = Preferences.SETTINGS[settingId];
|
||||
setting.ready && setting.ready.call(this);
|
||||
|
||||
if (setting.migrate && settingId in savedPrefs) {
|
||||
setting.migrate.call(this, savedPrefs, savedPrefs[settingId]);
|
||||
}
|
||||
}
|
||||
|
||||
for (let settingId in Preferences.SETTINGS) {
|
||||
const setting = Preferences.SETTINGS[settingId];
|
||||
if (!setting) {
|
||||
alert(`Undefined setting key: ${settingId}`);
|
||||
console.log('Undefined setting key');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore deprecated settings
|
||||
if (setting.migrate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (settingId in savedPrefs) {
|
||||
this.#prefs[settingId] = this.#validateValue(settingId, savedPrefs[settingId]);
|
||||
} else {
|
||||
this.#prefs[settingId] = setting.default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#validateValue(key: keyof typeof Preferences.SETTINGS, value: any) {
|
||||
const config = Preferences.SETTINGS[key];
|
||||
if (!config) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined' || value === null) {
|
||||
value = config.default;
|
||||
}
|
||||
|
||||
if ('min' in config) {
|
||||
value = Math.max(config.min!, value);
|
||||
}
|
||||
|
||||
if ('max' in config) {
|
||||
value = Math.min(config.max!, value);
|
||||
}
|
||||
|
||||
if ('options' in config && !(value in config.options!)) {
|
||||
value = config.default;
|
||||
} else if ('multipleOptions' in config) {
|
||||
if (value.length) {
|
||||
const validOptions = Object.keys(config.multipleOptions!);
|
||||
value.forEach((item: any, idx: number) => {
|
||||
(validOptions.indexOf(item) === -1) && value.splice(idx, 1);
|
||||
});
|
||||
}
|
||||
|
||||
if (!value.length) {
|
||||
value = config.default;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
get(key: PrefKey) {
|
||||
if (typeof key === 'undefined') {
|
||||
debugger;
|
||||
return;
|
||||
}
|
||||
|
||||
// Return default value if the feature is not supported
|
||||
if (Preferences.SETTINGS[key].unsupported) {
|
||||
return Preferences.SETTINGS[key].default;
|
||||
}
|
||||
|
||||
if (!(key in this.#prefs)) {
|
||||
this.#prefs[key] = this.#validateValue(key, null);
|
||||
}
|
||||
|
||||
return this.#prefs[key];
|
||||
}
|
||||
|
||||
set(key: PrefKey, value: any) {
|
||||
value = this.#validateValue(key, value);
|
||||
|
||||
this.#prefs[key] = value;
|
||||
this.#updateStorage();
|
||||
}
|
||||
|
||||
#updateStorage() {
|
||||
this.#storage.setItem(this.#key, JSON.stringify(this.#prefs));
|
||||
}
|
||||
|
||||
toElement(key: keyof typeof Preferences.SETTINGS, onChange: any, overrideParams={}) {
|
||||
const setting = Preferences.SETTINGS[key];
|
||||
let currentValue = this.get(key);
|
||||
|
||||
let $control;
|
||||
let type;
|
||||
if ('type' in setting) {
|
||||
type = setting.type;
|
||||
} else if ('options' in setting) {
|
||||
type = SettingElementType.OPTIONS;
|
||||
} else if ('multipleOptions' in setting) {
|
||||
type = SettingElementType.MULTIPLE_OPTIONS;
|
||||
} else if (typeof setting.default === 'number') {
|
||||
type = SettingElementType.NUMBER;
|
||||
} else {
|
||||
type = SettingElementType.CHECKBOX;
|
||||
}
|
||||
|
||||
const params = Object.assign(overrideParams, setting.params || {});
|
||||
if (params.disabled) {
|
||||
currentValue = Preferences.SETTINGS[key].default;
|
||||
}
|
||||
|
||||
$control = SettingElement.render(type!, key as string, setting, currentValue, (e: any, value: any) => {
|
||||
this.set(key, value);
|
||||
onChange && onChange(e, value);
|
||||
}, params);
|
||||
|
||||
return $control;
|
||||
}
|
||||
|
||||
toNumberStepper(key: keyof typeof Preferences.SETTINGS, onChange: any, options={}) {
|
||||
return SettingElement.render(SettingElementType.NUMBER_STEPPER, key, Preferences.SETTINGS[key], this.get(key), (e: any, value: any) => {
|
||||
this.set(key, value);
|
||||
onChange && onChange(e, value);
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const prefs = new Preferences();
|
||||
export const getPref = prefs.get.bind(prefs);
|
||||
export const setPref = prefs.set.bind(prefs);
|
||||
export const toPrefElement = prefs.toElement.bind(prefs);
|
@@ -1,4 +1,4 @@
|
||||
import { getPref, PrefKey } from "../modules/preferences";
|
||||
import { getPref, PrefKey } from "./preferences";
|
||||
import { STATES } from "./global";
|
||||
|
||||
|
||||
|
268
src/utils/settings.ts
Normal file
268
src/utils/settings.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import type { PreferenceSetting } from "../types/preferences";
|
||||
import { CE } from "./html";
|
||||
|
||||
type MultipleOptionsParams = {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
type NumberStepperParams = {
|
||||
suffix?: string;
|
||||
disabled?: boolean;
|
||||
hideSlider?: boolean;
|
||||
|
||||
ticks?: number;
|
||||
exactTicks?: number;
|
||||
}
|
||||
|
||||
export enum SettingElementType {
|
||||
OPTIONS = 'options',
|
||||
MULTIPLE_OPTIONS = 'multiple-options',
|
||||
NUMBER = 'number',
|
||||
NUMBER_STEPPER = 'number-stepper',
|
||||
CHECKBOX = 'checkbox',
|
||||
}
|
||||
|
||||
export class SettingElement {
|
||||
static #renderOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any) {
|
||||
const $control = CE<HTMLSelectElement>('select') as HTMLSelectElement;
|
||||
for (let value in setting.options) {
|
||||
const label = setting.options[value];
|
||||
|
||||
const $option = CE<HTMLOptionElement>('option', {value: value}, label);
|
||||
$control.appendChild($option);
|
||||
}
|
||||
|
||||
$control.value = currentValue;
|
||||
onChange && $control.addEventListener('change', e => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
const value = (setting.type && setting.type === 'number') ? parseInt(target.value) : target.value;
|
||||
onChange(e, value);
|
||||
});
|
||||
|
||||
// Custom method
|
||||
($control as any).setValue = (value: any) => {
|
||||
$control.value = value;
|
||||
};
|
||||
|
||||
return $control;
|
||||
}
|
||||
|
||||
static #renderMultipleOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any, params: MultipleOptionsParams={}) {
|
||||
const $control = CE<HTMLSelectElement>('select', {'multiple': true});
|
||||
if (params && params.size) {
|
||||
$control.setAttribute('size', params.size.toString());
|
||||
}
|
||||
|
||||
for (let value in setting.multipleOptions) {
|
||||
const label = setting.multipleOptions[value];
|
||||
|
||||
const $option = CE<HTMLOptionElement>('option', {value: value}, label) as HTMLOptionElement;
|
||||
$option.selected = currentValue.indexOf(value) > -1;
|
||||
|
||||
$option.addEventListener('mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const target = e.target as HTMLOptionElement;
|
||||
target.selected = !target.selected;
|
||||
|
||||
const $parent = target.parentElement!;
|
||||
$parent.focus();
|
||||
$parent.dispatchEvent(new Event('change'));
|
||||
});
|
||||
|
||||
$control.appendChild($option);
|
||||
}
|
||||
|
||||
$control.addEventListener('mousedown', function(e) {
|
||||
const self = this;
|
||||
const orgScrollTop = self.scrollTop;
|
||||
window.setTimeout(() => (self.scrollTop = orgScrollTop), 0);
|
||||
});
|
||||
|
||||
$control.addEventListener('mousemove', e => e.preventDefault());
|
||||
|
||||
onChange && $control.addEventListener('change', (e: Event) => {
|
||||
const target = e.target as HTMLSelectElement
|
||||
const values = Array.from(target.selectedOptions).map(i => i.value);
|
||||
onChange(e, values);
|
||||
});
|
||||
|
||||
return $control;
|
||||
}
|
||||
|
||||
static #renderNumber(key: string, setting: PreferenceSetting, currentValue: any, onChange: any) {
|
||||
const $control = CE('input', {'type': 'number', 'min': setting.min, 'max': setting.max}) as HTMLInputElement;
|
||||
$control.value = currentValue;
|
||||
onChange && $control.addEventListener('change', (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();
|
||||
|
||||
onChange(e, value);
|
||||
});
|
||||
|
||||
return $control;
|
||||
}
|
||||
|
||||
static #renderCheckbox(key: string, setting: PreferenceSetting, currentValue: any, onChange: any) {
|
||||
const $control = CE('input', {'type': 'checkbox'}) as HTMLInputElement;
|
||||
$control.checked = currentValue;
|
||||
|
||||
onChange && $control.addEventListener('change', e => {
|
||||
onChange(e, (e.target as HTMLInputElement).checked);
|
||||
});
|
||||
|
||||
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 $decBtn: HTMLButtonElement;
|
||||
let $incBtn: HTMLButtonElement;
|
||||
let $range: HTMLInputElement;
|
||||
|
||||
const MIN = setting.min!;
|
||||
const MAX = setting.max!;
|
||||
const STEPS = Math.max(setting.steps || 1, 1);
|
||||
|
||||
const $wrapper = CE('div', {'class': 'bx-number-stepper'},
|
||||
$decBtn = CE('button', {'data-type': 'dec'}, '-') as HTMLButtonElement,
|
||||
$text = CE('span', {}, value + options.suffix) as HTMLSpanElement,
|
||||
$incBtn = CE('button', {'data-type': 'inc'}, '+') as HTMLButtonElement,
|
||||
);
|
||||
|
||||
if (!options.disabled && !options.hideSlider) {
|
||||
$range = CE('input', {'type': 'range', 'min': MIN, 'max': MAX, 'value': value, 'step': STEPS}) as HTMLInputElement;
|
||||
$range.addEventListener('input', e => {
|
||||
value = parseInt((e.target as HTMLInputElement).value);
|
||||
|
||||
$text.textContent = value + options.suffix;
|
||||
onChange && onChange(e, value);
|
||||
});
|
||||
$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(MIN / options.exactTicks), 1) * options.exactTicks;
|
||||
|
||||
if (start === MIN) {
|
||||
start += options.exactTicks;
|
||||
}
|
||||
|
||||
for (let i = start; i < MAX; i += options.exactTicks) {
|
||||
$markers.appendChild(CE<HTMLOptionElement>('option', {'value': i}));
|
||||
}
|
||||
} else {
|
||||
for (let i = MIN + options.ticks!; i < MAX; i += options.ticks!) {
|
||||
$markers.appendChild(CE<HTMLOptionElement>('option', {'value': i}));
|
||||
}
|
||||
}
|
||||
$wrapper.appendChild($markers);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.disabled) {
|
||||
$incBtn.disabled = true;
|
||||
$incBtn.classList.add('bx-hidden');
|
||||
|
||||
$decBtn.disabled = true;
|
||||
$decBtn.classList.add('bx-hidden');
|
||||
return $wrapper;
|
||||
}
|
||||
|
||||
let interval: number;
|
||||
let isHolding = false;
|
||||
|
||||
const onClick = (e: Event) => {
|
||||
if (isHolding) {
|
||||
e.preventDefault();
|
||||
isHolding = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let value = parseInt($range.value);
|
||||
const btnType = (e.target as HTMLElement).getAttribute('data-type');
|
||||
if (btnType === 'dec') {
|
||||
value = Math.max(MIN, value - STEPS);
|
||||
} else {
|
||||
value = Math.min(MAX, value + STEPS);
|
||||
}
|
||||
|
||||
$text.textContent = value.toString() + options.suffix;
|
||||
$range && ($range.value = value.toString());
|
||||
|
||||
isHolding = false;
|
||||
onChange && onChange(e, value);
|
||||
}
|
||||
|
||||
const onMouseDown = (e: MouseEvent | TouchEvent) => {
|
||||
isHolding = true;
|
||||
|
||||
const args = arguments;
|
||||
interval = window.setInterval(() => {
|
||||
const event = new Event('click');
|
||||
(event as any).arguments = args;
|
||||
|
||||
e.target?.dispatchEvent(event);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const onMouseUp = (e: MouseEvent | TouchEvent) => {
|
||||
clearInterval(interval);
|
||||
isHolding = false;
|
||||
};
|
||||
|
||||
// Custom method
|
||||
($wrapper as any).setValue = (value: any) => {
|
||||
$text.textContent = value + options.suffix;
|
||||
$range && ($range.value = value);
|
||||
};
|
||||
|
||||
$decBtn.addEventListener('click', onClick);
|
||||
$decBtn.addEventListener('mousedown', onMouseDown);
|
||||
$decBtn.addEventListener('mouseup', onMouseUp);
|
||||
$decBtn.addEventListener('touchstart', onMouseDown);
|
||||
$decBtn.addEventListener('touchend', onMouseUp);
|
||||
|
||||
$incBtn.addEventListener('click', onClick);
|
||||
$incBtn.addEventListener('mousedown', onMouseDown);
|
||||
$incBtn.addEventListener('mouseup', onMouseUp);
|
||||
$incBtn.addEventListener('touchstart', onMouseDown);
|
||||
$incBtn.addEventListener('touchend', onMouseUp);
|
||||
|
||||
return $wrapper;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
static render(type: SettingElementType, key: string, setting: PreferenceSetting, currentValue: any, onChange: any, options: any) {
|
||||
const method = SettingElement.#METHOD_MAP[type];
|
||||
// @ts-ignore
|
||||
const $control = method(...Array.from(arguments).slice(1)) as HTMLElement;
|
||||
$control.id = `bx_setting_${key}`;
|
||||
|
||||
// Add "name" property to "select" elements
|
||||
if (type === SettingElementType.OPTIONS || type === SettingElementType.MULTIPLE_OPTIONS) {
|
||||
($control as HTMLSelectElement).name = $control.id;
|
||||
}
|
||||
|
||||
return $control;
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { PrefKey } from "../modules/preferences";
|
||||
import { getPref } from "../modules/preferences";
|
||||
import { PrefKey } from "./preferences";
|
||||
import { getPref } from "./preferences";
|
||||
import { STATES } from "./global";
|
||||
import { UserAgent } from "./user-agent";
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { PrefKey, getPref } from "../modules/preferences";
|
||||
import { PrefKey, getPref } from "./preferences";
|
||||
|
||||
export enum UserAgentProfile {
|
||||
EDGE_WINDOWS = 'edge-windows',
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { PrefKey, getPref, setPref } from "../modules/preferences";
|
||||
import { PrefKey, getPref, setPref } from "./preferences";
|
||||
import { SCRIPT_VERSION } from "./global";
|
||||
import { UserAgent } from "./user-agent";
|
||||
|
||||
|
Reference in New Issue
Block a user