Compare commits

..

11 Commits

Author SHA1 Message Date
f7266d6361 Bump version to 6.0.1 2024-12-07 16:56:34 +07:00
4bd96de89e Update translations 2024-12-07 16:54:31 +07:00
4011eb402a Linting 2024-12-07 16:48:58 +07:00
557a38214d Fix native MKB not working in Android app 2024-12-07 10:31:28 +07:00
4648126f03 Use toFixed(1) in stats 2024-12-07 09:45:47 +07:00
07b2e47757 Don't show Shortcut button on Android TV 2024-12-07 09:42:29 +07:00
cf4609d87b Support dynamic resolution with WebGL2 in Genshin 2024-12-07 08:24:44 +07:00
1ca2b771e7 Fix forcing native MKB not working when mode = "default" 2024-12-07 07:46:13 +07:00
fe98a1165f Adjust video position (#583) 2024-12-06 21:42:18 +07:00
4777f90a53 Fix the Shortcut button not showing in product page 2024-12-06 18:16:58 +07:00
1ea1afe4d4 Optimize Patcher 2024-12-06 18:13:24 +07:00
70 changed files with 603 additions and 408 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.0
// @version 6.0.1
// ==/UserScript==

File diff suppressed because one or more lines are too long

View File

@ -47,7 +47,41 @@ body[data-media-type=tv] .bx-stream-home-button {
}
div[data-testid=media-container] {
&[data-position=center] {
display: flex;
}
&[data-position=top] {
video, canvas {
top: 0;
}
}
&[data-position=bottom] {
video, canvas {
bottom: 0;
}
}
}
#game-stream {
video {
margin: auto;
align-self: center;
background: #000;
position: absolute;
left: 0;
right: 0;
}
canvas {
align-self: center;
margin: auto;
position: absolute;
left: 0;
right: 0;
}
&.bx-taking-screenshot:before {
animation: bx-anim-taking-screenshot 0.5s ease;
@ -59,21 +93,6 @@ div[data-testid=media-container] {
}
}
#game-stream video {
margin: auto;
align-self: center;
background: #000;
}
#game-stream canvas {
position: absolute;
align-self: center;
margin: auto;
left: 0;
right: 0;
}
#gamepass-dialog-root div[class^=Guide-module__guide] {
.bx-button {
overflow: visible;

View File

@ -47,7 +47,7 @@ export const enum PrefKey {
CONTROLLER_POLLING_RATE = 'controller.pollingRate',
NATIVE_MKB_MODE = 'nativeMkb.mode',
FORCE_NATIVE_MKB_GAMES = 'nativeMkb.forcedGames',
NATIVE_MKB_FORCED_GAMES = 'nativeMkb.forcedGames',
NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY = 'nativeMkb.scroll.sensitivityX',
NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY = 'nativeMkb.scroll.sensitivityY',
@ -93,6 +93,7 @@ export const enum PrefKey {
VIDEO_BRIGHTNESS = 'video.brightness',
VIDEO_CONTRAST = 'video.contrast',
VIDEO_SATURATION = 'video.saturation',
VIDEO_POSITION = 'video.position',
AUDIO_MIC_ON_PLAYING = 'audio.mic.onPlaying',
AUDIO_VOLUME_CONTROL_ENABLED = 'audio.volume.booster.enabled',

View File

@ -93,6 +93,14 @@ export const enum VideoRatio {
FILL = 'fill',
}
export const enum VideoPosition {
CENTER = 'center',
TOP = 'top',
TOP_HALF = 'top-half',
BOTTOM = 'bottom',
BOTTOM_HALF = 'bottom-half',
}
export const enum StreamPlayerType {
VIDEO = 'default',
WEBGL2 = 'webgl2',

View File

@ -16,7 +16,7 @@ import { LoadingScreen } from "@modules/loading-screen";
import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider";
import { TouchController } from "@modules/touch-controller";
import { checkForUpdate, disablePwa, productTitleToSlug } from "@utils/utils";
import { Patcher } from "@modules/patcher";
import { Patcher } from "@/modules/patcher/patcher";
import { RemotePlayManager } from "@/modules/remote-play-manager";
import { onHistoryChanged, patchHistoryMethod } from "@utils/history";
import { disableAdobeAudienceManager, patchAudioContext, patchCanvasContext, patchMeControl, patchPointerLockApi, patchRtcCodecs, patchRtcPeerConnection, patchVideoApi } from "@utils/monkey-patches";
@ -266,9 +266,9 @@ window.addEventListener(BxEvent.STREAM_ERROR_PAGE, e => {
BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
});
window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, e => {
isFullVersion() && window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, e => {
const component = (e as any).component;
if (component === 'product-details') {
if (component === 'product-detail') {
ProductDetailsPage.injectButtons();
}
});
@ -353,8 +353,8 @@ isFullVersion() && window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, e => {
function main() {
GhPagesUtils.fetchLatestCommit();
if (getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON) {
const customList = getPref<string[]>(PrefKey.FORCE_NATIVE_MKB_GAMES);
if (getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) !== NativeMkbMode.OFF) {
const customList = getPref<string[]>(PrefKey.NATIVE_MKB_FORCED_GAMES);
BX_FLAGS.ForceNativeMkbTitles.push(...customList);
}
@ -406,7 +406,7 @@ function main() {
}
// Start PointerProviderServer
if (getPref(PrefKey.MKB_ENABLED) && AppInterface) {
if (AppInterface && (getPref(PrefKey.MKB_ENABLED) || getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON)) {
STATES.pointerServerPort = AppInterface.startPointerServer() || 9269;
BxLogger.info('startPointerServer', 'Port', STATES.pointerServerPort.toString());
}

View File

@ -95,7 +95,7 @@ export class KeyHelper {
const tmp = str.split(':');
const code = tmp[0] as KeyEventInfo['code'];
const modifiers = parseInt(tmp[1]);
const modifiers = parseInt(tmp[1] as string);
return {
code,

View File

@ -16,6 +16,8 @@ import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { GamepadKey, GamepadStick } from "@/enums/gamepad";
import { MkbPopup } from "./mkb-popup";
import type { MkbConvertedPresetData } from "@/types/presets";
import { StreamSettings } from "@/utils/stream-settings";
import { ShortcutAction } from "@/enums/shortcut-actions";
const PointerToMouseButton = {
1: 0,
@ -529,7 +531,12 @@ export class EmulatedMkbHandler extends MkbHandler {
MkbPopup.getInstance().reset();
if (AppInterface) {
Toast.show(t('press-key-to-toggle-mkb', {key: `<b>F8</b>`}), t('virtual-controller'), {html: true});
const shortcutKey = StreamSettings.findKeyboardShortcut(ShortcutAction.MKB_TOGGLE);
if (shortcutKey) {
const msg = t('press-key-to-toggle-mkb', { key: `<b>${KeyHelper.codeToKeyName(shortcutKey)}</b>` });
Toast.show(msg, t('native-mkb'), { html: true });
}
this.waitForMouseData(false);
} else {
this.waitForMouseData(true);

View File

@ -0,0 +1,45 @@
import type { PatchArray, PatchName, PatchPage } from "./patcher";
export class PatcherUtils {
static indexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number {
const index = txt.indexOf(searchString, startIndex);
if (index < 0 || (maxRange && index - startIndex > maxRange)) {
return -1;
}
return index;
}
static lastIndexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number {
const index = txt.lastIndexOf(searchString, startIndex);
if (index < 0 || (maxRange && startIndex - index > maxRange)) {
return -1;
}
return index;
}
static insertAt(txt: string, index: number, insertString: string): string {
return txt.substring(0, index) + insertString + txt.substring(index);
}
static replaceWith(txt: string, index: number, fromString: string, toString: string): string {
return txt.substring(0, index) + toString + txt.substring(index + fromString.length);
}
static filterPatches(patches: Array<string | false>): PatchArray {
return patches.filter((item): item is PatchName => !!item);
}
static patchBeforePageLoad(str: string, page: PatchPage): string | false {
let text = `chunkName:()=>"${page}-page",`;
if (!str.includes(text)) {
return false;
}
str = str.replace('requireAsync(e){', `requireAsync(e){window.BX_EXPOSED.beforePageLoad("${page}");`);
str = str.replace('requireSync(e){', `requireSync(e){window.BX_EXPOSED.beforePageLoad("${page}");`);
return str;
}
}

View File

@ -1,4 +1,4 @@
import { SCRIPT_VERSION, STATES } from "@utils/global";
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";
@ -17,39 +17,12 @@ 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 { PatcherUtils } from "./patcher-utils.js";
type PathName = keyof typeof PATCHES;
type PatchArray = PathName[];
export type PatchName = keyof typeof PATCHES;
export type PatchArray = PatchName[];
export type PatchPage = 'home' | 'stream' | 'product-detail';
class PatcherUtils {
static indexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number {
const index = txt.indexOf(searchString, startIndex);
if (index < 0 || (maxRange && index - startIndex > maxRange)) {
return -1;
}
return index;
}
static lastIndexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number {
const index = txt.lastIndexOf(searchString, startIndex);
if (index < 0 || (maxRange && startIndex - index > maxRange)) {
return -1;
}
return index;
}
static insertAt(txt: string, index: number, insertString: string): string {
return txt.substring(0, index) + insertString + txt.substring(index);
}
static replaceWith(txt: string, index: number, fromString: string, toString: string): string {
return txt.substring(0, index) + toString + txt.substring(index + fromString.length);
}
}
const ENDING_CHUNKS_PATCH_NAME = 'loadingEndingChunks';
const LOG_TAG = 'Patcher';
const PATCHES = {
@ -314,19 +287,6 @@ logFunc(logTag, '//', logMessage);
return str;
},
// Add patches that are only needed when start playing
loadingEndingChunks(str: string) {
let text = '"FamilySagaManager"';
if (!str.includes(text)) {
return false;
}
BxLogger.info(LOG_TAG, 'Remaining patches:', PATCH_ORDERS);
PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS);
return str;
},
// Disable StreamGate
disableStreamGate(str: string) {
const index = str.indexOf('case"partially-ready":');
@ -520,6 +480,10 @@ BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED);
// Get param name
const params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)![1];
if (!params) {
return false;
}
const titleInfoVar = params.split(',')[0];
const newCode = `
@ -542,6 +506,10 @@ BxLogger.info('patchXcloudTitleInfo', ${titleInfoVar});
// Get param name
const params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)![1];
if (!params) {
return false;
}
const configsVar = params.split(',')[1];
const newCode = `
@ -719,30 +687,6 @@ true` + text;
return str;
},
/*
(x.AW, {
path: V.LoginDeviceCode.path,
exact: !0,
render: () => (0, n.jsx)(qe, {
children: (0, n.jsx)(Et.R, {})
})
}, V.LoginDeviceCode.name),
const qe = e => {
let {
children: t
} = e;
const {
isTV: a,
isSupportedTVBrowser: r
} = (0, T.d)();
return a && r ? (0, n.jsx)(n.Fragment, {
children: t
}) : (0, n.jsx)(x.l_, {
to: V.Home.getLink()
})
};
*/
enableTvRoutes(str: string) {
let index = str.indexOf('.LoginDeviceCode.path,');
if (index < 0) {
@ -912,16 +856,15 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
return str;
},
// product-details-page.js#2388, 24.17.20
detectProductDetailsPage(str: string) {
detectProductDetailPage(str: string) {
let index = str.indexOf('{location:"ProductDetailPage",');
index >= 0 && (index = PatcherUtils.lastIndexOf('return', str, index, 200));
index >= 0 && (index = PatcherUtils.lastIndexOf(str, 'return', index, 200));
if (index < 0) {
return false;
}
str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-details" });' + str.substring(index);
str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-detail" });' + str.substring(index);
return str;
},
@ -992,9 +935,21 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
str = str.replace(text, '=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);');
return str;
},
homePageBeforeLoad(str: string) {
return PatcherUtils.patchBeforePageLoad(str, 'home');
},
productDetailPageBeforeLoad(str: string) {
return PatcherUtils.patchBeforePageLoad(str, 'product-detail');
},
streamPageBeforeLoad(str: string) {
return PatcherUtils.patchBeforePageLoad(str, 'stream');
},
};
let PATCH_ORDERS: PatchArray = [
let PATCH_ORDERS = PatcherUtils.filterPatches([
...(getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
'enableNativeMkb',
'exposeInputSink',
@ -1015,10 +970,13 @@ let PATCH_ORDERS: PatchArray = [
'exposeStreamSession',
'exposeDialogRoutes',
'homePageBeforeLoad',
'productDetailPageBeforeLoad',
'streamPageBeforeLoad',
'guideAchievementsDefaultLocked',
'enableTvRoutes',
// AppInterface && 'detectProductDetailsPage',
'supportLocalCoOp',
'overrideStorageGetSettings',
@ -1027,11 +985,6 @@ let PATCH_ORDERS: PatchArray = [
getPref<UiLayout>(PrefKey.UI_LAYOUT) !== UiLayout.DEFAULT && 'websiteLayout',
getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole',
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection',
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection',
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',
...(STATES.userAgent.capabilities.touch ? [
'disableTouchContextMenu',
] : []),
@ -1059,12 +1012,19 @@ let PATCH_ORDERS: PatchArray = [
'enableConsoleLogging',
'enableXcloudLogger',
] : []),
].filter((item): item is string => !!item) as PatchArray;
]);
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',
]);
// Only when playing
// TODO: check this
// @ts-ignore
let PLAYING_PATCH_ORDERS: PatchArray = [
let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
'patchXcloudTitleInfo',
'disableGamepadDisconnectedScreen',
'patchStreamHud',
@ -1085,7 +1045,7 @@ let PLAYING_PATCH_ORDERS: PatchArray = [
...(STATES.userAgent.capabilities.touch ? [
getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'patchShowSensorControls',
getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'exposeTouchLayoutManager',
(getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF || getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF) || !STATES.userAgent.capabilities.touch) && 'disableTakRenderer',
(getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF || getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF)) && 'disableTakRenderer',
getPref<TouchControllerDefaultOpacity>(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity',
'patchBabylonRendererClass',
] : []),
@ -1106,12 +1066,32 @@ let PLAYING_PATCH_ORDERS: PatchArray = [
'patchMouseAndKeyboardEnabled',
'disableNativeRequestPointerLock',
] : []),
].filter((item): item is string => !!item);
]);
const ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS];
let PRODUCT_DETAIL_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
AppInterface && 'detectProductDetailPage',
]);
const ALL_PATCHES = [...PATCH_ORDERS, ...HOME_PAGE_PATCH_ORDERS, ...STREAM_PAGE_PATCH_ORDERS, ...PRODUCT_DETAIL_PAGE_PATCH_ORDERS];
export class Patcher {
static #patchFunctionBind() {
private static remainingPatches: { [key in PatchPage]: PatchArray } = {
home: HOME_PAGE_PATCH_ORDERS,
stream: STREAM_PAGE_PATCH_ORDERS,
'product-detail': PRODUCT_DETAIL_PAGE_PATCH_ORDERS,
};
static patchPage(page: PatchPage) {
const remaining = Patcher.remainingPatches[page];
if (!remaining) {
return;
}
PATCH_ORDERS = PATCH_ORDERS.concat(remaining);
delete Patcher.remainingPatches[page];
}
private static patchNativeBind() {
const nativeBind = Function.prototype.bind;
Function.prototype.bind = function() {
let valid = false;
@ -1132,8 +1112,6 @@ export class Patcher {
return nativeBind.apply(this, arguments);
}
PatcherCache.getInstance().init();
if (typeof arguments[1] === 'function') {
BxLogger.info(LOG_TAG, 'Restored Function.prototype.bind()');
Function.prototype.bind = nativeBind;
@ -1211,6 +1189,7 @@ export class Patcher {
patchesToCheck.splice(patchIndex, 1);
patchIndex--;
PATCH_ORDERS = PATCH_ORDERS.filter(item => item != patchName);
BxLogger.info(LOG_TAG, 'Remaining patches', PATCH_ORDERS);
}
// Apply patched functions
@ -1236,7 +1215,7 @@ export class Patcher {
}
static init() {
Patcher.#patchFunctionBind();
Patcher.patchNativeBind();
}
}
@ -1249,7 +1228,29 @@ export class PatcherCache {
private CACHE!: { [key: string]: PatchArray };
private isInitialized = false;
private constructor() {
this.checkSignature();
// Read cache from storage
this.CACHE = JSON.parse(window.localStorage.getItem(this.KEY_CACHE) || '{}');
BxLogger.info(LOG_TAG, 'Cache', this.CACHE);
const pathName = window.location.pathname;
if (pathName.includes('/play/launch/')) {
Patcher.patchPage('stream');
} else if (pathName.includes('/play/games/')) {
Patcher.patchPage('product-detail');
} else if (pathName.endsWith('/play') || pathName.endsWith('/play/')) {
Patcher.patchPage('home');
}
// Remove cached patches from PATCH_ORDERS & PLAYING_PATCH_ORDERS
PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS);
STREAM_PAGE_PATCH_ORDERS = this.cleanupPatches(STREAM_PAGE_PATCH_ORDERS);
PRODUCT_DETAIL_PAGE_PATCH_ORDERS = this.cleanupPatches(PRODUCT_DETAIL_PAGE_PATCH_ORDERS);
BxLogger.info(LOG_TAG, 'PATCH_ORDERS', PATCH_ORDERS.slice(0));
}
/**
* Get patch's signature
@ -1333,30 +1334,4 @@ export class PatcherCache {
// Save to storage
window.localStorage.setItem(this.KEY_CACHE, JSON.stringify(this.CACHE));
}
init() {
if (this.isInitialized) {
return;
}
this.isInitialized = true;
this.checkSignature();
// Read cache from storage
this.CACHE = JSON.parse(window.localStorage.getItem(this.KEY_CACHE) || '{}');
BxLogger.info(LOG_TAG, this.CACHE);
if (window.location.pathname.includes('/play/')) {
PATCH_ORDERS.push(...PLAYING_PATCH_ORDERS);
} else {
PATCH_ORDERS.push(ENDING_CHUNKS_PATCH_NAME);
}
// Remove cached patches from PATCH_ORDERS & PLAYING_PATCH_ORDERS
PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS);
PLAYING_PATCH_ORDERS = this.cleanupPatches(PLAYING_PATCH_ORDERS);
BxLogger.info(LOG_TAG, PATCH_ORDERS.slice(0));
BxLogger.info(LOG_TAG, PLAYING_PATCH_ORDERS.slice(0));
}
}

View File

@ -7,7 +7,7 @@ import { STATES } from "@/utils/global";
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { BX_FLAGS } from "@/utils/bx-flags";
import { StreamPlayerType, StreamVideoProcessing, VideoRatio } from "@/enums/pref-values";
import { StreamPlayerType, StreamVideoProcessing, VideoPosition, VideoRatio } from "@/enums/pref-values";
export type StreamPlayerOptions = Partial<{
processing: string,
@ -39,7 +39,6 @@ export class StreamPlayer {
private setupVideoElements() {
this.$videoCss = document.getElementById('bx-video-css') as HTMLStyleElement;
if (this.$videoCss) {
this.$usmMatrix = this.$videoCss.querySelector('#bx-filter-usm-matrix') as any;
return;
}
@ -141,6 +140,23 @@ export class StreamPlayer {
$video.dataset.width = width.toString();
$video.dataset.height = height.toString();
// Set position
const $parent = $video.parentElement!;
const position = getPref<VideoPosition>(PrefKey.VIDEO_POSITION);
$parent.style.removeProperty('padding-top');
$parent.dataset.position = position;
if (position === VideoPosition.TOP_HALF || position === VideoPosition.BOTTOM_HALF) {
let padding = Math.floor((window.innerHeight - height) / 4);
if (padding > 0) {
if (position === VideoPosition.BOTTOM_HALF) {
padding *= 3;
}
$parent.style.paddingTop = padding + 'px';
}
}
// Update size
targetWidth = `${width}px`;
targetHeight = `${height}px`;
@ -164,6 +180,8 @@ export class StreamPlayer {
$webGL2Canvas.style.width = targetWidth;
$webGL2Canvas.style.height = targetHeight;
$webGL2Canvas.style.objectFit = targetObjectFit;
$video.dispatchEvent(new Event('resize'));
}
// Update video dimensions

View File

@ -71,7 +71,6 @@ export function updateVideoPlayer() {
streamPlayer.setPlayerType(getPref(PrefKey.VIDEO_PLAYER_TYPE));
streamPlayer.updateOptions(options);
streamPlayer.refreshPlayer();
}
window.addEventListener('resize', updateVideoPlayer);

View File

@ -239,7 +239,7 @@ export class TouchController {
msg = t('touch-control-layout');
}
layoutChanged && Toast.show(msg, layout.name, {html: html});
layoutChanged && Toast.show(msg, layout.name, { html });
window.setTimeout(() => {
// Show gyroscope control in the "More options" dialog if this layout has gyroscope

View File

@ -393,7 +393,7 @@ export class NavigationDialogManager {
}
if (releasedButton === GamepadKey.A) {
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click', {bubbles: true}));
document.activeElement?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
return;
} else if (releasedButton === GamepadKey.B) {
this.hide();

View File

@ -32,7 +32,7 @@ export class RemotePlayDialog extends NavigationDialog {
}
private setupDialog() {
const $fragment = CE('div', {'class': 'bx-remote-play-container'});
const $fragment = CE('div', { class: 'bx-remote-play-container' });
const $settingNote = CE('p', {});

View File

@ -12,7 +12,7 @@ import { STATES, AppInterface, deepClone, SCRIPT_VERSION, STORAGE, SCRIPT_VARIAN
import { t, Translations } from "@/utils/translation";
import { BxSelectElement } from "@/web-components/bx-select";
import { setNearby } from "@/utils/navigation-utils";
import { PatcherCache } from "@/modules/patcher";
import { PatcherCache } from "@/modules/patcher/patcher";
import { UserAgentProfile } from "@/enums/user-agent";
import { UserAgent } from "@/utils/user-agent";
import { BX_FLAGS } from "@/utils/bx-flags";
@ -231,8 +231,9 @@ export class SettingsDialog extends NavigationDialog {
items: [
PrefKey.NATIVE_MKB_MODE,
{
pref: PrefKey.FORCE_NATIVE_MKB_GAMES,
pref: PrefKey.NATIVE_MKB_FORCED_GAMES,
multiLines: true,
note: CE('a', { href: 'https://github.com/redphx/better-xcloud/discussions/574', target: '_blank' }, t('unofficial-game-list')),
},
PrefKey.MKB_ENABLED,
@ -475,6 +476,9 @@ export class SettingsDialog extends NavigationDialog {
}, {
pref: PrefKey.VIDEO_RATIO,
onChange: updateVideoPlayer,
}, {
pref: PrefKey.VIDEO_POSITION,
onChange: updateVideoPlayer,
}, {
pref: PrefKey.VIDEO_SHARPNESS,
onChange: updateVideoPlayer,
@ -855,7 +859,7 @@ export class SettingsDialog extends NavigationDialog {
setting.options[value] = label;
const $option = CE<HTMLOptionElement>('option', {value: value}, label);
const $option = CE<HTMLOptionElement>('option', { value }, label);
const continent = continents[region.contintent];
if (!continent.children) {
continent.children = [];

View File

@ -28,7 +28,7 @@ export class GameTile {
}
if (typeof totalWaitTime === 'number' && isElementVisible($elm)) {
const $div = CE('div', {'class': 'bx-game-tile-wait-time'},
const $div = CE('div', { class: 'bx-game-tile-wait-time' },
createSvgIcon(BxIcon.PLAYTIME),
CE('span', {}, secondsToHms(totalWaitTime)),
);

View File

@ -40,7 +40,7 @@ export class ProductDetailsPage {
$container.parentElement.appendChild(CE('div', {
class: 'bx-product-details-buttons',
},
BX_FLAGS.DeviceInfo.deviceType === 'android' && ProductDetailsPage.$btnShortcut,
['android-handheld', 'android'].includes(BX_FLAGS.DeviceInfo.deviceType) && ProductDetailsPage.$btnShortcut,
ProductDetailsPage.$btnWallpaper,
));
}

View File

@ -11,6 +11,7 @@ import { getPref } from "./settings-storages/global-settings-storage";
import { GamePassCloudGallery } from "@/enums/game-pass-gallery";
import { TouchController } from "@/modules/touch-controller";
import { NativeMkbMode, TouchControllerMode } from "@/enums/pref-values";
import { Patcher, type PatchPage } from "@/modules/patcher/patcher";
export enum SupportedInputType {
CONTROLLER = 'Controller',
@ -208,5 +209,10 @@ export const BxExposed = {
/ /g,
],
toggleLocalCoOp: (enable: boolean) => {},
toggleLocalCoOp(enable: boolean) {},
beforePageLoad: isFullVersion() ? (page: PatchPage) => {
BxLogger.info('beforePageLoad', page);
Patcher.patchPage(page);
} : () => {},
};

View File

@ -300,7 +300,7 @@ export function renderPresetsList<T extends PresetRecord>($select: HTMLSelectEle
const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
export function humanFileSize(size: number) {
const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return (size / Math.pow(1024, i)).toFixed(2) + ' ' + FILE_SIZE_UNITS[i];
return (size / Math.pow(1024, i)).toFixed(1) + ' ' + FILE_SIZE_UNITS[i];
}
export function secondsToHm(seconds: number) {

View File

@ -26,6 +26,10 @@ export class ControllerSettingsTable extends BaseLocalTable<ControllerSettingsRe
const results: { [key: string]: ControllerSettingsRecord['data'] } = {};
for (const key in all) {
if (!all[key]) {
continue;
}
const settings = all[key].data;
// Pre-calculate virabtionIntensity
settings.vibrationIntensity /= 100;

View File

@ -20,7 +20,7 @@ export class ScreenshotManager {
this.$download = CE<HTMLAnchorElement>('a');
this.$canvas = CE<HTMLCanvasElement>('canvas', {'class': 'bx-gone'});
this.$canvas = CE<HTMLCanvasElement>('canvas', { class: 'bx-gone' });
this.canvasContext = this.$canvas.getContext('2d', {
alpha: false,
willReadFrequently: false,
@ -59,8 +59,11 @@ export class ScreenshotManager {
return;
}
$player.parentElement!.addEventListener('animationend', this.onAnimationEnd, { once: true });
$player.parentElement!.classList.add('bx-taking-screenshot');
const $gameStream = $player.closest('#game-stream');
if ($gameStream) {
$gameStream.addEventListener('animationend', this.onAnimationEnd, { once: true });
$gameStream.classList.add('bx-taking-screenshot');
}
const canvasContext = this.canvasContext;

View File

@ -41,7 +41,7 @@ export class SettingElement {
for (let value in setting.options) {
const label = setting.options[value];
const $option = CE<HTMLOptionElement>('option', {value: value}, label);
const $option = CE<HTMLOptionElement>('option', { value }, label);
$parent.appendChild($option);
}
@ -68,13 +68,14 @@ export class SettingElement {
tabindex: 0,
});
const size = params.size ? params.size : Object.keys(setting.multipleOptions!).length;
const totalOptions = Object.keys(setting.multipleOptions!).length;
const size = params.size ? Math.min(params.size, totalOptions) : totalOptions;
$control.setAttribute('size', size.toString());
for (let value in setting.multipleOptions) {
for (const value in setting.multipleOptions) {
const label = setting.multipleOptions[value];
const $option = CE<HTMLOptionElement>('option', {value: value}, label) as HTMLOptionElement;
const $option = CE<HTMLOptionElement>('option', { value }, label) as HTMLOptionElement;
$option.selected = currentValue.indexOf(value) > -1;
$option.addEventListener('mousedown', function(e) {

View File

@ -8,7 +8,7 @@ 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 } from "@/enums/pref-values";
import { CodecProfile, StreamResolution, TouchControllerMode, TouchControllerStyleStandard, TouchControllerStyleCustom, GameBarPosition, DeviceVibrationMode, NativeMkbMode, UiLayout, UiSection, StreamPlayerType, StreamVideoProcessing, VideoRatio, StreamStat, VideoPosition } 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";
@ -424,7 +424,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
},
},
[PrefKey.FORCE_NATIVE_MKB_GAMES]: {
[PrefKey.NATIVE_MKB_FORCED_GAMES]: {
label: t('force-native-mkb-games'),
default: [],
unsupported: !AppInterface && UserAgent.isMobile(),
@ -437,6 +437,9 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
});
}
},
params: {
size: 6,
},
},
[PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY]: {
@ -681,10 +684,10 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
},
[PrefKey.VIDEO_RATIO]: {
label: t('aspect-ratio'),
note: t('aspect-ratio-note'),
note: STATES.browser.capabilities.touch ? t('aspect-ratio-note') : undefined,
default: VideoRatio['16:9'],
options: {
[VideoRatio['16:9']]: '16:9',
[VideoRatio['16:9']]: `16:9 (${t('default')})`,
[VideoRatio['18:9']]: '18:9',
[VideoRatio['21:9']]: '21:9',
[VideoRatio['16:10']]: '16:10',
@ -694,6 +697,19 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
//'cover': 'Cover',
},
},
[PrefKey.VIDEO_POSITION]: {
label: t('position'),
note: STATES.browser.capabilities.touch ? t('aspect-ratio-note') : undefined,
default: VideoPosition.CENTER,
options: {
[VideoPosition.TOP]: t('top'),
[VideoPosition.TOP_HALF]: t('top-half'),
[VideoPosition.CENTER]: `${t('center')} (${t('default')})`,
[VideoPosition.BOTTOM_HALF]: t('bottom-half'),
[VideoPosition.BOTTOM]: t('bottom'),
},
},
[PrefKey.VIDEO_SATURATION]: {
label: t('saturation'),
default: 100,
@ -746,7 +762,6 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
},
},
[PrefKey.STATS_ITEMS]: {
label: t('stats'),
default: [StreamStat.PING, StreamStat.FPS, StreamStat.BITRATE, StreamStat.DECODE_TIME, StreamStat.PACKETS_LOST, StreamStat.FRAMES_LOST],

View File

@ -104,7 +104,7 @@ export class StreamStatsCollector {
current: 0,
grades: [30, 40, 60],
toString() {
return `${this.current.toFixed(2)}ms`;
return `${this.current.toFixed(1)}ms`;
},
},
@ -119,7 +119,7 @@ export class StreamStatsCollector {
[StreamStat.BITRATE]: {
current: 0,
toString() {
return `${this.current.toFixed(2)} Mbps`;
return `${this.current.toFixed(1)} Mbps`;
},
},
@ -127,7 +127,7 @@ export class StreamStatsCollector {
received: 0,
dropped: 0,
toString() {
const framesDroppedPercentage = (this.dropped * 100 / ((this.dropped + this.received) || 1)).toFixed(2);
const framesDroppedPercentage = (this.dropped * 100 / ((this.dropped + this.received) || 1)).toFixed(1);
return framesDroppedPercentage === '0.00' ? this.dropped.toString() : `${this.dropped} (${framesDroppedPercentage}%)`;
},
},
@ -136,7 +136,7 @@ export class StreamStatsCollector {
received: 0,
dropped: 0,
toString() {
const packetsLostPercentage = (this.dropped * 100 / ((this.dropped + this.received) || 1)).toFixed(2);
const packetsLostPercentage = (this.dropped * 100 / ((this.dropped + this.received) || 1)).toFixed(1);
return packetsLostPercentage === '0.00' ? this.dropped.toString() : `${this.dropped} (${packetsLostPercentage}%)`;
},
},
@ -146,7 +146,7 @@ export class StreamStatsCollector {
total: 0,
grades: [6, 9, 12],
toString() {
return isNaN(this.current) ? '??ms' : `${this.current.toFixed(2)}ms`;
return isNaN(this.current) ? '??ms' : `${this.current.toFixed(1)}ms`;
},
},

View File

@ -47,6 +47,8 @@ const Texts = {
"better-xcloud": "Better xCloud",
"bitrate-audio-maximum": "Maximum audio bitrate",
"bitrate-video-maximum": "Maximum video bitrate",
"bottom": "Bottom",
"bottom-half": "Bottom half",
"bottom-left": "Bottom-left",
"bottom-right": "Bottom-right",
"brazil": "Brazil",
@ -56,6 +58,7 @@ const Texts = {
"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",
"clarity-boost": "Clarity boost",
"clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",
"clear": "Clear",
@ -339,7 +342,9 @@ const Texts = {
"tc-standard-layout-style": "Standard layout's button style",
"text-size": "Text size",
"toggle": "Toggle",
"top": "Top",
"top-center": "Top-center",
"top-half": "Top half",
"top-left": "Top-left",
"top-right": "Top-right",
"touch-control-layout": "Touch control layout",

View File

@ -9,12 +9,12 @@ type UserAgentConfig = {
const SMART_TV_UNIQUE_ID = 'FC4A1DA2-711C-4E9C-BC7F-047AF8A672EA';
let CHROMIUM_VERSION = '123.0.0.0';
let CHROMIUM_VERSION = '125.0.0.0';
if (!!(window as any).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) {
CHROMIUM_VERSION = match[1];
CHROMIUM_VERSION = match[1] as string;
}
}

View File

@ -128,7 +128,7 @@ export function parseDetailsPath(path: string) {
return;
}
const titleSlug = matches.groups.titleSlug.replaceAll('\%' + '7C', '-');
const titleSlug = matches.groups.titleSlug!.replaceAll('\%' + '7C', '-');
const productId = matches.groups.productId;
return { titleSlug, productId };

View File

@ -3,7 +3,7 @@ import { NATIVE_FETCH } from "./bx-flags"
export class XboxApi {
private static CACHED_TITLES: Record<string, string> = {};
static async getProductTitle(xboxTitleId: number | string): Promise<string | null> {
static async getProductTitle(xboxTitleId: number | string): Promise<string | undefined> {
xboxTitleId = xboxTitleId.toString();
if (XboxApi.CACHED_TITLES[xboxTitleId]) {
return XboxApi.CACHED_TITLES[xboxTitleId];
@ -20,6 +20,6 @@ export class XboxApi {
return productTitle;
} catch (e) {}
return null;
return;
}
}

View File

@ -14,14 +14,14 @@ export class XcloudApi {
BxLogger.info(this.LOG_TAG, 'constructor()');
}
async getTitleInfo(id: string): Promise<XcloudTitleInfo | null> {
async getTitleInfo(id: string): Promise<XcloudTitleInfo | undefined> {
if (id in this.CACHE_TITLES) {
return this.CACHE_TITLES[id];
}
const baseUri = STATES.selectedRegion.baseUri;
if (!baseUri || !STATES.gsToken) {
return null;
return;
}
let json;

View File

@ -116,7 +116,7 @@ export class XcloudInterceptor {
let match = serverRegex.exec(region.baseUri);
if (match) {
shortName = match[1];
shortName = match[1] as string;
if (serverExtra[regionName]) {
shortName = serverExtra[regionName][0] + ' ' + shortName;
region.contintent = serverExtra[regionName][1];
@ -155,10 +155,10 @@ export class XcloudInterceptor {
const url = (typeof request === 'string') ? request : (request as Request).url;
const parsedUrl = new URL(url);
let badgeRegion: string = parsedUrl.host.split('.', 1)[0];
let badgeRegion: string = parsedUrl.host.split('.', 1)[0] as string;
for (let regionName in STATES.serverRegions) {
const region = STATES.serverRegions[regionName];
if (parsedUrl.origin == region.baseUri) {
if (region && parsedUrl.origin === region.baseUri) {
badgeRegion = regionName;
break;
}

View File

@ -90,7 +90,7 @@ export class XhomeInterceptor {
XhomeInterceptor.consoleAddrs = {};
for (const pair of pairs) {
const [keyAddr, keyPort] = pair;
if (serverDetails[keyAddr]) {
if (keyAddr && keyPort && serverDetails[keyAddr]) {
const port = serverDetails[keyPort];
// Add port 9002 to the list of ports
const ports = new Set<number>();

View File

@ -260,6 +260,9 @@ export class BxSelectElement extends HTMLSelectElement {
for (let i = 0; i < optionsList.length; i++) {
const $option = optionsList[i];
const $indicator = indicatorsList[i];
if (!$option || !$indicator) {
continue;
}
clearDataSet($indicator);
if ($option.selected) {
@ -288,7 +291,7 @@ export class BxSelectElement extends HTMLSelectElement {
visibleIndex: currentIndex,
} = this;
const goNext = (e.target as any).closest('button') === $btnNext;
const goNext = (e.target as HTMLElement).closest('button') === $btnNext;
let newIndex = goNext ? currentIndex + 1 : currentIndex - 1;
if (newIndex > this.optionsList.length - 1) {