Compare commits

...

55 Commits

Author SHA1 Message Date
458928d615 Bump version to 6.0.6 2024-12-13 06:32:52 +07:00
20bf2b1ab6 Turn on "EnableTakControlResizing" flag 2024-12-13 06:32:32 +07:00
901f55c683 Show different colors for wait time 2024-12-13 06:23:58 +07:00
15bb18644f Fix wait time stopped showing in game tile (#597) 2024-12-13 05:56:02 +07:00
873f6546a4 Fix input slider not working with gamepad (#596) 2024-12-13 05:41:25 +07:00
1db7d4f8d7 Set unadjustedMovement for MKB 2024-12-12 21:37:41 +07:00
e0b04f306f Bump version to 6.0.5 2024-12-12 06:55:00 +07:00
a3c948b070 Fix problem with Smart TV profile and Guide menu (#594) 2024-12-12 06:53:25 +07:00
4e736175b4 Fix Bx button in Guide menu not working 2024-12-12 06:46:41 +07:00
cb66340177 Fix not showing Bx button in unsupported page 2024-12-12 06:35:07 +07:00
9f5f7b9d2e Bump version to 6.0.4 2024-12-11 20:37:10 +07:00
d04742bc25 Update translations 2024-12-11 20:36:51 +07:00
ed871bbe83 Update dists 2024-12-11 18:59:31 +07:00
dca8ab9cf6 Fix stats texts 2024-12-11 18:59:24 +07:00
1bf2f41813 Fix not getting the correct candidate pair 2024-12-11 18:55:28 +07:00
0fb3b7b7f7 Pad stats 2024-12-11 18:08:28 +07:00
7709cceff0 Add stat's background opacity 2024-12-11 17:50:04 +07:00
f8b8012f5c Add "ignoreNewsSection" patch 2024-12-11 17:21:20 +07:00
1d8517a997 Update <select multiple> CSS 2024-12-11 07:51:41 +07:00
c893bb2a5d Block notifications 2024-12-11 07:25:48 +07:00
46469e3949 Fix Guide CSS in TV layout 2024-12-11 06:12:15 +07:00
d8a085d43f Update suggestion's styles 2024-12-10 21:53:57 +07:00
b84c464066 Add "Disable features" setting 2024-12-10 21:30:21 +07:00
f0549b388a Update dists 2024-12-10 20:55:12 +07:00
9c3b1bd908 Change background color of selected options in <select multiple> 2024-12-10 20:54:53 +07:00
d671be21ee Also disable Friends feature when blocking social features 2024-12-10 20:53:43 +07:00
11aefb34d1 Fix overriding features not working 2024-12-10 20:51:27 +07:00
597cc9782d Always show error log 2024-12-10 20:51:00 +07:00
61cfd3f8db Alert new server 2024-12-10 20:50:42 +07:00
a3d5d6a819 Add note for local co-op feature 2024-12-10 20:50:27 +07:00
ca64b592c5 Bump version to 6.0.3 2024-12-09 20:08:59 +07:00
d0a8b894b9 Show indicator for current preset 2024-12-09 19:59:30 +07:00
3230b99a05 Show lock icon in Default preset 2024-12-09 19:36:52 +07:00
f0e4d4b8d0 Fix exception in app 2024-12-09 18:18:40 +07:00
d0b84d4591 Update dists 2024-12-09 17:44:24 +07:00
d292bef5e7 Only show unbind note on custom presets 2024-12-09 17:42:51 +07:00
5381575048 Add "Xbox button > Press" shortcut 2024-12-09 17:42:11 +07:00
7206c9e8bc Migrate more events to EventBus 2024-12-09 07:01:13 +07:00
5fb0dec9f2 Call methods inside app in EventBus 2024-12-08 22:20:46 +07:00
4ffc034076 Rename EventBus events 2024-12-08 21:57:29 +07:00
b11d465804 Migrate to EventBus 2024-12-08 21:06:42 +07:00
e1ba2344b7 Declare window.navigator typing 2024-12-08 20:52:13 +07:00
8c446ceec3 Refactor patches 2024-12-08 20:26:05 +07:00
7438375356 Fix disableAdobeAudienceManager() 2024-12-08 20:24:15 +07:00
741bc9a4e5 Rename EventBus to BxEventBus 2024-12-08 20:09:12 +07:00
de7bf3edc7 Refactor 2024-12-08 20:05:29 +07:00
79ebb1a817 EventBus (#590)
* Replace BxEvent.TITLE_INFO_READY with Event Bus

* Migrate more events

* Migrate stream events to event bus

* Migrate preset events

* Migrate more

* Fix dispatching "input" event twice in Number Stepper
2024-12-08 17:55:44 +07:00
160044c958 Add new domain to ignore 2024-12-08 11:36:35 +07:00
78c70b5d90 Change "Max FPS" to "Limit FPS" 2024-12-08 11:20:35 +07:00
9044a07c0b Add note for default presets 2024-12-08 10:49:09 +07:00
e40d258c79 Bump version to 6.0.2 2024-12-08 07:11:10 +07:00
3864457a07 Fix applying "disableAbsoluteMouse" patch in the wrong place 2024-12-08 07:08:15 +07:00
da362325f2 Disable absolute mouse in Android app 2024-12-08 06:50:08 +07:00
4062852904 Beta 2024-12-07 22:09:22 +07:00
c426f64ea9 Add generateMsDeviceInfo() 2024-12-07 22:00:50 +07:00
73 changed files with 1557 additions and 1023 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 6.0.1
// @version 6.0.6
// ==/UserScript==

File diff suppressed because one or more lines are too long

View File

@ -7,11 +7,11 @@
margin-bottom: 0 !important;
}
html[data-xds-platform=tv] & {
body[data-bx-media-type=tv] & {
flex-direction: column;
}
html:not([data-xds-platform=tv]) & {
body:not([data-bx-media-type=tv]) & {
flex-direction: row;
> button:first-of-type {
@ -34,7 +34,7 @@
flex-direction: row;
gap: 12px;
html[data-xds-platform=tv] & {
body[data-bx-media-type=tv] & {
flex-direction: column;
button {
@ -42,7 +42,7 @@
}
}
html:not([data-xds-platform=tv]) & {
body:not([data-bx-media-type=tv]) & {
button {
span {
display: none;

View File

@ -16,7 +16,7 @@
margin-right: -50%;
transform: translate(-50%, -50%);
min-width: 420px;
padding: 20px;
padding: 16px;
border-radius: 8px;
z-index: var(--bx-key-binding-dialog-z-index);
background: #1a1b1e;

View File

@ -45,7 +45,7 @@
width: 450px;
max-width: calc(100vw - 20px);
margin: 0 0 0 auto;
padding: 20px;
padding: 16px;
max-height: 95vh;
flex-direction: column;
@ -90,6 +90,13 @@
flex: 1;
}
}
.bx-default-preset-note {
font-size: 12px;
font-style: italic;
text-align: center;
margin-bottom: 10px;
}
}
.bx-centered-dialog,
@ -214,5 +221,6 @@
.bx-settings-row {
background: none;
padding: 10px;
}
}

View File

@ -10,7 +10,7 @@
width: 420px;
max-width: calc(100vw - 20px);
margin: 0 0 0 auto;
padding: 20px;
padding: 16px;
> .bx-button {
display: table;

View File

@ -140,8 +140,28 @@ div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module
font-family: var(--bx-normal-font) !important;
}
select[multiple] {
select[multiple], select[multiple]:focus {
overflow: auto;
border: none;
option {
padding: 4px 6px;
&:checked {
color = #1a7bc0;
background: color linear-gradient(0deg, color 0%, color 100%);
&::before {
content: '';
font-size: 12px;
display: inline-block;
margin-right: 6px;
height: 100%;
line-height: 100%;
vertical-align: middle;
}
}
}
}
/* Hide UI elements */
@ -174,7 +194,7 @@ div[class*=SupportedInputsBadge] {
top: 0;
left: 0;
z-index: 1;
background: #0000008c;
background: rgba(0, 0, 0, 0.5);
display: flex;
border-radius: 4px 0 4px 0;
align-items: center;
@ -194,6 +214,18 @@ div[class*=SupportedInputsBadge] {
font-weight: bold;
margin-left: 2px;
}
&[data-duration=short] {
background-color: rgba(0, 133, 133, 0.75);
}
&[data-duration=medium] {
background-color: rgba(213, 133, 0, 0.75);
}
&[data-duration=long] {
background-color: rgba(150, 0, 0, 0.75);
}
}

View File

@ -324,18 +324,22 @@
border-radius: 4px;
overflow: hidden;
background: #003861;
height: 45px;
align-items: center;
label {
flex: 1;
padding: 10px;
align-content: center;
padding: 0 10px;
background: #004f87;
height: 100%;
}
span {
display: inline-block;
align-self: center;
padding: 10px;
width: 40px;
width: 45px;
text-align: center;
}

View File

@ -144,7 +144,7 @@ div[class^=StreamMenu-module__container] .bx-badges {
border-radius: 0 0 4px 4px;
}
&[data-transparent=true] {
&[data-shadow=true] {
background: none;
filter: drop-shadow(1px 0 0 #000000f0) drop-shadow(-1px 0 0 #000000f0) drop-shadow(0 1px 0 #000000f0) drop-shadow(0 -1px 0 #000000f0);
}
@ -165,10 +165,10 @@ div[class^=StreamMenu-module__container] .bx-badges {
}
span {
min-width: 60px;
display: inline-block;
text-align: right;
vertical-align: middle;
white-space: pre;
&[data-grade=good] {
color: #6bffff;
@ -181,9 +181,5 @@ div[class^=StreamMenu-module__container] .bx-badges {
&[data-grade=bad] {
color: #ff5f5f;
}
&:first-of-type {
min-width: 22px;
}
}
}

View File

@ -62,6 +62,7 @@ div.bx-select {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
min-height: 15px;
span {
display: block;
@ -70,6 +71,8 @@ div.bx-select {
text-align: left;
line-height: initial;
white-space: pre;
min-height: 15px;
align-content: center;
}
}
}

View File

@ -63,7 +63,7 @@ export const enum PrefKey {
SCREENSHOT_APPLY_FILTERS = 'screenshot.applyFilters',
BLOCK_TRACKING = 'block.tracking',
BLOCK_SOCIAL_FEATURES = 'block.social',
BLOCK_FEATURES = 'block.features',
LOADING_SCREEN_GAME_ART = 'loadingScreen.gameArt.show',
LOADING_SCREEN_SHOW_WAIT_TIME = 'loadingScreen.waitTime.show',
@ -73,7 +73,6 @@ export const enum PrefKey {
UI_LAYOUT = 'ui.layout',
UI_SCROLLBAR_HIDE = 'ui.hideScrollbar',
UI_HIDE_SECTIONS = 'ui.hideSections',
BYOG_DISABLED = 'feature.byog.disabled',
UI_GAME_CARD_SHOW_WAIT_TIME = 'ui.gameCard.waitTime.show',
UI_SIMPLIFY_STREAM_MENU = 'ui.streamMenu.simplify',
@ -104,8 +103,8 @@ export const enum PrefKey {
STATS_QUICK_GLANCE_ENABLED = 'stats.quickGlance.enabled',
STATS_POSITION = 'stats.position',
STATS_TEXT_SIZE = 'stats.textSize',
STATS_TRANSPARENT = 'stats.transparent',
STATS_OPACITY = 'stats.opacity',
STATS_OPACITY_ALL = 'stats.opacity.all',
STATS_OPACITY_BACKGROUND = 'stats.opacity.background',
STATS_CONDITIONAL_FORMATTING = 'stats.colors',
REMOTE_PLAY_ENABLED = 'xhome.enabled',

View File

@ -110,3 +110,11 @@ export const enum StreamVideoProcessing {
USM = 'usm',
CAS = 'cas',
}
export const enum BlockFeature {
CHAT = 'chat',
FRIENDS = 'friends',
BYOG = 'byog',
NOTIFICATIONS_INVITES = 'notifications-invites',
NOTIFICATIONS_ACHIEVEMENTS = 'notifications-achievements',
}

View File

@ -1,6 +1,8 @@
export const enum ShortcutAction {
BETTER_XCLOUD_SETTINGS_SHOW = 'bx.settings.show',
CONTROLLER_XBOX_BUTTON_PRESS = 'controller.xbox.press',
STREAM_VIDEO_TOGGLE = 'stream.video.toggle',
STREAM_SCREENSHOT_CAPTURE = 'stream.screenshot.capture',

View File

@ -27,7 +27,7 @@ import { ScreenshotManager } from "./utils/screenshot-manager";
import { NativeMkbHandler } from "./modules/mkb/native-mkb-handler";
import { GuideMenu } from "./modules/ui/guide-menu";
import { updateVideoPlayer } from "./modules/stream/stream-settings-utils";
import { NativeMkbMode, TouchControllerMode, UiSection } from "./enums/pref-values";
import { BlockFeature, NativeMkbMode, TouchControllerMode, UiSection } from "./enums/pref-values";
import { HeaderSection } from "./modules/ui/header";
import { GameTile } from "./modules/ui/game-tile";
import { ProductDetailsPage } from "./modules/ui/product-details";
@ -44,6 +44,7 @@ import { StreamSettings } from "./utils/stream-settings";
import { KeyboardShortcutHandler } from "./modules/mkb/keyboard-shortcut-handler";
import { GhPagesUtils } from "./utils/gh-pages";
import { DeviceVibrationManager } from "./modules/device-vibration-manager";
import { BxEventBus } from "./utils/bx-event-bus";
// Handle login page
if (window.location.pathname.includes('/auth/msa')) {
@ -159,7 +160,7 @@ document.addEventListener('readystatechange', e => {
return;
}
STATES.isSignedIn = !!((window as any).xbcUser?.isSignedIn);
STATES.isSignedIn = !!window.xbcUser?.isSignedIn;
if (STATES.isSignedIn) {
// Preload Remote Play
@ -170,7 +171,7 @@ document.addEventListener('readystatechange', e => {
}
// Hide "Play with Friends" skeleton section
if (getPref<UiSection[]>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS)) {
if (getPref<UiSection[]>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) || getPref<BlockFeature[]>(PrefKey.BLOCK_FEATURES).includes(BlockFeature.FRIENDS)) {
const $parent = document.querySelector('div[class*=PlayWithFriendsSkeleton]')?.closest<HTMLElement>('div[class*=HomePage-module]');
$parent && ($parent.style.display = 'none');
}
@ -190,7 +191,7 @@ window.addEventListener('popstate', onHistoryChanged);
window.history.pushState = patchHistoryMethod('pushState');
window.history.replaceState = patchHistoryMethod('replaceState');
window.addEventListener(BxEvent.XCLOUD_SERVERS_UNAVAILABLE, e => {
BxEventBus.Script.once('xcloud.server.unavailable', () => {
STATES.supportedRegion = false;
window.setTimeout(HeaderSection.watchHeader, 2000);
@ -199,14 +200,14 @@ window.addEventListener(BxEvent.XCLOUD_SERVERS_UNAVAILABLE, e => {
if ($unsupportedPage) {
SettingsDialog.getInstance().show();
}
}, { once: true });
});
window.addEventListener(BxEvent.XCLOUD_SERVERS_READY, e => {
BxEventBus.Script.on('xcloud.server.ready', () => {
STATES.isSignedIn = true;
window.setTimeout(HeaderSection.watchHeader, 2000);
});
window.addEventListener(BxEvent.STREAM_LOADING, e => {
BxEventBus.Stream.on('state.loading', () => {
// Get title ID for screenshot's name
if (window.location.pathname.includes('/launch/') && STATES.currentStream.titleInfo) {
STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title);
@ -216,9 +217,9 @@ window.addEventListener(BxEvent.STREAM_LOADING, e => {
});
// Setup loading screen
getPref(PrefKey.LOADING_SCREEN_GAME_ART) && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup);
getPref(PrefKey.LOADING_SCREEN_GAME_ART) && BxEventBus.Script.on('titleInfo.ready', LoadingScreen.setup);
window.addEventListener(BxEvent.STREAM_STARTING, e => {
BxEventBus.Stream.on('state.starting', () => {
// Hide loading screen
LoadingScreen.hide();
@ -232,7 +233,7 @@ window.addEventListener(BxEvent.STREAM_STARTING, e => {
}
});
window.addEventListener(BxEvent.STREAM_PLAYING, e => {
BxEventBus.Stream.on('state.playing', payload => {
window.BX_STREAM_SETTINGS = StreamSettings.settings;
StreamSettings.refreshAllSettings();
@ -251,7 +252,7 @@ window.addEventListener(BxEvent.STREAM_PLAYING, e => {
KeyboardShortcutHandler.getInstance().start();
// Setup screenshot
const $video = (e as any).$video as HTMLVideoElement;
const $video = payload.$video as HTMLVideoElement;
ScreenshotManager.getInstance().updateCanvasSize($video.videoWidth, $video.videoHeight);
// Setup local co-op
@ -262,8 +263,8 @@ window.addEventListener(BxEvent.STREAM_PLAYING, e => {
updateVideoPlayer();
});
window.addEventListener(BxEvent.STREAM_ERROR_PAGE, e => {
BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
BxEventBus.Stream.on('state.error', () => {
BxEventBus.Stream.emit('state.stopped', {});
});
isFullVersion() && window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, e => {
@ -274,9 +275,9 @@ isFullVersion() && window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, e
});
// Detect game change
window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => {
const dataChannel = (e as any).dataChannel;
if (!dataChannel || dataChannel.label !== 'message') {
BxEventBus.Stream.on('dataChannelCreated', payload => {
const { dataChannel } = payload;
if (dataChannel?.label !== 'message') {
return;
}
@ -285,26 +286,29 @@ window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => {
return;
}
// Get xboxTitleId from message
if (msg.data.includes('/titleinfo')) {
const json = JSON.parse(JSON.parse(msg.data).content);
const xboxTitleId = parseInt(json.titleid, 16);
STATES.currentStream.xboxTitleId = xboxTitleId;
if (!msg.data.includes('/titleinfo')) {
return;
}
// Get titleSlug for Remote Play
if (STATES.remotePlay.isPlaying) {
STATES.currentStream.titleSlug = 'remote-play';
if (json.focused) {
const productTitle = await XboxApi.getProductTitle(xboxTitleId);
if (productTitle) {
STATES.currentStream.titleSlug = productTitleToSlug(productTitle);
}
// Get xboxTitleId from message
const json = JSON.parse(JSON.parse(msg.data).content);
const xboxTitleId = parseInt(json.titleid, 16);
STATES.currentStream.xboxTitleId = xboxTitleId;
// Get titleSlug for Remote Play
if (STATES.remotePlay.isPlaying) {
STATES.currentStream.titleSlug = 'remote-play';
if (json.focused) {
const productTitle = await XboxApi.getProductTitle(xboxTitleId);
if (productTitle) {
STATES.currentStream.titleSlug = productTitleToSlug(productTitle);
}
}
}
});
});
function unload() {
if (!STATES.isPlaying) {
return;
@ -340,9 +344,9 @@ function unload() {
}
}
window.addEventListener(BxEvent.STREAM_STOPPED, unload);
BxEventBus.Stream.on('state.stopped', unload);
window.addEventListener('pagehide', e => {
BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
BxEventBus.Stream.emit('state.stopped', {});
});
isFullVersion() && window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, e => {

View File

@ -1,6 +1,6 @@
import { AppInterface, STATES } from "@utils/global";
import { BxEvent } from "@utils/bx-event";
import { StreamSettings } from "@/utils/stream-settings";
import { BxEventBus } from "@/utils/bx-event-bus";
const VIBRATION_DATA_MAP = {
gamepadIndex: 8,
@ -37,8 +37,8 @@ export class DeviceVibrationManager {
constructor() {
this.boundOnMessage = this.onMessage.bind(this);
window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => {
const dataChannel = (e as any).dataChannel as RTCDataChannel;
BxEventBus.Stream.on('dataChannelCreated', payload => {
const { dataChannel } = payload;
if (dataChannel?.label === 'input') {
this.reset();
@ -47,9 +47,7 @@ export class DeviceVibrationManager {
}
});
window.addEventListener(BxEvent.DEVICE_VIBRATION_CHANGED, e => {
this.setupDataChannel();
});
BxEventBus.Script.on('deviceVibration.updated', () => this.setupDataChannel());
}
private setupDataChannel() {

View File

@ -1,4 +1,4 @@
import { BxEvent } from "@/utils/bx-event";
import { BxEventBus } from "@/utils/bx-event-bus";
export abstract class BaseGameBarAction {
abstract $content: HTMLElement;
@ -7,7 +7,7 @@ export abstract class BaseGameBarAction {
reset() {}
onClick(e: Event) {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
BxEventBus.Stream.emit('gameBar.activated', {});
};
render(): HTMLElement {

View File

@ -13,6 +13,7 @@ import { SpeakerAction } from "./speaker-action";
import { RendererAction } from "./renderer-action";
import { BxLogger } from "@/utils/bx-logger";
import { GameBarPosition, TouchControllerMode } from "@/enums/pref-values";
import { BxEventBus } from "@/utils/bx-event-bus";
export class GameBar {
@ -81,7 +82,7 @@ export class GameBar {
});
// Hide game bar after clicking on an action
window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar);
BxEventBus.Stream.on('gameBar.activated', this.hideBar);
$container.addEventListener('pointerover', this.clearHideTimeout);
$container.addEventListener('pointerout', this.beginHideTimeout);

View File

@ -1,8 +1,8 @@
import { BxEvent } from "@utils/bx-event";
import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle, CE } from "@utils/html";
import { BaseGameBarAction } from "./base-action";
import { MicrophoneShortcut, MicrophoneState } from "../shortcuts/microphone-shortcut";
import { BxEventBus } from "@/utils/bx-event-bus";
export class MicrophoneAction extends BaseGameBarAction {
@ -26,9 +26,8 @@ export class MicrophoneAction extends BaseGameBarAction {
this.$content = CE('div', {}, $btnMuted, $btnDefault);
window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, e => {
const microphoneState = (e as any).microphoneState;
const enabled = microphoneState === MicrophoneState.ENABLED;
BxEventBus.Stream.on('microphone.state.changed', payload => {
const enabled = payload.state === MicrophoneState.ENABLED;
this.$content.dataset.activated = enabled.toString();
// Show the button in Game Bar if the mic is enabled

View File

@ -2,7 +2,7 @@ import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle, CE } from "@utils/html";
import { BaseGameBarAction } from "./base-action";
import { RendererShortcut } from "../shortcuts/renderer-shortcut";
import { BxEvent } from "@/utils/bx-event";
import { BxEventBus } from "@/utils/bx-event-bus";
export class RendererAction extends BaseGameBarAction {
@ -26,9 +26,8 @@ export class RendererAction extends BaseGameBarAction {
this.$content = CE('div', {}, $btnDefault, $btnActivated);
window.addEventListener(BxEvent.VIDEO_VISIBILITY_CHANGED, e => {
const isShowing = (e as any).isShowing;
this.$content.dataset.activated = (!isShowing).toString();
BxEventBus.Stream.on('video.visibility.changed', payload => {
this.$content.dataset.activated = (!payload.isVisible).toString();
});
}

View File

@ -1,8 +1,8 @@
import { BxEvent } from "@utils/bx-event";
import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle, CE } from "@utils/html";
import { BaseGameBarAction } from "./base-action";
import { SoundShortcut, SpeakerState } from "../shortcuts/sound-shortcut";
import { BxEventBus } from "@/utils/bx-event-bus";
export class SpeakerAction extends BaseGameBarAction {
@ -26,10 +26,8 @@ export class SpeakerAction extends BaseGameBarAction {
this.$content = CE('div', {}, $btnEnable, $btnMuted);
window.addEventListener(BxEvent.SPEAKER_STATE_CHANGED, e => {
const speakerState = (e as any).speakerState;
const enabled = speakerState === SpeakerState.ENABLED;
BxEventBus.Stream.on('speaker.state.changed', payload => {
const enabled = payload.state === SpeakerState.ENABLED;
this.$content.dataset.activated = (!enabled).toString();
});
}

View File

@ -18,6 +18,7 @@ import { MkbPopup } from "./mkb-popup";
import type { MkbConvertedPresetData } from "@/types/presets";
import { StreamSettings } from "@/utils/stream-settings";
import { ShortcutAction } from "@/enums/shortcut-actions";
import { BxEventBus } from "@/utils/bx-event-bus";
const PointerToMouseButton = {
1: 0,
@ -439,7 +440,9 @@ export class EmulatedMkbHandler extends MkbHandler {
}
if (this.enabled) {
document.body.requestPointerLock();
document.body.requestPointerLock({
unadjustedMovement: true,
});
} else {
document.pointerLockElement && document.exitPointerLock();
}
@ -517,7 +520,7 @@ export class EmulatedMkbHandler extends MkbHandler {
window.addEventListener('keyup', this.onKeyboardEvent);
window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.onDialogShown);
BxEventBus.Script.on('dialog.shown', this.onDialogShown);
if (AppInterface) {
// Android app doesn't support PointerLock API so we need to use a different method
@ -568,7 +571,7 @@ export class EmulatedMkbHandler extends MkbHandler {
}
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.onDialogShown);
BxEventBus.Script.off('dialog.shown', this.onDialogShown);
this.mouseDataProvider?.destroy();
@ -639,7 +642,7 @@ export class EmulatedMkbHandler extends MkbHandler {
static setupEvents() {
if (isFullVersion()) {
window.addEventListener(BxEvent.STREAM_PLAYING, () => {
BxEventBus.Stream.on('state.playing', () => {
if (STATES.currentStream.titleInfo?.details.hasMkbSupport) {
// Enable native MKB in Android app
NativeMkbHandler.getInstance()?.init();
@ -649,7 +652,7 @@ export class EmulatedMkbHandler extends MkbHandler {
});
if (EmulatedMkbHandler.isAllowed()) {
window.addEventListener(BxEvent.MKB_UPDATED, () => {
BxEventBus.Script.on('mkb.setting.updated', () => {
EmulatedMkbHandler.getInstance()?.refreshPresetData();
});
}

View File

@ -1,12 +1,12 @@
import { CE, createButton, ButtonStyle, type BxButtonOptions } from "@/utils/html";
import { t } from "@/utils/translation";
import { BxEvent } from "@/utils/bx-event";
import { ShortcutAction } from "@/enums/shortcut-actions";
import { SettingsDialog } from "../ui/dialog/settings-dialog";
import type { MkbHandler } from "./base-mkb-handler";
import { NativeMkbHandler } from "./native-mkb-handler";
import { StreamSettings } from "@/utils/stream-settings";
import { KeyHelper } from "./key-helper";
import { BxEventBus } from "@/utils/bx-event-bus";
type MkbPopupType = 'virtual' | 'native';
@ -24,7 +24,7 @@ export class MkbPopup {
constructor() {
this.render();
window.addEventListener(BxEvent.KEYBOARD_SHORTCUTS_UPDATED, e => {
BxEventBus.Script.on('keyboardShortcuts.updated', () => {
const $newButton = this.createActivateButton();
this.$btnActivate.replaceWith($newButton);
this.$btnActivate = $newButton;

View File

@ -12,6 +12,7 @@ import { KeyHelper } from "./key-helper";
import { StreamSettings } from "@/utils/stream-settings";
import { ShortcutAction } from "@/enums/shortcut-actions";
import { NativeMkbMode } from "@/enums/pref-values";
import { BxEventBus } from "@/utils/bx-event-bus";
type NativeMouseData = {
X: number,
@ -100,10 +101,6 @@ export class NativeMkbHandler extends MkbHandler {
this.onKeyboardEvent(event as KeyboardEvent);
break;
case BxEvent.XCLOUD_DIALOG_SHOWN:
this.onDialogShown();
break;
case BxEvent.POINTER_LOCK_REQUESTED:
this.onPointerLockRequested(event);
break;
@ -135,10 +132,10 @@ export class NativeMkbHandler extends MkbHandler {
window.addEventListener('keyup', this);
window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this);
window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this);
window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this);
window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this);
BxEventBus.Script.on('dialog.shown', this.onDialogShown);
const shortcutKey = StreamSettings.findKeyboardShortcut(ShortcutAction.MKB_TOGGLE);
if (shortcutKey) {
@ -195,14 +192,17 @@ export class NativeMkbHandler extends MkbHandler {
destroy(): void {
this.pointerClient?.stop();
this.stop();
window.removeEventListener('keyup', this);
window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this);
window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this);
window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this);
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this);
BxEventBus.Script.off('dialog.shown', this.onDialogShown);
this.waitForMouseData(false);
document.pointerLockElement && document.exitPointerLock();
}
handleMouseMove(data: MkbMouseMove): void {

View File

@ -1,22 +1,20 @@
import { AppInterface, SCRIPT_VERSION, STATES } from "@utils/global";
import { BX_FLAGS } from "@utils/bx-flags";
import { BxLogger } from "@utils/bx-logger";
import { hashCode, renderString } from "@utils/utils";
import { blockSomeNotifications, hashCode, renderString } from "@utils/utils";
import { BxEvent } from "@/utils/bx-event";
import codeControllerShortcuts from "./patches/controller-shortcuts.js" with { type: "text" };
import codeExposeStreamSession from "./patches/expose-stream-session.js" with { type: "text" };
import codeLocalCoOpEnable from "./patches/local-co-op-enable.js" with { type: "text" };
import codeSetCurrentlyFocusedInteractable from "./patches/set-currently-focused-interactable.js" with { type: "text" };
import codeRemotePlayEnable from "./patches/remote-play-enable.js" with { type: "text" };
import codeRemotePlayKeepAlive from "./patches/remote-play-keep-alive.js" with { type: "text" };
import codeVibrationAdjust from "./patches/vibration-adjust.js" with { type: "text" };
import { FeatureGates } from "@/utils/feature-gates.js";
import { PrefKey, StorageKey } from "@/enums/pref-keys.js";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { GamePassCloudGallery } from "@/enums/game-pass-gallery";
import { t } from "@/utils/translation";
import { NativeMkbMode, TouchControllerMode, UiLayout, UiSection } from "@/enums/pref-values";
import { BlockFeature, NativeMkbMode, TouchControllerMode, UiLayout, UiSection } from "@/enums/pref-values";
import { PatcherUtils } from "./patcher-utils.js";
export type PatchName = keyof typeof PATCHES;
@ -245,25 +243,6 @@ logFunc(logTag, '//', logMessage);
return str;
},
// Override website's settings
overrideSettings(str: string) {
const index = str.indexOf(',EnableStreamGate:');
if (index < 0) {
return false;
}
// Find the next "},"
const endIndex = str.indexOf('},', index);
let newSettings = JSON.stringify(FeatureGates);
newSettings = newSettings.substring(1, newSettings.length - 1);
const newCode = newSettings;
str = str.substring(0, endIndex) + ',' + newCode + str.substring(endIndex);
return str;
},
disableGamepadDisconnectedScreen(str: string) {
const index = str.indexOf('"GamepadDisconnected_Title",');
if (index < 0) {
@ -715,6 +694,18 @@ true` + text;
return str;
},
// Don't render News section
ignoreNewsSection(str: string) {
let index = str.indexOf('Logger("CarouselRow")');
index > -1 && (index = PatcherUtils.lastIndexOf(str, 'const ', index, 200));
if (index < 0) {
return false;
}
str = PatcherUtils.insertAt(str, index, 'return null;');
return str;
},
// Don't render "Play With Friends" sections
ignorePlayWithFriendsSection(str: string) {
let index = str.indexOf('location:"PlayWithFriendsRow",');
@ -844,15 +835,15 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
return str;
},
// 24225.js#4127, 24.17.11
patchSetCurrentlyFocusedInteractable(str: string) {
let index = str.indexOf('.setCurrentlyFocusedInteractable=(');
// 49851.js#4083, 27.0.4
patchSetCurrentFocus(str: string) {
let index = str.indexOf('.setCurrentFocus=(');
if (index < 0) {
return false;
}
index = str.indexOf('{', index) + 1;
str = str.substring(0, index) + codeSetCurrentlyFocusedInteractable + str.substring(index);
str = PatcherUtils.insertAt(str, index, 'e && BxEvent.dispatch(window, BxEvent.NAVIGATION_FOCUS_CHANGED, { element: e });');
return str;
},
@ -947,12 +938,59 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
streamPageBeforeLoad(str: string) {
return PatcherUtils.patchBeforePageLoad(str, 'stream');
},
disableAbsoluteMouse(str: string) {
let text = 'sendAbsoluteMouseCapableMessage(e){';
if (!str.includes(text)) {
return false;
}
str = str.replace(text, text + 'return;');
return str;
},
changeNotificationsSubscription(str: string) {
let text = ';buildSubscriptionQueryParamsForNotifications(';
let index = str.indexOf(text);
if (index < 0) {
return false;
}
index += text.length;
// Get parameter name
const subsVar = str[index];
// Find index after {
index = str.indexOf('{', index) + 1;
const blockFeatures = getPref<BlockFeature[]>(PrefKey.BLOCK_FEATURES);
const filters = [];
if (blockFeatures.includes(BlockFeature.NOTIFICATIONS_INVITES)) {
filters.push('GameInvite', 'PartyInvite');
}
if (blockFeatures.includes(BlockFeature.FRIENDS)) {
filters.push('Follower');
}
if (blockFeatures.includes(BlockFeature.NOTIFICATIONS_ACHIEVEMENTS)) {
filters.push('AchievementUnlock');
}
const newCode = `
let subs = ${subsVar};
subs = subs.filter(val => !${JSON.stringify(filters)}.includes(val));
${subsVar} = subs;
`;
str = PatcherUtils.insertAt(str, index, newCode);
return str;
},
};
let PATCH_ORDERS = PatcherUtils.filterPatches([
...(getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
...(AppInterface && getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
'enableNativeMkb',
'exposeInputSink',
'disableAbsoluteMouse',
] : []),
'modifyPreloadedState',
@ -963,7 +1001,6 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([
'patchRequestInfoCrash',
'disableStreamGate',
'overrideSettings',
'broadcastPollingMode',
'patchGamepadPolling',
@ -980,7 +1017,7 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([
'supportLocalCoOp',
'overrideStorageGetSettings',
getPref(PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME) && 'patchSetCurrentlyFocusedInteractable',
getPref(PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME) && 'patchSetCurrentFocus',
getPref<UiLayout>(PrefKey.UI_LAYOUT) !== UiLayout.DEFAULT && 'websiteLayout',
getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole',
@ -1012,13 +1049,19 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([
'enableConsoleLogging',
'enableXcloudLogger',
] : []),
...(blockSomeNotifications() ? [
'changeNotificationsSubscription',
] : []),
]);
const hideSections = getPref<UiSection[]>(PrefKey.UI_HIDE_SECTIONS);
let HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection',
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection',
STATES.browser.capabilities.touch && getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection',
(getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections',
hideSections.includes(UiSection.NEWS) && 'ignoreNewsSection',
hideSections.includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection',
hideSections.includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection',
STATES.browser.capabilities.touch && hideSections.includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection',
hideSections.some(value => [UiSection.NATIVE_MKB, UiSection.MOST_POPULAR].includes(value)) && 'ignoreSiglSections',
]);
// Only when playing
@ -1062,7 +1105,7 @@ let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
] : []),
// Native MKB
...(getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
...(AppInterface && getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
'patchMouseAndKeyboardEnabled',
'disableNativeRequestPointerLock',
] : []),

View File

@ -3,11 +3,7 @@ window.BX_EXPOSED.streamSession = this;
const orgSetMicrophoneState = this.setMicrophoneState.bind(this);
this.setMicrophoneState = state => {
orgSetMicrophoneState(state);
const evt = new Event(BxEvent.MICROPHONE_STATE_CHANGED);
evt.microphoneState = state;
window.dispatchEvent(evt);
window.BxEventBus.Stream.emit('microphone.state.changed', { state });
};
window.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY));

View File

@ -1 +0,0 @@
e && BxEvent.dispatch(window, BxEvent.NAVIGATION_FOCUS_CHANGED, { element: e });

View File

@ -1,21 +1,21 @@
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { limitVideoPlayerFps } from "../stream/stream-settings-utils";
import { BxEvent } from "@/utils/bx-event";
import { BxEventBus } from "@/utils/bx-event-bus";
export class RendererShortcut {
static toggleVisibility() {
const $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]');
if (!$mediaContainer) {
BxEvent.dispatch(window, BxEvent.VIDEO_VISIBILITY_CHANGED, { isShowing: true });
BxEventBus.Stream.emit('video.visibility.changed', { isVisible: true });
return;
}
$mediaContainer.classList.toggle('bx-gone');
const isShowing = !$mediaContainer.classList.contains('bx-gone');
const isVisible = !$mediaContainer.classList.contains('bx-gone');
// Switch FPS
limitVideoPlayerFps(isShowing ? getPref(PrefKey.VIDEO_MAX_FPS) : 0);
BxEvent.dispatch(window, BxEvent.VIDEO_VISIBILITY_CHANGED, { isShowing });
limitVideoPlayerFps(isVisible ? getPref(PrefKey.VIDEO_MAX_FPS) : 0);
BxEventBus.Stream.emit('video.visibility.changed', { isVisible });
}
}

View File

@ -16,6 +16,17 @@ export const SHORTCUT_ACTIONS: ShortcutActions = {
[ShortcutAction.BETTER_XCLOUD_SETTINGS_SHOW]: [t('settings'), t('show')],
},
// MKB
...(STATES.browser.capabilities.mkb ? {
[t('mouse-and-keyboard')]: {
[ShortcutAction.MKB_TOGGLE]: [t('toggle')],
},
} : {}),
[t('controller')]: {
[ShortcutAction.CONTROLLER_XBOX_BUTTON_PRESS]: [t('button-xbox'), t('press')],
},
// Device
...(!!AppInterface ? {
[t('device')]: {
@ -45,13 +56,6 @@ export const SHORTCUT_ACTIONS: ShortcutActions = {
[ShortcutAction.STREAM_MICROPHONE_TOGGLE]: [t('microphone'), t('toggle')],
},
// MKB
...(STATES.browser.capabilities.mkb ? {
[t('mouse-and-keyboard')]: {
[ShortcutAction.MKB_TOGGLE]: [t('toggle')],
},
} : {}),
// Other
[t('other')]: {
[ShortcutAction.TRUE_ACHIEVEMENTS_OPEN]: [t('true-achievements'), t('show')],

View File

@ -4,7 +4,7 @@ import { Toast } from "@utils/toast";
import { ceilToNearest, floorToNearest } from "@/utils/utils";
import { PrefKey } from "@/enums/pref-keys";
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
import { BxEvent } from "@/utils/bx-event";
import { BxEventBus } from "@/utils/bx-event-bus";
export enum SpeakerState {
ENABLED,
@ -71,8 +71,8 @@ export class SoundShortcut {
SoundShortcut.setGainNodeVolume(targetValue);
Toast.show(`${t('stream')} ${t('volume')}`, status, { instant: true });
BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, {
speakerState: targetValue === 0 ? SpeakerState.MUTED : SpeakerState.ENABLED,
BxEventBus.Stream.emit('speaker.state.changed', {
state: targetValue === 0 ? SpeakerState.MUTED : SpeakerState.ENABLED,
});
return;
}
@ -84,9 +84,9 @@ export class SoundShortcut {
const status = $media.muted ? t('muted') : t('unmuted');
Toast.show(`${t('stream')} ${t('volume')}`, status, { instant: true });
BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, {
speakerState: $media.muted ? SpeakerState.MUTED : SpeakerState.ENABLED,
})
BxEventBus.Stream.emit('speaker.state.changed', {
state: $media.muted ? SpeakerState.MUTED : SpeakerState.ENABLED,
});
}
}
}

View File

@ -0,0 +1,21 @@
import { generateVirtualControllerMapping } from "@/utils/gamepad";
export class VirtualControllerShortcut {
static pressXboxButton(): void {
const streamSession = window.BX_EXPOSED.streamSession;
if (!streamSession) {
return;
}
const released = generateVirtualControllerMapping();
const pressed = generateVirtualControllerMapping({
Nexus: 1,
VirtualPhysicality: 1024, // Home
});
streamSession.onVirtualGamepadInput('systemMenu', performance.now(), [pressed]);
setTimeout(() => {
streamSession.onVirtualGamepadInput('systemMenu', performance.now(), [released]);
}, 100);
}
}

View File

@ -253,8 +253,9 @@ export class StreamBadges {
const allAudioCodecs: Record<string, RTCBasicStat> = {};
let audioCodecId;
const allCandidates: Record<string, string> = {};
let candidateId;
const allCandidatePairs: Record<string, string> = {};
const allRemoteCandidates: Record<string, string> = {};
let candidatePairId;
stats.forEach((stat: RTCBasicStat) => {
if (stat.type === 'codec') {
@ -275,10 +276,12 @@ export class StreamBadges {
} else if (stat.kind === 'audio') {
audioCodecId = stat.codecId;
}
} else if (stat.type === 'candidate-pair' && stat.packetsReceived > 0 && stat.state === 'succeeded') {
candidateId = stat.remoteCandidateId;
} else if (stat.type === 'transport' && (stat as unknown as RTCTransportStats).selectedCandidatePairId) {
candidatePairId = (stat as unknown as RTCTransportStats).selectedCandidatePairId;
} else if (stat.type === 'candidate-pair') {
allCandidatePairs[stat.id] = (stat as unknown as RTCIceCandidatePairStats).remoteCandidateId;
} else if (stat.type === 'remote-candidate') {
allCandidates[stat.id] = stat.address;
allRemoteCandidates[stat.id] = stat.address;
}
});
@ -336,12 +339,12 @@ export class StreamBadges {
}
// Get server type
if (candidateId) {
BxLogger.info('candidate', candidateId, allCandidates);
if (candidatePairId) {
BxLogger.info('candidate', candidatePairId, allCandidatePairs);
// Server + Region
let text = '';
const isIpv6 = allCandidates[candidateId].includes(':');
const isIpv6 = allRemoteCandidates[allCandidatePairs[candidatePairId]].includes(':');
const server = this.serverInfo.server;
if (server && server.region) {

View File

@ -1,4 +1,3 @@
import { BxEvent } from "@utils/bx-event"
import { CE } from "@utils/html"
import { t } from "@utils/translation"
import { STATES } from "@utils/global"
@ -7,6 +6,7 @@ import { getPref } from "@/utils/settings-storages/global-settings-storage"
import { StreamStatsCollector, type StreamStatGrade } from "@/utils/stream-stats-collector"
import { BxLogger } from "@/utils/bx-logger"
import { StreamStat } from "@/enums/pref-values"
import { BxEventBus } from "@/utils/bx-event-bus"
export class StreamStats {
@ -193,12 +193,21 @@ export class StreamStats {
refreshStyles() {
const PREF_ITEMS = getPref<StreamStat[]>(PrefKey.STATS_ITEMS);
const PREF_OPACITY_BG = getPref<number>(PrefKey.STATS_OPACITY_BACKGROUND);
const $container = this.$container;
$container.dataset.stats = '[' + PREF_ITEMS.join('][') + ']';
$container.dataset.position = getPref(PrefKey.STATS_POSITION);
$container.dataset.transparent = getPref(PrefKey.STATS_TRANSPARENT);
$container.style.opacity = getPref(PrefKey.STATS_OPACITY) + '%';
if (PREF_OPACITY_BG === 0) {
$container.style.removeProperty('background-color');
$container.dataset.shadow = 'true';
} else {
delete $container.dataset.shadow;
$container.style.backgroundColor = `rgba(0, 0, 0, ${PREF_OPACITY_BG}%)`;
}
$container.style.opacity = getPref(PrefKey.STATS_OPACITY_ALL) + '%';
$container.style.fontSize = getPref(PrefKey.STATS_TEXT_SIZE);
}
@ -230,7 +239,7 @@ export class StreamStats {
}
static setupEvents() {
window.addEventListener(BxEvent.STREAM_PLAYING, e => {
BxEventBus.Stream.on('state.playing', () => {
const PREF_STATS_QUICK_GLANCE = getPref(PrefKey.STATS_QUICK_GLANCE_ENABLED);
const PREF_STATS_SHOW_WHEN_PLAYING = getPref(PrefKey.STATS_SHOW_WHEN_PLAYING);

View File

@ -1,11 +1,11 @@
import { STATES } from "@utils/global.ts";
import { createSvgIcon } from "@utils/html.ts";
import { BxIcon } from "@utils/bx-icon";
import { BxEvent } from "@utils/bx-event.ts";
import { t } from "@utils/translation.ts";
import { StreamBadges } from "./stream-badges.ts";
import { StreamStats } from "./stream-stats.ts";
import { SettingsDialog } from "../ui/dialog/settings-dialog.ts";
import { BxEventBus } from "@/utils/bx-event-bus.ts";
export class StreamUiHandler {
@ -243,7 +243,7 @@ export class StreamUiHandler {
// Error Page: .PureErrorPage.ErrorScreen
if (className.includes('PureErrorPage')) {
BxEvent.dispatch(window, BxEvent.STREAM_ERROR_PAGE);
BxEventBus.Stream.emit('state.error', {});
return;
}

View File

@ -8,6 +8,7 @@ import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { TouchControllerStyleCustom, TouchControllerStyleStandard } from "@/enums/pref-values";
import { GhPagesUtils } from "@/utils/gh-pages";
import { BxEventBus } from "@/utils/bx-event-bus";
const LOG_TAG = 'TouchController';
@ -268,7 +269,7 @@ export class TouchController {
static setup() {
// Function for testing touch control
(window as any).testTouchLayout = (layout: any) => {
window.testTouchLayout = (layout: any) => {
const { touchLayoutManager } = window.BX_EXPOSED;
touchLayoutManager && touchLayoutManager.changeLayoutForScope({
@ -291,9 +292,9 @@ export class TouchController {
const PREF_STYLE_STANDARD = getPref<TouchControllerStyleStandard>(PrefKey.TOUCH_CONTROLLER_STYLE_STANDARD);
const PREF_STYLE_CUSTOM = getPref<TouchControllerStyleCustom>(PrefKey.TOUCH_CONTROLLER_STYLE_CUSTOM);
window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => {
const dataChannel = (e as any).dataChannel;
if (!dataChannel || dataChannel.label !== 'message') {
BxEventBus.Stream.on('dataChannelCreated', payload => {
const { dataChannel } = payload;
if (dataChannel?.label !== 'message') {
return;
}

View File

@ -2,6 +2,7 @@ import { GamepadKey } from "@/enums/gamepad";
import { PrefKey } from "@/enums/pref-keys";
import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler";
import { BxEvent } from "@/utils/bx-event";
import { BxEventBus } from "@/utils/bx-event-bus";
import { BxLogger } from "@/utils/bx-logger";
import { CE, isElementVisible } from "@/utils/html";
import { setNearby } from "@/utils/navigation-utils";
@ -439,10 +440,10 @@ export class NavigationDialogManager {
show(dialog: NavigationDialog, configs={}, clearStack=false) {
this.clearGamepadHoldingInterval();
BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN);
BxEventBus.Script.emit('dialog.shown', {});
// Stop xCloud's navigation polling
(window as any).BX_EXPOSED.disableGamepadPolling = true;
window.BX_EXPOSED.disableGamepadPolling = true;
// Lock scroll bar
document.body.classList.add('bx-no-scroll');
@ -475,11 +476,14 @@ export class NavigationDialogManager {
hide() {
this.clearGamepadHoldingInterval();
if (!this.isShowing()) {
return;
}
// Unlock scroll bar
document.body.classList.remove('bx-no-scroll');
BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_DISMISSED);
BxEventBus.Script.emit('dialog.dismissed', {});
// Hide content
this.$overlay.classList.add('bx-gone');
@ -504,7 +508,7 @@ export class NavigationDialogManager {
this.unmountCurrentDialog();
// Enable xCloud's navigation polling
(window as any).BX_EXPOSED.disableGamepadPolling = false;
window.BX_EXPOSED.disableGamepadPolling = false;
// Show the last dialog in dialogs stack
if (this.dialogsStack.length) {

View File

@ -16,6 +16,7 @@ export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends N
private $presets!: HTMLSelectElement;
private $header!: HTMLElement;
private $defaultNote!: HTMLElement;
protected $content!: HTMLElement;
private $btnRename!: HTMLButtonElement;
@ -36,15 +37,12 @@ export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends N
const isDefaultPreset = this.currentPresetId <= 0;
this.$btnRename.disabled = isDefaultPreset;
this.$btnDelete.disabled = isDefaultPreset;
this.$defaultNote.classList.toggle('bx-gone', !isDefaultPreset);
}
private async renderPresetsList() {
this.allPresets = await this.presetsDb.getPresets();
if (!this.currentPresetId) {
this.currentPresetId = this.allPresets.default[0];
}
renderPresetsList<T>(this.$presets, this.allPresets, this.currentPresetId);
renderPresetsList<T>(this.$presets, this.allPresets, this.currentPresetId, { selectedIndicator: true });
}
private promptNewName(action: string,value='') {
@ -121,7 +119,7 @@ export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends N
createButton({
icon: BxIcon.NEW,
title: t('new'),
style: ButtonStyle.FOCUSABLE,
style: ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY,
onClick: async (e) => {
const newName = this.promptNewName(t('new'));
if (!newName) {
@ -140,7 +138,7 @@ export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends N
createButton({
icon: BxIcon.COPY,
title: t('copy'),
style: ButtonStyle.FOCUSABLE,
style: ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY,
onClick: async (e) => {
const preset = this.allPresets.data[this.currentPresetId];
@ -168,8 +166,11 @@ export abstract class BaseProfileManagerDialog<T extends PresetRecord> extends N
onClick: e => this.hide(),
}),
),
$header,
CE('div', { class: 'bx-dialog-content bx-hide-scroll-bar' }, this.$content),
CE('div', {},
$header,
this.$defaultNote = CE('div', { class: 'bx-default-preset-note bx-gone' }, t('default-preset-note')),
),
CE('div', { class: 'bx-dialog-content' }, this.$content),
);
}

View File

@ -19,6 +19,7 @@ export class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog<Key
// private readonly LOG_TAG = 'KeyboardShortcutsManagerDialog';
protected $content: HTMLElement;
private $unbindNote: HTMLElement;
private readonly allKeyElements: BxKeyBindingButton[] = [];
protected readonly BLANK_PRESET_DATA: KeyboardShortcutPresetData = {
@ -65,7 +66,10 @@ export class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog<Key
}
}
this.$content = CE('div', {}, $rows);
this.$content = CE('div', {},
this.$unbindNote = CE('i', { class: 'bx-mkb-note' }, t('right-click-to-unbind')),
$rows,
);
}
private onKeyChanged = (e: Event) => {
@ -109,6 +113,9 @@ export class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog<Key
const isDefaultPreset = id <= 0;
this.updateButtonStates();
// Toggle unbind note
this.$unbindNote.classList.toggle('bx-gone', isDefaultPreset);
// Update buttons
for (const $elm of this.allKeyElements) {
const { action } = this.parseDataset($elm);

View File

@ -48,6 +48,7 @@ export class MkbMappingManagerDialog extends BaseProfileManagerDialog<MkbPresetR
private $mouseSensitivityX!: BxNumberStepper;
private $mouseSensitivityY!: BxNumberStepper;
private $mouseDeadzone!: BxNumberStepper;
private $unbindNote!: HTMLElement;
constructor(title: string) {
super(title, MkbMappingPresetsTable.getInstance());
@ -93,7 +94,7 @@ export class MkbMappingManagerDialog extends BaseProfileManagerDialog<MkbPresetR
private render() {
const $rows = CE('div', {},
CE('i', { class: 'bx-mkb-note' }, t('right-click-to-unbind')),
this.$unbindNote = CE('i', { class: 'bx-mkb-note' }, t('right-click-to-unbind')),
);
for (const buttonIndex of this.BUTTONS_ORDER) {
@ -185,6 +186,9 @@ export class MkbMappingManagerDialog extends BaseProfileManagerDialog<MkbPresetR
const isDefaultPreset = id <= 0;
this.updateButtonStates();
// Toggle unbind note
this.$unbindNote.classList.toggle('bx-gone', isDefaultPreset);
// Update buttons
for (const $elm of this.allKeyElements) {
const { buttonIndex, keySlot } = this.parseDataset($elm);

View File

@ -40,6 +40,7 @@ export class RemotePlayDialog extends NavigationDialog {
let $resolutions : HTMLSelectElement | NavigationElement = CE<HTMLSelectElement>('select', {},
CE('option', { value: StreamResolution.DIM_720P }, '720p'),
CE('option', { value: StreamResolution.DIM_1080P }, '1080p'),
// CE('option', { value: StreamResolution.DIM_1080P_HQ }, `1080p (HQ) ${t('experimental')}`),
);
$resolutions = BxSelectElement.create($resolutions as HTMLSelectElement);

View File

@ -30,6 +30,7 @@ import { SuggestionsSetting } from "./settings/suggestions";
import { StreamSettings } from "@/utils/stream-settings";
import { MkbExtraSettings } from "./settings/mkb-extra";
import { BxExposed } from "@/utils/bx-exposed";
import { BxEventBus } from "@/utils/bx-event-bus";
type SettingTabSectionItem = Partial<{
@ -281,12 +282,14 @@ export class SettingsDialog extends NavigationDialog {
PrefKey.UI_HIDE_SYSTEM_MENU_ICON,
PrefKey.UI_DISABLE_FEEDBACK_DIALOG,
PrefKey.UI_REDUCE_ANIMATIONS,
PrefKey.BLOCK_SOCIAL_FEATURES,
PrefKey.BYOG_DISABLED,
{
pref: PrefKey.UI_HIDE_SECTIONS,
multiLines: true,
},
{
pref: PrefKey.BLOCK_FEATURES,
multiLines: true,
},
],
}, {
requiredVariants: 'full',
@ -317,7 +320,7 @@ export class SettingsDialog extends NavigationDialog {
pref: PrefKey.USER_AGENT_PROFILE,
multiLines: true,
onCreated: (setting, $control) => {
const defaultUserAgent = (window.navigator as any).orgUserAgent || window.navigator.userAgent;
const defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent;
const $inpCustomUserAgent = CE<HTMLInputElement>('input', {
type: 'text',
@ -434,16 +437,13 @@ export class SettingsDialog extends NavigationDialog {
},
onCreated: (setting: SettingTabSectionItem, $elm: HTMLElement) => {
const $range = $elm.querySelector<HTMLInputElement>('input[type=range')!;
window.addEventListener(BxEvent.SETTINGS_CHANGED, e => {
const { storageKey, settingKey, settingValue } = e as any;
if (storageKey !== StorageKey.GLOBAL || settingKey !== PrefKey.AUDIO_VOLUME) {
return;
}
$range.value = settingValue;
BxEvent.dispatch($range, 'input', {
ignoreOnChange: true,
});
BxEventBus.Script.on('setting.changed', payload => {
const { storageKey, settingKey, settingValue } = payload;
if (storageKey === StorageKey.GLOBAL && settingKey === PrefKey.AUDIO_VOLUME) {
$range.value = settingValue;
BxEvent.dispatch($range, 'input', { ignoreOnChange: true });
}
});
},
}],
@ -630,10 +630,10 @@ export class SettingsDialog extends NavigationDialog {
pref: PrefKey.STATS_TEXT_SIZE,
onChange: StreamStats.refreshStyles,
}, {
pref: PrefKey.STATS_OPACITY,
pref: PrefKey.STATS_OPACITY_ALL,
onChange: StreamStats.refreshStyles,
}, {
pref: PrefKey.STATS_TRANSPARENT,
pref: PrefKey.STATS_OPACITY_BACKGROUND,
onChange: StreamStats.refreshStyles,
}, {
pref: PrefKey.STATS_CONDITIONAL_FORMATTING,

View File

@ -136,7 +136,7 @@ export class ControllerExtraSettings extends HTMLElement {
// Render shortcut presets
const allShortcutPresets = await ControllerShortcutsTable.getInstance().getPresets();
renderPresetsList(this.$selectShortcuts, allShortcutPresets, null, true);
renderPresetsList(this.$selectShortcuts, allShortcutPresets, null, { addOffValue: true });
for (const name of this.controllerIds) {
const $option = CE<HTMLOptionElement>('option', { value: name }, name);

View File

@ -108,11 +108,11 @@ export class MkbExtraSettings extends HTMLElement {
private static async updateLayout(this: MkbExtraSettings) {
// Render shortcut presets
const mappingPresets = await MkbMappingPresetsTable.getInstance().getPresets();
renderPresetsList(this.$mappingPresets, mappingPresets, getPref<MkbPresetId>(PrefKey.MKB_P1_MAPPING_PRESET_ID), false);
renderPresetsList(this.$mappingPresets, mappingPresets, getPref<MkbPresetId>(PrefKey.MKB_P1_MAPPING_PRESET_ID));
// Render shortcut presets
const shortcutsPresets = await KeyboardShortcutsTable.getInstance().getPresets();
renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getPref<MkbPresetId>(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID), true);
renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getPref<MkbPresetId>(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID), { addOffValue: true });
}
private static async saveMkbSettings(this: MkbExtraSettings) {

View File

@ -1,6 +1,6 @@
import { BxEvent } from "@/utils/bx-event";
import { BxIcon } from "@/utils/bx-icon";
import { CE, createSvgIcon, getReactProps, isElementVisible, secondsToHms } from "@/utils/html";
import { CE, createSvgIcon, getReactProps, isElementVisible, secondsToHm } from "@/utils/html";
import { XcloudApi } from "@/utils/xcloud-api";
interface GameTimeElement extends HTMLElement {
@ -30,8 +30,14 @@ export class GameTile {
if (typeof totalWaitTime === 'number' && isElementVisible($elm)) {
const $div = CE('div', { class: 'bx-game-tile-wait-time' },
createSvgIcon(BxIcon.PLAYTIME),
CE('span', {}, secondsToHms(totalWaitTime)),
CE('span', {}, totalWaitTime < 60 ? totalWaitTime + 's' : secondsToHm(totalWaitTime)),
);
const duration = (totalWaitTime >= 15 * 60) ? 'long' : (totalWaitTime >= 10 * 60) ? 'medium' : (totalWaitTime >= 5 * 60 ) ? 'short' : '';
if (duration) {
$div.dataset.duration = duration;
}
$elm.insertAdjacentElement('afterbegin', $div);
}
}

View File

@ -7,6 +7,10 @@ import { t } from "@/utils/translation";
import { SettingsDialog } from "./dialog/settings-dialog";
import { TrueAchievements } from "@/utils/true-achievements";
import { BxIcon } from "@/utils/bx-icon";
import { BxEventBus } from "@/utils/bx-event-bus";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { UiLayout } from "@/enums/pref-values";
import { PrefKey } from "@/enums/pref-keys";
export enum GuideMenuTab {
HOME = 'home',
@ -40,9 +44,9 @@ export class GuideMenu {
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY,
onClick: () => {
// Wait until the Guide dialog is closed
window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, e => {
BxEventBus.Script.once('dialog.dismissed', () => {
setTimeout(() => SettingsDialog.getInstance().show(), 50);
}, { once: true });
});
// Close all xCloud's dialogs
this.closeGuideMenu();
@ -110,6 +114,11 @@ export class GuideMenu {
class: 'bx-guide-home-buttons',
});
// Set TV tag
if (STATES.userAgent.isTv || getPref<UiLayout>(PrefKey.UI_LAYOUT) === UiLayout.TV) {
document.body.dataset.bxMediaType = 'tv';
}
for (const $button of buttonsLayout) {
if (!$button) {
continue;

View File

@ -24,4 +24,4 @@ export function localRedirect(path: string) {
$anchor.click();
}
(window as any).localRedirect = localRedirect;
window.localRedirect = localRedirect;

25
src/types/global.d.ts vendored
View File

@ -2,6 +2,8 @@ import type { BxExposed } from "@/utils/bx-exposed";
import type { AllPresets, ControllerShortcutPresetRecord } from "./presets";
import type { PrefKey } from "@/enums/pref-keys";
import type { StreamSettings, type StreamSettingsData } from "@/utils/stream-settings";
import type { BxEvent } from "@/utils/bx-event";
import type { BxLogger } from "@/utils/bx-logger";
export {};
@ -24,5 +26,28 @@ declare global {
BX_REMOTE_PLAY_CONFIG: BxStates.remotePlay.config;
BX_STREAM_SETTINGS: StreamSettingsData;
BX_FETCH: typeof window['fetch'];
BxEvent: typeof BxEvent;
BxEventBus: typeof BxEventBus;
BxLogger: typeof BxLogger;
localRedirect: (path: stringn) => void;
testTouchLayout: (layout: any) => void;
chrome?: any;
// xCloud properties
xbcUser?: {
isSignedIn: boolean;
};
MSA: any;
MeControl: any;
adobe: any;
}
interface Navigator {
orgUserAgent?: string;
orgUserAgentData?: any;
}
}

View File

@ -162,3 +162,5 @@ type XboxAchievement = {
name: string;
}
};
type OsName = 'windows' | 'tizen' | 'android';

139
src/utils/bx-event-bus.ts Normal file
View File

@ -0,0 +1,139 @@
import type { PrefKey, StorageKey } from "@/enums/pref-keys";
import { BX_FLAGS } from "./bx-flags";
import { BxLogger } from "./bx-logger";
import { AppInterface } from "./global";
import type { MicrophoneState } from "@/modules/shortcuts/microphone-shortcut";
import type { SpeakerState } from "@/modules/shortcuts/sound-shortcut";
type EventCallback<T = any> = (payload: T) => void;
type ScriptEvents = {
'xcloud.server.ready': {};
'xcloud.server.unavailable': {};
'dialog.shown': {},
'dialog.dismissed': {},
'titleInfo.ready': {};
'setting.changed': {
storageKey: StorageKey;
settingKey: PrefKey;
settingValue: any;
};
'mkb.setting.updated': {};
'keyboardShortcuts.updated': {};
'deviceVibration.updated': {};
// GH pages
'list.forcedNativeMkb.updated': {
data: {
data: any;
};
};
};
type StreamEvents = {
'state.loading': {};
'state.starting': {};
'state.playing': { $video?: HTMLVideoElement };
'state.stopped': {};
'state.error': {};
'gameBar.activated': {},
'speaker.state.changed': { state: SpeakerState },
'video.visibility.changed': { isVisible: boolean },
// Inside patch
'microphone.state.changed': { state: MicrophoneState },
dataChannelCreated: { dataChannel: RTCDataChannel };
};
export class BxEventBus<TEvents extends Record<string, any>> {
private listeners: Map<keyof TEvents, Set<EventCallback<any>>> = new Map();
private group: string;
private appJsInterfaces: { [key in keyof TEvents]?: string };
static readonly Script = new BxEventBus<ScriptEvents>('script', {
'dialog.shown': 'onDialogShown',
'dialog.dismissed': 'onDialogDismissed',
});
static readonly Stream = new BxEventBus<StreamEvents>('stream', {
'state.loading': 'onStreamPlaying',
'state.playing': 'onStreamPlaying',
'state.stopped': 'onStreamStopped',
});
constructor(group: string, appJsInterfaces: { [key in keyof TEvents]?: string }) {
this.group = group;
this.appJsInterfaces = appJsInterfaces;
}
on<K extends keyof TEvents>(event: K, callback: EventCallback<TEvents[K]>): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
BX_FLAGS.Debug && BxLogger.warning('EventBus', 'on', event, callback);
}
once<K extends keyof TEvents>(event: K, callback: EventCallback<TEvents[K]>): void {
const wrapper = (...args: any[]) => {
// @ts-ignore
callback(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
off<K extends keyof TEvents>(event: K, callback: EventCallback<TEvents[K]> | null): void {
BX_FLAGS.Debug && BxLogger.warning('EventBus', 'off', event, callback);
if (!callback) {
// Remove all listener callbacks
this.listeners.delete(event);
return;
}
const callbacks = this.listeners.get(event);
if (!callbacks) {
return;
}
callbacks.delete(callback);
if (callbacks.size === 0) {
this.listeners.delete(event);
}
}
offAll(): void {
this.listeners.clear();
}
emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void {
const callbacks = this.listeners.get(event) || [];
for (const callback of callbacks) {
callback(payload);
}
// Call method inside Android app
if (AppInterface) {
try {
if (event in this.appJsInterfaces) {
const method = this.appJsInterfaces[event];
AppInterface[method] && AppInterface[method]();
} else {
AppInterface.onEventBus(this.group + '.' + (event as string));
}
} catch (e) {
console.log(e);
}
}
BX_FLAGS.Debug && BxLogger.warning('EventBus', 'emit', event, payload);
}
}
window.BxEventBus = BxEventBus;

View File

@ -4,59 +4,28 @@ import { BX_FLAGS } from "./bx-flags";
export namespace BxEvent {
export const JUMP_BACK_IN_READY = 'bx-jump-back-in-ready';
export const POPSTATE = 'bx-popstate';
export const TITLE_INFO_READY = 'bx-title-info-ready';
export const SETTINGS_CHANGED = 'bx-settings-changed';
export const STREAM_LOADING = 'bx-stream-loading';
export const STREAM_STARTING = 'bx-stream-starting';
export const STREAM_STARTED = 'bx-stream-started';
export const STREAM_PLAYING = 'bx-stream-playing';
export const STREAM_STOPPED = 'bx-stream-stopped';
export const STREAM_ERROR_PAGE = 'bx-stream-error-page';
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';
// Inside patch
export const STREAM_SESSION_READY = 'bx-stream-session-ready';
export const CUSTOM_TOUCH_LAYOUTS_LOADED = 'bx-custom-touch-layouts-loaded';
export const TOUCH_LAYOUT_MANAGER_READY = 'bx-touch-layout-manager-ready';
// Inside app
export const REMOTE_PLAY_READY = 'bx-remote-play-ready';
export const REMOTE_PLAY_FAILED = 'bx-remote-play-failed';
export const XCLOUD_SERVERS_READY = 'bx-servers-ready';
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';
// Inside patch
export const CAPTURE_SCREENSHOT = 'bx-capture-screenshot';
export const POINTER_LOCK_REQUESTED = 'bx-pointer-lock-requested';
export const POINTER_LOCK_EXITED = 'bx-pointer-lock-exited';
// Inside patch
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';
export const XCLOUD_GUIDE_MENU_SHOWN = 'bx-xcloud-guide-menu-shown';
export const XCLOUD_POLLING_MODE_CHANGED = 'bx-xcloud-polling-mode-changed';
@ -76,7 +45,6 @@ export namespace BxEvent {
}
const event = new Event(eventName);
if (data) {
for (const key in data) {
(event as any)[key] = data[key];
@ -86,8 +54,8 @@ export namespace BxEvent {
target.dispatchEvent(event);
AppInterface && AppInterface.onEvent(eventName);
BX_FLAGS.Debug && BxLogger.warning('BxEvent', 'dispatch', eventName, data)
BX_FLAGS.Debug && BxLogger.warning('BxEvent', 'dispatch', target, eventName, data);
}
}
(window as any).BxEvent = BxEvent;
window.BxEvent = BxEvent;

View File

@ -1,7 +1,6 @@
import { isFullVersion } from "@macros/build" with { type: "macro" };
import { ControllerShortcut } from "@/modules/controller-shortcut";
import { BxEvent } from "@utils/bx-event";
import { deepClone, STATES } from "@utils/global";
import { BxLogger } from "./bx-logger";
import { BX_FLAGS } from "./bx-flags";
@ -12,6 +11,8 @@ import { GamePassCloudGallery } from "@/enums/game-pass-gallery";
import { TouchController } from "@/modules/touch-controller";
import { NativeMkbMode, TouchControllerMode } from "@/enums/pref-values";
import { Patcher, type PatchPage } from "@/modules/patcher/patcher";
import { BxEventBus } from "./bx-event-bus";
import { FeatureGates } from "./feature-gates";
export enum SupportedInputType {
CONTROLLER = 'Controller',
@ -36,6 +37,15 @@ export const BxExposed = {
BxLogger.error(LOG_TAG, e);
}
// Override feature gates
try {
for (const exp in FeatureGates) {
state.experiments.overrideFeatureGates[exp.toLocaleLowerCase()] = FeatureGates[exp];
}
} catch (e) {
BxLogger.error(LOG_TAG, e);
}
// Add list of games with custom layouts to the official list
try {
const sigls = state.xcloud.sigls;
@ -139,7 +149,7 @@ export const BxExposed = {
// Save this info in STATES
STATES.currentStream.titleInfo = titleInfo;
BxEvent.dispatch(window, BxEvent.TITLE_INFO_READY);
BxEventBus.Script.emit('titleInfo.ready', {});
return titleInfo;
},

View File

@ -7,13 +7,13 @@ const enum TextColor {
}
export class BxLogger {
static info = (tag: string, ...args: any[]) => BxLogger.log(TextColor.INFO, tag, ...args);
static warning = (tag: string, ...args: any[]) => BxLogger.log(TextColor.WARNING, tag, ...args);
static info = (tag: string, ...args: any[]) => BX_FLAGS.Debug && BxLogger.log(TextColor.INFO, tag, ...args);
static warning = (tag: string, ...args: any[]) => BX_FLAGS.Debug && BxLogger.log(TextColor.WARNING, tag, ...args);
static error = (tag: string, ...args: any[]) => BxLogger.log(TextColor.ERROR, tag, ...args);
private static log(color: string, tag: string, ...args: any) {
BX_FLAGS.Debug && console.log(`%c[BxC]`, `color:${color};font-weight:bold;`, tag, '//', ...args);
console.log(`%c[BxC]`, `color:${color};font-weight:bold;`, tag, '//', ...args);
}
}
(window as any).BxLogger = BxLogger;
window.BxLogger = BxLogger;

View File

@ -1,6 +1,6 @@
import { CE } from "@utils/html";
import { compressCss, renderStylus } from "@macros/build" with { type: "macro" };
import { UiSection } from "@/enums/pref-values";
import { BlockFeature, UiSection } from "@/enums/pref-values";
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "./settings-storages/global-settings-storage";
@ -18,7 +18,7 @@ export function addCss() {
}
// Hide BYOG section
if (getPref(PrefKey.BYOG_DISABLED)) {
if (getPref<BlockFeature[]>(PrefKey.BLOCK_FEATURES).includes(BlockFeature.BYOG)) {
selectorToHide.push('#BodyContent > div[class*=ByogRow-module__container___]');
}
@ -39,7 +39,7 @@ export function addCss() {
}
// Hide "Start a party" button in the Guide menu
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES)) {
if (getPref<BlockFeature[]>(PrefKey.BLOCK_FEATURES).includes(BlockFeature.FRIENDS)) {
selectorToHide.push('#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]');
}

View File

@ -1,13 +1,14 @@
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";
import { BlockFeature, NativeMkbMode } from "@/enums/pref-values";
export let FeatureGates: { [key: string]: boolean } = {
PwaPrompt: false,
EnableWifiWarnings: false,
EnableUpdateRequiredPage: false,
ShowForcedUpdateScreen: false,
EnableTakControlResizing: true, // Experimenting
};
// Enable Native Mouse & Keyboard
@ -17,12 +18,17 @@ if (nativeMkbMode !== NativeMkbMode.DEFAULT) {
}
// Disable chat feature
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES)) {
const blockFeatures = getPref<BlockFeature[]>(PrefKey.BLOCK_FEATURES);
if (blockFeatures.includes(BlockFeature.CHAT)) {
FeatureGates.EnableGuideChatTab = false;
}
if (blockFeatures.includes(BlockFeature.FRIENDS)) {
FeatureGates.EnableFriendsAndFollowers = false;
}
// Disable BYOG feature
if (getPref(PrefKey.BYOG_DISABLED)) {
if (blockFeatures.includes(BlockFeature.BYOG)) {
FeatureGates.EnableBYOG = false;
FeatureGates.EnableBYOGPurchase = false;
}

View File

@ -57,3 +57,36 @@ export function hasGamepad() {
return false;
}
export function generateVirtualControllerMapping(override: {}={}) {
const mapping = {
GamepadIndex: 0,
A: 0,
B: 0,
X: 0,
Y: 0,
LeftShoulder: 0,
RightShoulder: 0,
LeftTrigger: 0,
RightTrigger: 0,
View: 0,
Menu: 0,
LeftThumb: 0,
RightThumb: 0,
DPadUp: 0,
DPadDown: 0,
DPadLeft: 0,
DPadRight: 0,
Nexus: 0,
LeftThumbXAxis: 0,
LeftThumbYAxis: 0,
RightThumbXAxis: 0,
RightThumbYAxis: 0,
PhysicalPhysicality: 0,
VirtualPhysicality: 0,
Dirty: false,
Virtual: false,
};
return Object.assign({}, mapping, override);
}

View File

@ -1,7 +1,7 @@
import { StorageKey } from "@/enums/pref-keys";
import { NATIVE_FETCH } from "./bx-flags";
import { BxLogger } from "./bx-logger";
import { BxEvent } from "./bx-event";
import { BxEventBus } from "./bx-event-bus";
export type ForceNativeMkbResponse = {
@ -53,7 +53,9 @@ export class GhPagesUtils {
if (json.$schemaVersion === supportedSchema) {
// Save to storage
window.localStorage.setItem(key, JSON.stringify(json));
BxEvent.dispatch(window, BxEvent.GH_PAGES_FORCE_NATIVE_MKB_UPDATED);
BxEventBus.Script.emit('list.forcedNativeMkb.updated', {
data: json,
});
}
});

View File

@ -2,6 +2,7 @@ import { BxEvent } from "@utils/bx-event";
import { LoadingScreen } from "@modules/loading-screen";
import { RemotePlayManager } from "@/modules/remote-play-manager";
import { HeaderSection } from "@/modules/ui/header";
import { BxEventBus } from "./bx-event-bus";
import { NavigationDialogManager } from "@/modules/ui/dialog/navigation-dialog";
export function patchHistoryMethod(type: 'pushState' | 'replaceState') {
@ -9,8 +10,8 @@ export function patchHistoryMethod(type: 'pushState' | 'replaceState') {
return function(...args: any[]) {
BxEvent.dispatch(window, BxEvent.POPSTATE, {
arguments: args,
});
arguments: args,
});
// @ts-ignore
return orig.apply(this, arguments);
@ -26,17 +27,11 @@ export function onHistoryChanged(e: PopStateEvent) {
window.setTimeout(RemotePlayManager.detect, 10);
// Hide Global settings
const $settings = document.querySelector('.bx-settings-container');
if ($settings) {
$settings.classList.add('bx-gone');
}
// Hide Navigation dialog
NavigationDialogManager.getInstance().hide();
LoadingScreen.reset();
window.setTimeout(HeaderSection.watchHeader, 2000);
BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
BxEventBus.Stream.emit('state.stopped', {});
}

View File

@ -263,10 +263,10 @@ export function clearDataSet($elm: HTMLElement) {
});
}
export function renderPresetsList<T extends PresetRecord>($select: HTMLSelectElement, allPresets: AllPresets<T>, selectedValue: number | null, addOffValue=false) {
export function renderPresetsList<T extends PresetRecord>($select: HTMLSelectElement, allPresets: AllPresets<T>, selectedValue: number | null, options: { addOffValue?: boolean, selectedIndicator?: boolean }={}) {
removeChildElements($select);
if (addOffValue) {
if (options.addOffValue) {
const $option = CE<HTMLOptionElement>('option', { value: 0 }, t('off'));
$option.selected = selectedValue === 0;
@ -275,7 +275,7 @@ export function renderPresetsList<T extends PresetRecord>($select: HTMLSelectEle
// Render options
const groups = {
default: t('default'),
default: t('default') + ' 🔒',
custom: t('custom'),
};
@ -284,8 +284,13 @@ export function renderPresetsList<T extends PresetRecord>($select: HTMLSelectEle
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;
const selected = selectedValue === record.id;
const name = options.selectedIndicator && selected ? '✅ ' + record.name : record.name;
const $option = CE<HTMLOptionElement>('option', { value: record.id }, name);
if (selected) {
$option.selected = true;
}
$optGroup.appendChild($option);
}

View File

@ -7,6 +7,7 @@ import { PrefKey } from "@/enums/pref-keys";
import { getPref, getPrefDefinition } from "./settings-storages/global-settings-storage";
import { CodecProfile } from "@/enums/pref-values";
import type { SettingDefinition } from "@/types/setting-definition";
import { BxEventBus } from "./bx-event-bus";
export function patchVideoApi() {
const PREF_SKIP_SPLASH_VIDEO = getPref(PrefKey.UI_SKIP_SPLASH_VIDEO);
@ -27,9 +28,9 @@ export function patchVideoApi() {
} satisfies StreamPlayerOptions;
STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref(PrefKey.VIDEO_PLAYER_TYPE), playerOptions);
BxEvent.dispatch(window, BxEvent.STREAM_PLAYING, {
$video: this,
});
BxEventBus.Stream.emit('state.playing', {
$video: this,
})
}
const nativePlay = HTMLMediaElement.prototype.play;
@ -75,10 +76,7 @@ export function patchRtcPeerConnection() {
// @ts-ignore
const dataChannel = nativeCreateDataChannel.apply(this, arguments);
BxEvent.dispatch(window, BxEvent.DATA_CHANNEL_CREATED, {
dataChannel: dataChannel,
});
BxEventBus.Stream.emit('dataChannelCreated', { dataChannel });
return dataChannel;
}
@ -200,8 +198,8 @@ export function patchMeControl() {
},
};
(window as any).MSA = new Proxy(MSA, MsaHandler);
(window as any).MeControl = new Proxy(MeControl, MeControlHandler);
window.MSA = new Proxy(MSA, MsaHandler);
window.MeControl = new Proxy(MeControl, MeControlHandler);
}
@ -209,7 +207,9 @@ export function patchMeControl() {
* Disable Adobe Audience Manager (AAM)
*/
export function disableAdobeAudienceManager() {
(window as any).adobe = Object.freeze({});
Object.defineProperty(window, 'adobe', {
get() { return Object.freeze({}); }
});
}
/**

View File

@ -11,6 +11,8 @@ import { XcloudInterceptor } from "./xcloud-interceptor";
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "./settings-storages/global-settings-storage";
import type { RemotePlayConsoleAddresses } from "@/types/network";
import { BlockFeature, StreamResolution } from "@/enums/pref-values";
import { blockAllNotifications } from "./utils";
type RequestType = 'xcloud' | 'xhome';
@ -120,29 +122,44 @@ export async function patchIceCandidates(request: Request, consoleAddrs?: Remote
return response;
}
export function interceptHttpRequests() {
let BLOCKED_URLS: string[] = [];
if (getPref(PrefKey.BLOCK_TRACKING)) {
// Clear Applications Insight buffers
clearAllLogs();
BLOCKED_URLS = BLOCKED_URLS.concat([
BLOCKED_URLS.push(
'https://arc.msn.com',
'https://browser.events.data.microsoft.com',
'https://dc.services.visualstudio.com',
'https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io',
'https://mscom.demdex.net',
]);
);
}
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES)) {
BLOCKED_URLS = BLOCKED_URLS.concat([
// 'https://notificationinbox.xboxlive.com',
// 'https://accounts.xboxlive.com/family/memberXuid',
const blockFeatures = getPref<BlockFeature[]>(PrefKey.BLOCK_FEATURES);
if (blockFeatures.includes(BlockFeature.CHAT)) {
BLOCKED_URLS.push(
'https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox',
);
}
if (blockFeatures.includes(BlockFeature.FRIENDS)) {
BLOCKED_URLS.push(
'https://peoplehub.xboxlive.com/users/me/people/social',
'https://peoplehub.xboxlive.com/users/me/people/recommendations',
'https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox',
// 'https://notificationinbox.xboxlive.com',
// 'https://accounts.xboxlive.com/family/memberXuid',
]);
);
}
// Block all notifications
if (blockAllNotifications()) {
BLOCKED_URLS.push(
'https://notificationinbox.xboxlive.com/',
);
}
const xhrPrototype = XMLHttpRequest.prototype;
@ -157,11 +174,13 @@ export function interceptHttpRequests() {
};
xhrPrototype.send = function(...arg) {
for (const blocked of BLOCKED_URLS) {
if ((this as any)._url.startsWith(blocked)) {
if (blocked === 'https://dc.services.visualstudio.com') {
for (const url of BLOCKED_URLS) {
if ((this as any)._url.startsWith(url)) {
if (url === 'https://dc.services.visualstudio.com') {
window.setTimeout(clearAllLogs, 1000);
}
BxLogger.warning('Blocked URL', url);
return false;
}
}
@ -175,6 +194,7 @@ export function interceptHttpRequests() {
'chat.xboxlive.com',
'notificationinbox.xboxlive.com',
'peoplehub.xboxlive.com',
'peoplehub-public.xboxlive.com',
'rta.xboxlive.com',
'userpresence.xboxlive.com',
'xblmessaging.xboxlive.com',
@ -186,12 +206,13 @@ export function interceptHttpRequests() {
'2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io',
];
(window as any).BX_FETCH = window.fetch = async (request: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
window.BX_FETCH = window.fetch = async (request: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
let url = (typeof request === 'string') ? request : (request as Request).url;
// Check blocked URLs
for (let blocked of BLOCKED_URLS) {
for (const blocked of BLOCKED_URLS) {
if (url.startsWith(blocked)) {
BxLogger.warning('Blocked URL', url);
return new Response('{"acc":1,"webResult":{}}', {
status: 200,
statusText: '200 OK',
@ -199,7 +220,7 @@ export function interceptHttpRequests() {
}
}
// Ignore URLs
// Ignore domains
const domain = (new URL(url)).hostname;
if (IGNORED_DOMAINS.includes(domain)) {
return NATIVE_FETCH(request, init);
@ -283,3 +304,45 @@ export function interceptHttpRequests() {
return XcloudInterceptor.handle(request, init);
}
}
export function generateMsDeviceInfo(osName: OsName) {
return {
appInfo: {
env: {
clientAppId: window.location.host,
clientAppType: 'browser',
clientAppVersion: '26.1.97',
clientSdkVersion: '10.3.7',
httpEnvironment: 'prod',
sdkInstallId: '',
},
},
dev: {
os: { name: osName, ver: '22631.2715', platform: 'desktop' },
hw: { make: 'Microsoft', model: 'unknown', sdktype: 'web' },
browser: { browserName: 'chrome', browserVersion: '130.0' },
displayInfo: {
dimensions: { widthInPixels: 1920, heightInPixels: 1080 },
pixelDensity: { dpiX: 1, dpiY: 1 },
},
},
};
}
export function getOsNameFromResolution(resolution: StreamResolution): OsName {
let osName: OsName;
switch (resolution) {
case StreamResolution.DIM_1080P_HQ:
osName = 'tizen';
break;
case StreamResolution.DIM_1080P:
osName = 'windows';
break;
default:
osName = 'android';
break;
}
return osName;
}

View File

@ -1,5 +1,4 @@
import { GuideMenu } from "@/modules/ui/guide-menu";
import { BxEvent } from "./bx-event";
import { BX_FLAGS } from "./bx-flags";
import { BxLogger } from "./bx-logger";
import { BxIcon } from "./bx-icon";
@ -7,6 +6,7 @@ import { AppInterface } from "./global";
import { createButton, ButtonStyle } from "./html";
import { t } from "./translation";
import { parseDetailsPath } from "./utils";
import { BxEventBus } from "./bx-event-bus";
export class RootDialogObserver {
@ -85,7 +85,7 @@ export class RootDialogObserver {
const shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);
if (shown !== beingShown) {
beingShown = shown;
BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED);
BxEventBus.Script.emit(shown ? 'dialog.shown' : 'dialog.dismissed', {});
}
}
});

View File

@ -1,16 +1,16 @@
import type { PrefKey } from "@/enums/pref-keys";
import type { PrefKey, StorageKey } from "@/enums/pref-keys";
import type { NumberStepperParams, SettingAction, SettingDefinitions } from "@/types/setting-definition";
import { BxEvent } from "../bx-event";
import { t } from "../translation";
import { SCRIPT_VARIANT } from "../global";
import { BxEventBus } from "../bx-event-bus";
export class BaseSettingsStore {
private storage: Storage;
private storageKey: string;
private storageKey: StorageKey;
private _settings: object | null;
private definitions: SettingDefinitions;
constructor(storageKey: string, definitions: SettingDefinitions) {
constructor(storageKey: StorageKey, definitions: SettingDefinitions) {
this.storage = window.localStorage;
this.storageKey = storageKey;
@ -93,7 +93,7 @@ export class BaseSettingsStore {
this.settings[key] = this.validateValue('get', key, value);
this.saveSettings();
emitEvent && BxEvent.dispatch(window, BxEvent.SETTINGS_CHANGED, {
emitEvent && BxEventBus.Script.emit('setting.changed', {
storageKey: this.storageKey,
settingKey: key,
settingValue: value,

View File

@ -8,11 +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 { CodecProfile, StreamResolution, TouchControllerMode, TouchControllerStyleStandard, TouchControllerStyleCustom, GameBarPosition, DeviceVibrationMode, NativeMkbMode, UiLayout, UiSection, StreamPlayerType, StreamVideoProcessing, VideoRatio, StreamStat, VideoPosition } from "@/enums/pref-values";
import { CodecProfile, StreamResolution, TouchControllerMode, TouchControllerStyleStandard, TouchControllerStyleCustom, GameBarPosition, DeviceVibrationMode, NativeMkbMode, UiLayout, UiSection, StreamPlayerType, StreamVideoProcessing, VideoRatio, StreamStat, VideoPosition, BlockFeature } 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";
import { BxEventBus } from "../bx-event-bus";
function getSupportedCodecProfiles() {
@ -321,10 +321,14 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
requiredVariants: 'full',
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')),
note: () => CE('div', {},
CE<HTMLAnchorElement>('a', {
href: 'https://github.com/redphx/better-xcloud/discussions/275',
target: '_blank',
}, t('enable-local-co-op-support-note')),
CE('br'),
'⚠️ ' + t('unexpected-behavior'),
),
},
[PrefKey.UI_CONTROLLER_SHOW_STATUS]: {
@ -432,8 +436,8 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
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();
BxEventBus.Script.on('list.forcedNativeMkb.updated', payload => {
(setting as any).multipleOptions = payload.data.data;
});
}
},
@ -582,25 +586,29 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
},
},
[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: true,
},
[PrefKey.BLOCK_SOCIAL_FEATURES]: {
label: t('disable-social-features'),
default: false,
},
[PrefKey.BLOCK_TRACKING]: {
label: t('disable-xcloud-analytics'),
default: false,
},
[PrefKey.BLOCK_FEATURES]: {
label: t('disable-features'),
default: [],
multipleOptions: {
[BlockFeature.CHAT]: t('chat'),
[BlockFeature.FRIENDS]: t('friends-followers'),
[BlockFeature.BYOG]: t('stream-your-own-game'),
[BlockFeature.NOTIFICATIONS_INVITES]: t('notifications') + ': ' + t('invites'),
[BlockFeature.NOTIFICATIONS_ACHIEVEMENTS]: t('notifications') + ': ' + t('achievements'),
},
},
[PrefKey.USER_AGENT_PROFILE]: {
label: t('user-agent-profile'),
note: '⚠️ ' + t('unexpected-behavior'),
@ -652,7 +660,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
},
},
[PrefKey.VIDEO_MAX_FPS]: {
label: t('max-fps'),
label: t('limit-fps'),
default: 60,
min: 10,
max: 60,
@ -821,11 +829,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
'1.1rem': t('large'),
},
},
[PrefKey.STATS_TRANSPARENT]: {
label: t('transparent-background'),
default: false,
},
[PrefKey.STATS_OPACITY]: {
[PrefKey.STATS_OPACITY_ALL]: {
label: t('opacity'),
default: 80,
min: 50,
@ -836,6 +840,17 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
ticks: 10,
},
},
[PrefKey.STATS_OPACITY_BACKGROUND]: {
label: t('background-opacity'),
default: 100,
min: 0,
max: 100,
params: {
steps: 10,
suffix: '%',
ticks: 10,
},
},
[PrefKey.STATS_CONDITIONAL_FORMATTING]: {
label: t('conditional-formatting'),
default: false,

View File

@ -10,6 +10,7 @@ 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";
import { VirtualControllerShortcut } from "@/modules/shortcuts/virtual-controller-shortcut";
export class ShortcutHandler {
static runAction(action: ShortcutAction) {
@ -69,6 +70,10 @@ export class ShortcutHandler {
case ShortcutAction.TRUE_ACHIEVEMENTS_OPEN:
TrueAchievements.getInstance().open(false);
break;
case ShortcutAction.CONTROLLER_XBOX_BUTTON_PRESS:
VirtualControllerShortcut.pressXboxButton();
break;
}
}
}

View File

@ -10,10 +10,10 @@ 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";
import { BxEventBus } from "./bx-event-bus";
export type StreamSettingsData = {
@ -110,7 +110,7 @@ export class StreamSettings {
}
StreamSettings.settings.deviceVibrationIntensity = intensity;
BxEvent.dispatch(window, BxEvent.DEVICE_VIBRATION_CHANGED);
BxEventBus.Script.emit('deviceVibration.updated', {});
}
static async refreshMkbSettings() {
@ -148,7 +148,7 @@ export class StreamSettings {
settings.mkbPreset = converted;
setPref(PrefKey.MKB_P1_MAPPING_PRESET_ID, orgPreset.id);
BxEvent.dispatch(window, BxEvent.MKB_UPDATED);
BxEventBus.Script.emit('mkb.setting.updated', {});
}
static async refreshKeyboardShortcuts() {
@ -159,7 +159,7 @@ export class StreamSettings {
settings.keyboardShortcuts = null;
setPref(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID, presetId);
BxEvent.dispatch(window, BxEvent.KEYBOARD_SHORTCUTS_UPDATED);
BxEventBus.Script.emit('keyboardShortcuts.updated', {});
return;
}
@ -179,7 +179,7 @@ export class StreamSettings {
settings.keyboardShortcuts = converted;
setPref(PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID, orgPreset.id);
BxEvent.dispatch(window, BxEvent.KEYBOARD_SHORTCUTS_UPDATED);
BxEventBus.Script.emit('keyboardShortcuts.updated', {});
}
static async refreshAllSettings() {

View File

@ -1,10 +1,10 @@
import { PrefKey } from "@/enums/pref-keys";
import { BxEvent } from "./bx-event";
import { STATES } from "./global";
import { humanFileSize, secondsToHm } from "./html";
import { getPref } from "./settings-storages/global-settings-storage";
import { BxLogger } from "./bx-logger";
import { StreamStat } from "@/enums/pref-values";
import { BxEventBus } from "./bx-event-bus";
export type StreamStatGrade = '' | 'bad' | 'ok' | 'good';
@ -96,7 +96,7 @@ export class StreamStatsCollector {
current: -1,
grades: [40, 75, 100],
toString() {
return this.current === -1 ? '???' : this.current.toString();
return this.current === -1 ? '???' : this.current.toString().padStart(3, ' ');
},
},
@ -104,7 +104,7 @@ export class StreamStatsCollector {
current: 0,
grades: [30, 40, 60],
toString() {
return `${this.current.toFixed(1)}ms`;
return `${this.current.toFixed(1)}ms`.padStart(6, ' ');
},
},
@ -112,14 +112,14 @@ export class StreamStatsCollector {
current: 0,
toString() {
const maxFps = getPref<VideoMaxFps>(PrefKey.VIDEO_MAX_FPS);
return maxFps < 60 ? `${maxFps}/${this.current}` : this.current.toString();
return maxFps < 60 ? `${maxFps}/${this.current}`.padStart(5, ' ') : this.current.toString();
},
},
[StreamStat.BITRATE]: {
current: 0,
toString() {
return `${this.current.toFixed(1)} Mbps`;
return `${this.current.toFixed(1)} Mbps`.padStart(9, ' ');
},
},
@ -128,7 +128,7 @@ export class StreamStatsCollector {
dropped: 0,
toString() {
const framesDroppedPercentage = (this.dropped * 100 / ((this.dropped + this.received) || 1)).toFixed(1);
return framesDroppedPercentage === '0.00' ? this.dropped.toString() : `${this.dropped} (${framesDroppedPercentage}%)`;
return framesDroppedPercentage === '0.0' ? this.dropped.toString() : `${this.dropped} (${framesDroppedPercentage}%)`;
},
},
@ -137,7 +137,7 @@ export class StreamStatsCollector {
dropped: 0,
toString() {
const packetsLostPercentage = (this.dropped * 100 / ((this.dropped + this.received) || 1)).toFixed(1);
return packetsLostPercentage === '0.00' ? this.dropped.toString() : `${this.dropped} (${packetsLostPercentage}%)`;
return packetsLostPercentage === '0.0' ? this.dropped.toString() : `${this.dropped} (${packetsLostPercentage}%)`;
},
},
@ -146,14 +146,14 @@ export class StreamStatsCollector {
total: 0,
grades: [6, 9, 12],
toString() {
return isNaN(this.current) ? '??ms' : `${this.current.toFixed(1)}ms`;
return isNaN(this.current) ? '??ms' : `${this.current.toFixed(1)}ms`.padStart(6, ' ');
},
},
[StreamStat.DOWNLOAD]: {
total: 0,
toString() {
return humanFileSize(this.total);
return humanFileSize(this.total).padStart(8, ' ');
},
},
@ -201,6 +201,7 @@ export class StreamStatsCollector {
};
private lastVideoStat?: RTCInboundRtpStreamStats | null;
private selectedCandidatePairId: string | null | undefined = null;
private constructor() {
BxLogger.info(this.LOG_TAG, 'constructor()');
@ -212,6 +213,22 @@ export class StreamStatsCollector {
return;
}
// Find selected candidate
if (!this.selectedCandidatePairId) {
let found = false;
stats.forEach(stat => {
if (found || stat.type !== 'transport') {
return;
}
stat = (stat as unknown as RTCTransportStats);
if (stat.iceState === 'connected' && stat.selectedCandidatePairId) {
this.selectedCandidatePairId = (stat as unknown as RTCTransportStats).selectedCandidatePairId;
found = true;
}
});
}
stats.forEach(stat => {
if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
// FPS
@ -256,7 +273,7 @@ export class StreamStatsCollector {
dt.current = dt.total / framesDecodedDiff * 1000;
this.lastVideoStat = stat;
} else if (stat.type === 'candidate-pair' && stat.packetsReceived > 0 && stat.state === 'succeeded') {
} else if (this.selectedCandidatePairId && stat.type === 'candidate-pair' && stat.id === this.selectedCandidatePairId) {
// Round Trip Time
const ping = this.currentStats[StreamStat.PING];
ping.current = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1;
@ -310,9 +327,8 @@ export class StreamStatsCollector {
}
static setupEvents() {
window.addEventListener(BxEvent.STREAM_PLAYING, e => {
const statsCollector = StreamStatsCollector.getInstance();
statsCollector.reset();
BxEventBus.Stream.on('state.playing', () => {
StreamStatsCollector.getInstance().reset();
});
}
}

View File

@ -27,6 +27,7 @@ export const SUPPORTED_LANGUAGES = {
};
const Texts = {
"achievements": "Achievements",
"activate": "Activate",
"activated": "Activated",
"active": "Active",
@ -42,6 +43,7 @@ const Texts = {
"auto": "Auto",
"back-to-home": "Back to home",
"back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?",
"background-opacity": "Background opacity",
"battery": "Battery",
"battery-saving": "Battery saving",
"better-xcloud": "Better xCloud",
@ -54,11 +56,13 @@ const Texts = {
"brazil": "Brazil",
"brightness": "Brightness",
"browser-unsupported-feature": "Your browser doesn't support this feature",
"button-xbox": "Xbox button",
"bypass-region-restriction": "Bypass region restriction",
"can-stream-xbox-360-games": "Can stream Xbox 360 games",
"cancel": "Cancel",
"cant-stream-xbox-360-games": "Can't stream Xbox 360 games",
"center": "Center",
"chat": "Chat",
"clarity-boost": "Clarity boost",
"clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",
"clear": "Clear",
@ -83,6 +87,8 @@ const Texts = {
"contrast": "Contrast",
"controller": "Controller",
"controller-friendly-ui": "Controller-friendly UI",
"controller-mapping": "Controller mapping",
"controller-mapping-in-game": "In-game controller mapping",
"controller-shortcuts": "Controller shortcuts",
"controller-shortcuts-connect-note": "Connect a controller to use this feature",
"controller-shortcuts-in-game": "In-game controller shortcuts",
@ -94,6 +100,7 @@ const Texts = {
"deadzone-counterweight": "Deadzone counterweight",
"decrease": "Decrease",
"default": "Default",
"default-preset-note": "You can't modify default presets. Create a new one to customize it.",
"delete": "Delete",
"device": "Device",
"device-unsupported-touch": "Your device doesn't have touch support",
@ -101,6 +108,7 @@ const Texts = {
"device-vibration-not-using-gamepad": "On when not using gamepad",
"disable": "Disable",
"disable-byog-feature": "Disable \"Stream your own game\" feature",
"disable-features": "Disable features",
"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",
@ -125,6 +133,7 @@ const Texts = {
"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",
"friends-followers": "Friends and followers",
"game-bar": "Game Bar",
"getting-consoles-list": "Getting the list of consoles...",
"guide": "Guide",
@ -146,6 +155,7 @@ const Texts = {
"import": "Import",
"increase": "Increase",
"install-android": "Better xCloud app for Android",
"invites": "Invites",
"japan": "Japan",
"jitter": "Jitter",
"keyboard-key": "Keyboard key",
@ -156,13 +166,14 @@ const Texts = {
"large": "Large",
"layout": "Layout",
"left-stick": "Left stick",
"limit-fps": "Limit FPS",
"load-failed-message": "Failed to run Better xCloud",
"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",
"mapping": "Mapping",
"may-not-work-properly": "May not work properly!",
"menu": "Menu",
"microphone": "Microphone",
@ -201,6 +212,7 @@ const Texts = {
"no-consoles-found": "No consoles found",
"no-controllers-connected": "No controllers connected",
"normal": "Normal",
"notifications": "Notifications",
"off": "Off",
"official": "Official",
"on": "On",
@ -217,6 +229,7 @@ const Texts = {
"prefer-ipv6-server": "Prefer IPv6 server",
"preferred-game-language": "Preferred game's language",
"preset": "Preset",
"press": "Press",
"press-esc-to-cancel": "Press Esc to cancel",
"press-key-to-toggle-mkb": [
(e: any) => `Press ${e.key} to toggle this feature`,
@ -325,6 +338,7 @@ const Texts = {
"stream": "Stream",
"stream-settings": "Stream settings",
"stream-stats": "Stream stats",
"stream-your-own-game": "Stream your own game",
"stretch": "Stretch",
"suggest-settings": "Suggest settings",
"suggest-settings-link": "Suggest recommended settings for this device",

View File

@ -97,7 +97,7 @@ export class TrueAchievements {
}
this.updateIds(xboxTitleId);
if (document.documentElement.dataset.xdsPlatform === 'tv') {
if (document.body.dataset.mediaType === 'tv') {
$div.appendChild(this.$link);
} else {
$div.appendChild(this.$button);

View File

@ -10,7 +10,7 @@ type UserAgentConfig = {
const SMART_TV_UNIQUE_ID = 'FC4A1DA2-711C-4E9C-BC7F-047AF8A672EA';
let CHROMIUM_VERSION = '125.0.0.0';
if (!!(window as any).chrome || window.navigator.userAgent.includes('Chrome')) {
if (!!window.chrome || window.navigator.userAgent.includes('Chrome')) {
// Get Chromium version in the original User-Agent value
const match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/);
if (match) {
@ -59,7 +59,7 @@ export class UserAgent {
}
static getDefault(): string {
return (window.navigator as any).orgUserAgent || window.navigator.userAgent;
return window.navigator.orgUserAgent || window.navigator.userAgent;
}
static get(profile: UserAgentProfile): string {
@ -123,12 +123,12 @@ export class UserAgent {
// Clear data of navigator.userAgentData, force xCloud to detect browser based on navigator.userAgent
if ('userAgentData' in window.navigator) {
(window.navigator as any).orgUserAgentData = (window.navigator as any).userAgentData;
window.navigator.orgUserAgentData = window.navigator.userAgentData;
Object.defineProperty(window.navigator, 'userAgentData', {});
}
// Override navigator.userAgent
(window.navigator as any).orgUserAgent = window.navigator.userAgent;
window.navigator.orgUserAgent = window.navigator.userAgent;
Object.defineProperty(window.navigator, 'userAgent', {
value: newUserAgent,
});

View File

@ -5,6 +5,7 @@ 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";
import { BlockFeature } from "@/enums/pref-values";
/**
* Check for update
@ -44,7 +45,7 @@ export function checkForUpdate() {
* Disable PWA requirement on Safari
*/
export function disablePwa() {
const userAgent = ((window.navigator as any).orgUserAgent || window.navigator.userAgent || '').toLowerCase();
const userAgent = (window.navigator.orgUserAgent || window.navigator.userAgent || '').toLowerCase();
if (!userAgent) {
return;
}
@ -155,3 +156,19 @@ export function clearAllData() {
alert(t('clear-data-success'));
}
export function blockAllNotifications() {
const blockFeatures = getPref<BlockFeature[]>(PrefKey.BLOCK_FEATURES);
const blockAll = [BlockFeature.FRIENDS, BlockFeature.NOTIFICATIONS_ACHIEVEMENTS, BlockFeature.NOTIFICATIONS_INVITES].every(value => blockFeatures.includes(value));
return blockAll;
}
export function blockSomeNotifications() {
const blockFeatures = getPref<BlockFeature[]>(PrefKey.BLOCK_FEATURES);
if (blockAllNotifications()) {
return false;
}
const blockSome = [BlockFeature.FRIENDS, BlockFeature.NOTIFICATIONS_ACHIEVEMENTS, BlockFeature.NOTIFICATIONS_INVITES].some(value => blockFeatures.includes(value));
return blockSome;
}

View File

@ -4,15 +4,15 @@ import { LoadingScreen } from "@modules/loading-screen";
import { RemotePlayManager } from "@/modules/remote-play-manager";
import { StreamBadges } from "@modules/stream/stream-badges";
import { TouchController } from "@modules/touch-controller";
import { BxEvent } from "./bx-event";
import { NATIVE_FETCH, BX_FLAGS } from "./bx-flags";
import { STATES } from "./global";
import { patchIceCandidates } from "./network";
import { generateMsDeviceInfo, getOsNameFromResolution, patchIceCandidates } from "./network";
import { getPreferredServerRegion } from "./region";
import { BypassServerIps } from "@/enums/bypass-servers";
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "./settings-storages/global-settings-storage";
import { NativeMkbMode, StreamResolution, TouchControllerMode } from "@/enums/pref-values";
import { BxEventBus } from "./bx-event-bus";
export class XcloudInterceptor {
private static readonly SERVER_EXTRA_INFO: Record<string, [string, ServerContinent]> = {
@ -42,46 +42,6 @@ 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<string>(PrefKey.SERVER_BYPASS_RESTRICTION);
if (bypassServer !== 'off') {
@ -92,7 +52,7 @@ export class XcloudInterceptor {
const response = await NATIVE_FETCH(request, init);
if (response.status !== 200) {
// Unsupported region
BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE);
BxEventBus.Script.emit('xcloud.server.unavailable', {});
return response;
}
@ -122,6 +82,7 @@ export class XcloudInterceptor {
region.contintent = serverExtra[regionName][1];
} else {
region.contintent = 'other';
BX_FLAGS.Debug && alert('New server: ' + shortName);
}
}
@ -129,7 +90,7 @@ export class XcloudInterceptor {
STATES.serverRegions[region.name] = Object.assign({}, region);
}
BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY);
BxEventBus.Script.emit('xcloud.server.ready', {});
const preferredRegion = getPreferredServerRegion();
if (preferredRegion && preferredRegion in STATES.serverRegions) {
@ -147,7 +108,7 @@ export class XcloudInterceptor {
}
private static async handlePlay(request: RequestInfo | URL, init?: RequestInit) {
BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
BxEventBus.Stream.emit('state.loading', {});
const PREF_STREAM_TARGET_RESOLUTION = getPref<StreamResolution>(PrefKey.STREAM_RESOLUTION);
const PREF_STREAM_PREFERRED_LOCALE = getPref<StreamPreferredLocale>(PrefKey.STREAM_PREFERRED_LOCALE);
@ -175,24 +136,8 @@ export class XcloudInterceptor {
// Force stream's resolution
if (PREF_STREAM_TARGET_RESOLUTION !== 'auto') {
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
}
const osName = getOsNameFromResolution(PREF_STREAM_TARGET_RESOLUTION);
headers['x-ms-device-info'] = JSON.stringify(generateMsDeviceInfo(osName));
body.settings.osName = osName;
}
@ -245,7 +190,7 @@ export class XcloudInterceptor {
return response;
}
BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
BxEventBus.Stream.emit('state.starting', {});
const obj = JSON.parse(text);
let overrides = JSON.parse(obj.clientStreamingConfigOverrides || '{}') || {};

View File

@ -3,56 +3,17 @@ import { BxEvent } from "./bx-event";
import { SupportedInputType } from "./bx-exposed";
import { NATIVE_FETCH } from "./bx-flags";
import { STATES } from "./global";
import { patchIceCandidates } from "./network";
import { generateMsDeviceInfo, getOsNameFromResolution, patchIceCandidates } from "./network";
import { PrefKey } from "@/enums/pref-keys";
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";
import { BxEventBus } from "./bx-event-bus";
export class XhomeInterceptor {
private static consoleAddrs: RemotePlayConsoleAddresses = {};
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: Request) {
try {
const clone = request.clone();
@ -75,7 +36,7 @@ export class XhomeInterceptor {
}
private static async handleConfiguration(request: Request | URL) {
BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
BxEventBus.Stream.emit('state.starting', {});
const response = await NATIVE_FETCH(request);
const obj = await response.clone().json();
@ -164,7 +125,7 @@ export class XhomeInterceptor {
}
private static async handlePlay(request: RequestInfo | URL) {
BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
BxEventBus.Stream.emit('state.loading', {});
const clone = (request as Request).clone();
const body = await clone.json();
@ -191,21 +152,8 @@ export class XhomeInterceptor {
headers.authorization = `Bearer ${RemotePlayManager.getInstance()!.getXhomeToken()}`;
// Patch resolution
const deviceInfo = XhomeInterceptor.BASE_DEVICE_INFO;
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);
const osName = getOsNameFromResolution(getPref<StreamResolution>(PrefKey.REMOTE_PLAY_STREAM_RESOLUTION));
headers['x-ms-device-info'] = JSON.stringify(generateMsDeviceInfo(osName));
const opts: Record<string, any> = {
method: clone.method,

View File

@ -1,5 +1,4 @@
import type { NumberStepperParams } from "@/types/setting-definition";
import { BxEvent } from "@/utils/bx-event";
import { CE, escapeCssSelector } from "@/utils/html";
import { setNearby } from "@/utils/navigation-utils";
import type { BxHtmlSettingElement } from "@/utils/setting-element";
@ -26,7 +25,6 @@ export class BxNumberStepper extends HTMLInputElement implements BxHtmlSettingEl
private $btnDec!: HTMLButtonElement;
private $range!: HTMLInputElement | null;
onInput!: typeof BxNumberStepper['onInput'];
onRangeInput!: typeof BxNumberStepper['onRangeInput'];
onClick!: typeof BxNumberStepper['onClick'];
onPointerUp!: typeof BxNumberStepper['onPointerUp'];
@ -77,7 +75,6 @@ export class BxNumberStepper extends HTMLInputElement implements BxHtmlSettingEl
self.$btnDec = $btnDec;
self.onChange = onChange;
self.onInput = BxNumberStepper.onInput.bind(self);
self.onRangeInput = BxNumberStepper.onRangeInput.bind(self);
self.onClick = BxNumberStepper.onClick.bind(self);
self.onPointerUp = BxNumberStepper.onPointerUp.bind(self);
@ -118,7 +115,7 @@ export class BxNumberStepper extends HTMLInputElement implements BxHtmlSettingEl
options.hideSlider && $range.classList.add('bx-gone');
$range.addEventListener('input', self.onRangeInput);
self.addEventListener('input', self.onInput);
self.addEventListener('input', self.onRangeInput);
self.appendChild($range);
if (options.ticks || options.exactTicks) {
@ -183,10 +180,6 @@ export class BxNumberStepper extends HTMLInputElement implements BxHtmlSettingEl
return value;
}
private static onInput(this: BxNumberStepper, e: Event) {
BxEvent.dispatch(this.$range, 'input');
}
private static onRangeInput(this: BxNumberStepper, e: Event) {
let value = parseInt((e.target as HTMLInputElement).value);
if (this.options.reverse) {