Compare commits

...

11 Commits

Author SHA1 Message Date
fa19a5a68e Bump version to 5.8.5 2024-10-15 19:48:18 +07:00
3f834f74b6 Update "skipFeedbackDialog" patch 2024-10-15 16:49:38 +07:00
749d5d720e Update dist 2024-10-14 21:08:35 +07:00
b969d52a3c Show max FPS value in Stats bar 2024-10-14 21:06:52 +07:00
e5bd7e64a7 Refactor xCloud & xHome interceptors 2024-10-14 20:08:47 +07:00
82ee00b4ae Update dist 2024-10-14 17:17:32 +07:00
8e88af5f8c Set indent of built scripts to 1 space 2024-10-14 17:14:43 +07:00
927eae3f2f Refactor getInstance() methods 2024-10-14 16:56:05 +07:00
9f440e9cf4 Don't call animate() when hiding renderer 2024-10-14 16:47:03 +07:00
1acb30e3af Refactor Game Bar 2024-10-14 16:45:57 +07:00
34159fad22 Update better-xcloud.lite.user.js 2024-10-13 20:04:42 +07:00
31 changed files with 12805 additions and 12989 deletions

View File

@ -87,6 +87,15 @@ const postProcess = (str: string): string => {
return p1.toUpperCase(); return p1.toUpperCase();
}); });
// Replace " (e) =>" to " e =>"
// str = str.replaceAll(/ \(([^\s,.$()]+)\) =>/g, ' $1 =>');
// Set indent to 1 space
str = str.replaceAll(/\n(\s+)/g, (match, p1) => {
const len = p1.length / 2;
return '\n' + ' '.repeat(len);
});
assert(str.includes('/* ADDITIONAL CODE */')); assert(str.includes('/* ADDITIONAL CODE */'));
assert(str.includes('window.BX_EXPOSED = BxExposed')); assert(str.includes('window.BX_EXPOSED = BxExposed'));
assert(str.includes('window.BxEvent = BxEvent')); assert(str.includes('window.BxEvent = BxEvent'));

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,8 @@
import { BxEvent } from "@/utils/bx-event"; import { BxEvent } from "@/utils/bx-event";
export abstract class BaseGameBarAction { export abstract class BaseGameBarAction {
abstract $content: HTMLElement;
constructor() {} constructor() {}
reset() {} reset() {}
@ -8,5 +10,7 @@ export abstract class BaseGameBarAction {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
}; };
abstract render(): HTMLElement; render(): HTMLElement {
return this.$content;
};
} }

View File

@ -8,8 +8,6 @@ import { MicrophoneShortcut, MicrophoneState } from "../shortcuts/shortcut-micro
export class MicrophoneAction extends BaseGameBarAction { export class MicrophoneAction extends BaseGameBarAction {
$content: HTMLElement; $content: HTMLElement;
visible: boolean = false;
constructor() { constructor() {
super(); super();
@ -26,12 +24,7 @@ export class MicrophoneAction extends BaseGameBarAction {
onClick: this.onClick.bind(this), onClick: this.onClick.bind(this),
}); });
this.$content = CE('div', {}, this.$content = CE('div', {}, $btnMuted, $btnDefault);
$btnMuted,
$btnDefault,
);
this.reset();
window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, e => { window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, e => {
const microphoneState = (e as any).microphoneState; const microphoneState = (e as any).microphoneState;
@ -49,12 +42,7 @@ export class MicrophoneAction extends BaseGameBarAction {
this.$content.dataset.activated = enabled.toString(); this.$content.dataset.activated = enabled.toString();
} }
render(): HTMLElement {
return this.$content;
}
reset(): void { reset(): void {
this.visible = false;
this.$content.classList.add('bx-gone'); this.$content.classList.add('bx-gone');
this.$content.dataset.activated = 'false'; this.$content.dataset.activated = 'false';
} }

View File

@ -23,12 +23,7 @@ export class RendererAction extends BaseGameBarAction {
classes: ['bx-activated'], classes: ['bx-activated'],
}); });
this.$content = CE('div', {}, this.$content = CE('div', {}, $btnDefault, $btnActivated);
$btnDefault,
$btnActivated,
);
this.reset();
} }
onClick(e: Event) { onClick(e: Event) {
@ -37,10 +32,6 @@ export class RendererAction extends BaseGameBarAction {
this.$content.dataset.activated = (!isVisible).toString(); this.$content.dataset.activated = (!isVisible).toString();
} }
render(): HTMLElement {
return this.$content;
}
reset(): void { reset(): void {
this.$content.dataset.activated = 'false'; this.$content.dataset.activated = 'false';
} }

View File

@ -22,8 +22,4 @@ export class ScreenshotAction extends BaseGameBarAction {
super.onClick(e); super.onClick(e);
Screenshot.takeScreenshot(); Screenshot.takeScreenshot();
} }
render(): HTMLElement {
return this.$content;
}
} }

View File

@ -24,12 +24,7 @@ export class SpeakerAction extends BaseGameBarAction {
classes: ['bx-activated'], classes: ['bx-activated'],
}); });
this.$content = CE('div', {}, this.$content = CE('div', {}, $btnEnable, $btnMuted);
$btnEnable,
$btnMuted,
);
this.reset();
window.addEventListener(BxEvent.SPEAKER_STATE_CHANGED, e => { window.addEventListener(BxEvent.SPEAKER_STATE_CHANGED, e => {
const speakerState = (e as any).speakerState; const speakerState = (e as any).speakerState;
@ -44,10 +39,6 @@ export class SpeakerAction extends BaseGameBarAction {
SoundShortcut.muteUnmute(); SoundShortcut.muteUnmute();
} }
render(): HTMLElement {
return this.$content;
}
reset(): void { reset(): void {
this.$content.dataset.activated = 'false'; this.$content.dataset.activated = 'false';
} }

View File

@ -25,12 +25,7 @@ export class TouchControlAction extends BaseGameBarAction {
classes: ['bx-activated'], classes: ['bx-activated'],
}); });
this.$content = CE('div', {}, this.$content = CE('div', {}, $btnEnable, $btnDisable);
$btnEnable,
$btnDisable,
);
this.reset();
} }
onClick(e: Event) { onClick(e: Event) {
@ -39,10 +34,6 @@ export class TouchControlAction extends BaseGameBarAction {
this.$content.dataset.activated = (!isVisible).toString(); this.$content.dataset.activated = (!isVisible).toString();
} }
render(): HTMLElement {
return this.$content;
}
reset(): void { reset(): void {
this.$content.dataset.activated = 'false'; this.$content.dataset.activated = 'false';
} }

View File

@ -1,6 +1,5 @@
import { BxIcon } from "@/utils/bx-icon"; import { BxIcon } from "@/utils/bx-icon";
import { createButton, ButtonStyle } from "@/utils/html"; import { createButton, ButtonStyle } from "@/utils/html";
import { t } from "@/utils/translation";
import { BaseGameBarAction } from "./action-base"; import { BaseGameBarAction } from "./action-base";
import { TrueAchievements } from "@/utils/true-achievements"; import { TrueAchievements } from "@/utils/true-achievements";
@ -13,7 +12,6 @@ export class TrueAchievementsAction extends BaseGameBarAction {
this.$content = createButton({ this.$content = createButton({
style: ButtonStyle.GHOST, style: ButtonStyle.GHOST,
icon: BxIcon.TRUE_ACHIEVEMENTS, icon: BxIcon.TRUE_ACHIEVEMENTS,
title: t('true-achievements'),
onClick: this.onClick.bind(this), onClick: this.onClick.bind(this),
}); });
} }
@ -22,8 +20,4 @@ export class TrueAchievementsAction extends BaseGameBarAction {
super.onClick(e); super.onClick(e);
TrueAchievements.open(false); TrueAchievements.open(false);
} }
render(): HTMLElement {
return this.$content;
}
} }

View File

@ -7,7 +7,7 @@ import type { BaseGameBarAction } from "./action-base";
import { STATES } from "@utils/global"; import { STATES } from "@utils/global";
import { MicrophoneAction } from "./action-microphone"; import { MicrophoneAction } from "./action-microphone";
import { PrefKey } from "@/enums/pref-keys"; import { PrefKey } from "@/enums/pref-keys";
import { getPref, StreamTouchController } from "@/utils/settings-storages/global-settings-storage"; import { getPref, StreamTouchController, type GameBarPosition } from "@/utils/settings-storages/global-settings-storage";
import { TrueAchievementsAction } from "./action-true-achievements"; import { TrueAchievementsAction } from "./action-true-achievements";
import { SpeakerAction } from "./action-speaker"; import { SpeakerAction } from "./action-speaker";
import { RendererAction } from "./action-renderer"; import { RendererAction } from "./action-renderer";
@ -15,13 +15,7 @@ import { RendererAction } from "./action-renderer";
export class GameBar { export class GameBar {
private static instance: GameBar; private static instance: GameBar;
public static getInstance(): GameBar { public static getInstance = () => GameBar.instance ?? (GameBar.instance = new GameBar());
if (!GameBar.instance) {
GameBar.instance = new GameBar();
}
return GameBar.instance;
}
private static readonly VISIBLE_DURATION = 2000; private static readonly VISIBLE_DURATION = 2000;
@ -35,12 +29,12 @@ export class GameBar {
private constructor() { private constructor() {
let $container; let $container;
const position = getPref(PrefKey.GAME_BAR_POSITION); const position = getPref(PrefKey.GAME_BAR_POSITION) as GameBarPosition;
const $gameBar = CE('div', {id: 'bx-game-bar', class: 'bx-gone', 'data-position': position}, const $gameBar = CE('div', {id: 'bx-game-bar', class: 'bx-gone', 'data-position': position},
$container = CE('div', {class: 'bx-game-bar-container bx-offscreen'}), $container = CE('div', {class: 'bx-game-bar-container bx-offscreen'}),
createSvgIcon(position === 'bottom-left' ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT), createSvgIcon(position === 'bottom-left' ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT),
); );
this.actions = [ this.actions = [
new ScreenshotAction(), new ScreenshotAction(),
@ -78,11 +72,7 @@ export class GameBar {
// Add animation when hiding game bar // Add animation when hiding game bar
$container.addEventListener('transitionend', e => { $container.addEventListener('transitionend', e => {
const classList = $container.classList; $container.classList.replace('bx-hide', 'bx-offscreen');
if (classList.contains('bx-hide')) {
classList.remove('bx-hide');
classList.add('bx-offscreen');
}
}); });
document.documentElement.appendChild($gameBar); document.documentElement.appendChild($gameBar);
@ -106,9 +96,9 @@ export class GameBar {
this.clearHideTimeout(); this.clearHideTimeout();
this.timeoutId = window.setTimeout(() => { this.timeoutId = window.setTimeout(() => {
this.timeoutId = null; this.timeoutId = null;
this.hideBar(); this.hideBar();
}, GameBar.VISIBLE_DURATION); }, GameBar.VISIBLE_DURATION);
} }
private clearHideTimeout() { private clearHideTimeout() {
@ -117,19 +107,15 @@ export class GameBar {
} }
enable() { enable() {
this.$gameBar && this.$gameBar.classList.remove('bx-gone'); this.$gameBar.classList.remove('bx-gone');
} }
disable() { disable() {
this.hideBar(); this.hideBar();
this.$gameBar && this.$gameBar.classList.add('bx-gone'); this.$gameBar.classList.add('bx-gone');
} }
showBar() { showBar() {
if (!this.$container) {
return;
}
this.$container.classList.remove('bx-offscreen', 'bx-hide' , 'bx-gone'); this.$container.classList.remove('bx-offscreen', 'bx-hide' , 'bx-gone');
this.$container.classList.add('bx-show'); this.$container.classList.add('bx-show');
@ -142,18 +128,11 @@ export class GameBar {
// Stop focusing Game Bar // Stop focusing Game Bar
clearFocus(); clearFocus();
if (!this.$container) { this.$container.classList.replace('bx-show', 'bx-hide');
return;
}
this.$container.classList.remove('bx-show');
this.$container.classList.add('bx-hide');
} }
// Reset all states // Reset all states
reset() { reset() {
for (const action of this.actions) { this.actions.forEach(action => action.reset());
action.reset();
}
} }
} }

View File

@ -124,14 +124,8 @@ This class uses some code from Yuzu emulator to handle mouse's movements
Source: https://github.com/yuzu-emu/yuzu-mainline/blob/master/src/input_common/drivers/mouse.cpp Source: https://github.com/yuzu-emu/yuzu-mainline/blob/master/src/input_common/drivers/mouse.cpp
*/ */
export class EmulatedMkbHandler extends MkbHandler { export class EmulatedMkbHandler extends MkbHandler {
static #instance: EmulatedMkbHandler; private static instance: EmulatedMkbHandler;
public static getInstance(): EmulatedMkbHandler { public static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler());
if (!EmulatedMkbHandler.#instance) {
EmulatedMkbHandler.#instance = new EmulatedMkbHandler();
}
return EmulatedMkbHandler.#instance;
}
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);

View File

@ -23,6 +23,8 @@ type XcloudInputSink = {
export class NativeMkbHandler extends MkbHandler { export class NativeMkbHandler extends MkbHandler {
private static instance: NativeMkbHandler; private static instance: NativeMkbHandler;
public static getInstance = () => NativeMkbHandler.instance ?? (NativeMkbHandler.instance = new NativeMkbHandler());
#pointerClient: PointerClient | undefined; #pointerClient: PointerClient | undefined;
#enabled: boolean = false; #enabled: boolean = false;
@ -37,14 +39,6 @@ export class NativeMkbHandler extends MkbHandler {
#$message?: HTMLElement; #$message?: HTMLElement;
public static getInstance(): NativeMkbHandler {
if (!NativeMkbHandler.instance) {
NativeMkbHandler.instance = new NativeMkbHandler();
}
return NativeMkbHandler.instance;
}
#onKeyboardEvent(e: KeyboardEvent) { #onKeyboardEvent(e: KeyboardEvent) {
if (e.type === 'keyup' && e.code === 'F8') { if (e.type === 'keyup' && e.code === 'F8') {
e.preventDefault(); e.preventDefault();

View File

@ -15,13 +15,7 @@ enum PointerAction {
export class PointerClient { export class PointerClient {
private static instance: PointerClient; private static instance: PointerClient;
public static getInstance(): PointerClient { public static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient());
if (!PointerClient.instance) {
PointerClient.instance = new PointerClient();
}
return PointerClient.instance;
}
private socket: WebSocket | undefined | null; private socket: WebSocket | undefined | null;
private mkbHandler: MkbHandler | undefined; private mkbHandler: MkbHandler | undefined;

View File

@ -632,12 +632,12 @@ true` + text;
}, },
skipFeedbackDialog(str: string) { skipFeedbackDialog(str: string) {
let text = '&&this.shouldTransitionToFeedback('; let text = 'shouldTransitionToFeedback(e){';
if (!str.includes(text)) { if (!str.includes(text)) {
return false; return false;
} }
str = str.replace(text, '&& false ' + text); str = str.replace(text, text + 'return !1;');
return str; return str;
}, },

View File

@ -26,7 +26,7 @@ export class WebGL2Player {
}; };
private targetFps = 60; private targetFps = 60;
private frameInterval = Math.ceil(1000 / this.targetFps); private frameInterval = 0;
private lastFrameTime = 0; private lastFrameTime = 0;
private animFrameId: number | null = null; private animFrameId: number | null = null;
@ -73,7 +73,8 @@ export class WebGL2Player {
setTargetFps(target: number) { setTargetFps(target: number) {
this.targetFps = target; this.targetFps = target;
this.frameInterval = Math.ceil(1000 / target); this.lastFrameTime = 0;
this.frameInterval = target ? Math.floor(1000 / target) : 0;
} }
getCanvas() { getCanvas() {
@ -94,6 +95,11 @@ export class WebGL2Player {
} }
drawFrame() { drawFrame() {
// Don't draw when FPS is 0
if (this.targetFps === 0) {
return;
}
// Limit FPS // Limit FPS
if (this.targetFps < 60) { if (this.targetFps < 60) {
const currentTime = performance.now(); const currentTime = performance.now();
@ -233,10 +239,10 @@ export class WebGL2Player {
const gl = this.gl; const gl = this.gl;
if (gl) { if (gl) {
gl.getExtension('WEBGL_lose_context')?.loseContext(); gl.getExtension('WEBGL_lose_context')?.loseContext();
gl.useProgram(null);
for (const resource of this.resources) { for (const resource of this.resources) {
if (resource instanceof WebGLProgram) { if (resource instanceof WebGLProgram) {
gl.useProgram(null);
gl.deleteProgram(resource); gl.deleteProgram(resource);
} else if (resource instanceof WebGLShader) { } else if (resource instanceof WebGLShader) {
gl.deleteShader(resource); gl.deleteShader(resource);

View File

@ -37,13 +37,7 @@ type RemotePlayConsole = {
export class RemotePlayManager { export class RemotePlayManager {
private static instance: RemotePlayManager; private static instance: RemotePlayManager;
public static getInstance(): RemotePlayManager { public static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager());
if (!this.instance) {
this.instance = new RemotePlayManager();
}
return this.instance;
}
private isInitialized = false; private isInitialized = false;

View File

@ -1,3 +1,7 @@
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { limitVideoPlayerFps } from "../stream/stream-settings-utils";
export class RendererShortcut { export class RendererShortcut {
static toggleVisibility(): boolean { static toggleVisibility(): boolean {
const $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]'); const $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]');
@ -6,6 +10,9 @@ export class RendererShortcut {
} }
$mediaContainer.classList.toggle('bx-gone'); $mediaContainer.classList.toggle('bx-gone');
return !$mediaContainer.classList.contains('bx-gone'); const isShowing = !$mediaContainer.classList.contains('bx-gone');
// Switch FPS
limitVideoPlayerFps(isShowing ? getPref(PrefKey.VIDEO_MAX_FPS) : 0);
return isShowing;
} }
} }

View File

@ -50,13 +50,7 @@ enum StreamBadge {
export class StreamBadges { export class StreamBadges {
private static instance: StreamBadges; private static instance: StreamBadges;
public static getInstance(): StreamBadges { public static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges());
if (!StreamBadges.instance) {
StreamBadges.instance = new StreamBadges();
}
return StreamBadges.instance;
}
private serverInfo: StreamServerInfo = {}; private serverInfo: StreamServerInfo = {};

View File

@ -45,8 +45,7 @@ export function onChangeVideoPlayerType() {
} }
export function limitVideoPlayerFps() { export function limitVideoPlayerFps(targetFps: number) {
const targetFps = getPref(PrefKey.VIDEO_MAX_FPS);
const streamPlayer = STATES.currentStream.streamPlayer; const streamPlayer = STATES.currentStream.streamPlayer;
streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps); streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);
} }
@ -58,7 +57,7 @@ export function updateVideoPlayer() {
return; return;
} }
limitVideoPlayerFps(); limitVideoPlayerFps(getPref(PrefKey.VIDEO_MAX_FPS));
const options = { const options = {
processing: getPref(PrefKey.VIDEO_PROCESSING), processing: getPref(PrefKey.VIDEO_PROCESSING),

View File

@ -9,13 +9,7 @@ import { StreamStat, StreamStatsCollector, type StreamStatGrade } from "@/utils/
export class StreamStats { export class StreamStats {
private static instance: StreamStats; private static instance: StreamStats;
public static getInstance(): StreamStats { public static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats());
if (!StreamStats.instance) {
StreamStats.instance = new StreamStats();
}
return StreamStats.instance;
}
private intervalId?: number | null; private intervalId?: number | null;
private readonly REFRESH_INTERVAL = 1 * 1000; private readonly REFRESH_INTERVAL = 1 * 1000;

View File

@ -88,12 +88,7 @@ export abstract class NavigationDialog {
export class NavigationDialogManager { export class NavigationDialogManager {
private static instance: NavigationDialogManager; private static instance: NavigationDialogManager;
public static getInstance(): NavigationDialogManager { public static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager());
if (!NavigationDialogManager.instance) {
NavigationDialogManager.instance = new NavigationDialogManager();
}
return NavigationDialogManager.instance;
}
private static readonly GAMEPAD_POLLING_INTERVAL = 50; private static readonly GAMEPAD_POLLING_INTERVAL = 50;
private static readonly GAMEPAD_KEYS = [ private static readonly GAMEPAD_KEYS = [

View File

@ -11,12 +11,7 @@ import { BxEvent } from "@/utils/bx-event";
export class RemotePlayNavigationDialog extends NavigationDialog { export class RemotePlayNavigationDialog extends NavigationDialog {
private static instance: RemotePlayNavigationDialog; private static instance: RemotePlayNavigationDialog;
public static getInstance(): RemotePlayNavigationDialog { public static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog());
if (!RemotePlayNavigationDialog.instance) {
RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog();
}
return RemotePlayNavigationDialog.instance;
}
private readonly STATE_LABELS: Record<RemotePlayConsoleState, string> = { private readonly STATE_LABELS: Record<RemotePlayConsoleState, string> = {
[RemotePlayConsoleState.ON]: t('powered-on'), [RemotePlayConsoleState.ON]: t('powered-on'),

View File

@ -64,12 +64,7 @@ type SettingTab = {
export class SettingsNavigationDialog extends NavigationDialog { export class SettingsNavigationDialog extends NavigationDialog {
private static instance: SettingsNavigationDialog; private static instance: SettingsNavigationDialog;
public static getInstance(): SettingsNavigationDialog { public static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog());
if (!SettingsNavigationDialog.instance) {
SettingsNavigationDialog.instance = new SettingsNavigationDialog();
}
return SettingsNavigationDialog.instance;
}
$container!: HTMLElement; $container!: HTMLElement;
private $tabs!: HTMLElement; private $tabs!: HTMLElement;
@ -409,7 +404,9 @@ export class SettingsNavigationDialog extends NavigationDialog {
onChange: onChangeVideoPlayerType, onChange: onChangeVideoPlayerType,
}, { }, {
pref: PrefKey.VIDEO_MAX_FPS, pref: PrefKey.VIDEO_MAX_FPS,
onChange: limitVideoPlayerFps, onChange: e => {
limitVideoPlayerFps(parseInt(e.target.value));
},
}, { }, {
pref: PrefKey.VIDEO_POWER_PREFERENCE, pref: PrefKey.VIDEO_POWER_PREFERENCE,
onChange: () => { onChange: () => {

View File

@ -2,12 +2,7 @@ import { CE } from "@/utils/html";
export class FullscreenText { export class FullscreenText {
private static instance: FullscreenText; private static instance: FullscreenText;
public static getInstance(): FullscreenText { public static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText());
if (!FullscreenText.instance) {
FullscreenText.instance = new FullscreenText();
}
return FullscreenText.instance;
}
$text: HTMLElement; $text: HTMLElement;

View File

@ -39,6 +39,10 @@ export const enum ControllerDeviceVibration {
} }
export type GameBarPosition = 'bottom-left' | 'bottom-right' | 'off';
export type GameBarPositionOptions = Record<GameBarPosition, string>;
function getSupportedCodecProfiles() { function getSupportedCodecProfiles() {
const options: PartialRecord<CodecProfile, string> = { const options: PartialRecord<CodecProfile, string> = {
default: t('default'), default: t('default'),
@ -323,12 +327,12 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
[PrefKey.GAME_BAR_POSITION]: { [PrefKey.GAME_BAR_POSITION]: {
requiredVariants: 'full', requiredVariants: 'full',
label: t('position'), label: t('position'),
default: 'bottom-left', default: 'bottom-left' satisfies GameBarPosition,
options: { options: {
'bottom-left': t('bottom-left'), 'bottom-left': t('bottom-left'),
'bottom-right': t('bottom-right'), 'bottom-right': t('bottom-right'),
'off': t('off'), 'off': t('off'),
}, } satisfies GameBarPositionOptions,
}, },
[PrefKey.LOCAL_CO_OP_ENABLED]: { [PrefKey.LOCAL_CO_OP_ENABLED]: {

View File

@ -1,6 +1,8 @@
import { PrefKey } from "@/enums/pref-keys";
import { BxEvent } from "./bx-event"; import { BxEvent } from "./bx-event";
import { STATES } from "./global"; import { STATES } from "./global";
import { humanFileSize, secondsToHm } from "./html"; import { humanFileSize, secondsToHm } from "./html";
import { getPref } from "./settings-storages/global-settings-storage";
export enum StreamStat { export enum StreamStat {
PING = 'ping', PING = 'ping',
@ -92,13 +94,7 @@ type CurrentStats = {
export class StreamStatsCollector { export class StreamStatsCollector {
private static instance: StreamStatsCollector; private static instance: StreamStatsCollector;
public static getInstance(): StreamStatsCollector { public static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector());
if (!StreamStatsCollector.instance) {
StreamStatsCollector.instance = new StreamStatsCollector();
}
return StreamStatsCollector.instance;
}
// Collect in background - 60 seconds // Collect in background - 60 seconds
static readonly INTERVAL_BACKGROUND = 60 * 1000; static readonly INTERVAL_BACKGROUND = 60 * 1000;
@ -127,7 +123,8 @@ export class StreamStatsCollector {
[StreamStat.FPS]: { [StreamStat.FPS]: {
current: 0, current: 0,
toString() { toString() {
return this.current.toString(); const maxFps = getPref(PrefKey.VIDEO_MAX_FPS);
return maxFps < 60 ? `${maxFps}/${this.current}` : this.current.toString();
}, },
}, },

View File

@ -3,13 +3,7 @@ import { STATES } from "./global";
export class XcloudApi { export class XcloudApi {
private static instance: XcloudApi; private static instance: XcloudApi;
public static getInstance(): XcloudApi { public static getInstance = () => XcloudApi.instance ?? (XcloudApi.instance = new XcloudApi());
if (!XcloudApi.instance) {
XcloudApi.instance = new XcloudApi();
}
return XcloudApi.instance;
}
private CACHE_TITLES: {[key: string]: XcloudTitleInfo} = {}; private CACHE_TITLES: {[key: string]: XcloudTitleInfo} = {};
private CACHE_WAIT_TIME: {[key: string]: XcloudWaitTimeInfo} = {}; private CACHE_WAIT_TIME: {[key: string]: XcloudWaitTimeInfo} = {};

View File

@ -13,9 +13,25 @@ import { BypassServerIps } from "@/enums/bypass-servers";
import { PrefKey } from "@/enums/pref-keys"; import { PrefKey } from "@/enums/pref-keys";
import { getPref, StreamResolution, StreamTouchController } from "./settings-storages/global-settings-storage"; import { getPref, StreamResolution, StreamTouchController } from "./settings-storages/global-settings-storage";
export export class XcloudInterceptor {
class XcloudInterceptor { private static readonly SERVER_EMOJIS = {
static async #handleLogin(request: RequestInfo | URL, init?: RequestInit) { AustraliaEast: '🇦🇺',
AustraliaSouthEast: '🇦🇺',
BrazilSouth: '🇧🇷',
EastUS: '🇺🇸',
EastUS2: '🇺🇸',
JapanEast: '🇯🇵',
KoreaCentral: '🇰🇷',
MexicoCentral: '🇲🇽',
NorthCentralUs: '🇺🇸',
SouthCentralUS: '🇺🇸',
UKSouth: '🇬🇧',
WestEurope: '🇪🇺',
WestUS: '🇺🇸',
WestUS2: '🇺🇸',
};
private static async handleLogin(request: RequestInfo | URL, init?: RequestInit) {
const bypassServer = getPref(PrefKey.SERVER_BYPASS_RESTRICTION); const bypassServer = getPref(PrefKey.SERVER_BYPASS_RESTRICTION);
if (bypassServer !== 'off') { if (bypassServer !== 'off') {
const ip = BypassServerIps[bypassServer as keyof typeof BypassServerIps]; const ip = BypassServerIps[bypassServer as keyof typeof BypassServerIps];
@ -35,24 +51,8 @@ class XcloudInterceptor {
RemotePlayManager.getInstance().xcloudToken = obj.gsToken; RemotePlayManager.getInstance().xcloudToken = obj.gsToken;
// Get server list // Get server list
const serverEmojis = {
AustraliaEast: '🇦🇺',
AustraliaSouthEast: '🇦🇺',
BrazilSouth: '🇧🇷',
EastUS: '🇺🇸',
EastUS2: '🇺🇸',
JapanEast: '🇯🇵',
KoreaCentral: '🇰🇷',
MexicoCentral: '🇲🇽',
NorthCentralUs: '🇺🇸',
SouthCentralUS: '🇺🇸',
UKSouth: '🇬🇧',
WestEurope: '🇪🇺',
WestUS: '🇺🇸',
WestUS2: '🇺🇸',
};
const serverRegex = /\/\/(\w+)\./; const serverRegex = /\/\/(\w+)\./;
const serverEmojis = XcloudInterceptor.SERVER_EMOJIS;
for (let region of obj.offeringSettings.regions) { for (let region of obj.offeringSettings.regions) {
const regionName = region.name as keyof typeof serverEmojis; const regionName = region.name as keyof typeof serverEmojis;
@ -91,7 +91,7 @@ class XcloudInterceptor {
return response; return response;
} }
static async #handlePlay(request: RequestInfo | URL, init?: RequestInit) { private static async handlePlay(request: RequestInfo | URL, init?: RequestInit) {
const PREF_STREAM_TARGET_RESOLUTION = getPref(PrefKey.STREAM_TARGET_RESOLUTION); const PREF_STREAM_TARGET_RESOLUTION = getPref(PrefKey.STREAM_TARGET_RESOLUTION);
const PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE); const PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE);
@ -129,7 +129,7 @@ class XcloudInterceptor {
return NATIVE_FETCH(newRequest); return NATIVE_FETCH(newRequest);
} }
static async #handleWaitTime(request: RequestInfo | URL, init?: RequestInit) { private static async handleWaitTime(request: RequestInfo | URL, init?: RequestInit) {
const response = await NATIVE_FETCH(request, init); const response = await NATIVE_FETCH(request, init);
if (getPref(PrefKey.UI_LOADING_SCREEN_WAIT_TIME)) { if (getPref(PrefKey.UI_LOADING_SCREEN_WAIT_TIME)) {
@ -143,7 +143,7 @@ class XcloudInterceptor {
return response; return response;
} }
static async #handleConfiguration(request: RequestInfo | URL, init?: RequestInit) { private static async handleConfiguration(request: RequestInfo | URL, init?: RequestInit) {
if ((request as Request).method !== 'GET') { if ((request as Request).method !== 'GET') {
return NATIVE_FETCH(request, init); return NATIVE_FETCH(request, init);
} }
@ -213,13 +213,13 @@ class XcloudInterceptor {
// Server list // Server list
if (url.endsWith('/v2/login/user')) { if (url.endsWith('/v2/login/user')) {
return XcloudInterceptor.#handleLogin(request, init); return XcloudInterceptor.handleLogin(request, init);
} else if (url.endsWith('/sessions/cloud/play')) { // Get session } else if (url.endsWith('/sessions/cloud/play')) { // Get session
return XcloudInterceptor.#handlePlay(request, init); return XcloudInterceptor.handlePlay(request, init);
} else if (url.includes('xboxlive.com') && url.includes('/waittime/')) { } else if (url.includes('xboxlive.com') && url.includes('/waittime/')) {
return XcloudInterceptor.#handleWaitTime(request, init); return XcloudInterceptor.handleWaitTime(request, init);
} else if (url.endsWith('/configuration')) { } else if (url.endsWith('/configuration')) {
return XcloudInterceptor.#handleConfiguration(request, init); return XcloudInterceptor.handleConfiguration(request, init);
} else if (url && url.endsWith('/ice') && url.includes('/sessions/') && (request as Request).method === 'GET') { } else if (url && url.endsWith('/ice') && url.includes('/sessions/') && (request as Request).method === 'GET') {
return patchIceCandidates(request as Request); return patchIceCandidates(request as Request);
} }

View File

@ -10,7 +10,7 @@ import type { RemotePlayConsoleAddresses } from "@/types/network";
import { RemotePlayManager } from "@/modules/remote-play-manager"; import { RemotePlayManager } from "@/modules/remote-play-manager";
export class XhomeInterceptor { export class XhomeInterceptor {
static #consoleAddrs: RemotePlayConsoleAddresses = {}; private static consoleAddrs: RemotePlayConsoleAddresses = {};
private static readonly BASE_DEVICE_INFO = { private static readonly BASE_DEVICE_INFO = {
appInfo: { appInfo: {
@ -52,7 +52,7 @@ export class XhomeInterceptor {
}, },
}; };
static async #handleLogin(request: Request) { private static async handleLogin(request: Request) {
try { try {
const clone = (request as Request).clone(); const clone = (request as Request).clone();
@ -74,7 +74,7 @@ export class XhomeInterceptor {
return NATIVE_FETCH(request); return NATIVE_FETCH(request);
} }
static async #handleConfiguration(request: Request | URL) { private static async handleConfiguration(request: Request | URL) {
const response = await NATIVE_FETCH(request); const response = await NATIVE_FETCH(request);
const obj = await response.clone().json() const obj = await response.clone().json()
@ -90,15 +90,15 @@ export class XhomeInterceptor {
const serverDetails = obj.serverDetails; const serverDetails = obj.serverDetails;
if (serverDetails.ipAddress) { if (serverDetails.ipAddress) {
XhomeInterceptor.#consoleAddrs[serverDetails.ipAddress] = processPorts(serverDetails.port); XhomeInterceptor.consoleAddrs[serverDetails.ipAddress] = processPorts(serverDetails.port);
} }
if (serverDetails.ipV4Address) { if (serverDetails.ipV4Address) {
XhomeInterceptor.#consoleAddrs[serverDetails.ipV4Address] = processPorts(serverDetails.ipV4Port); XhomeInterceptor.consoleAddrs[serverDetails.ipV4Address] = processPorts(serverDetails.ipV4Port);
} }
if (serverDetails.ipV6Address) { if (serverDetails.ipV6Address) {
XhomeInterceptor.#consoleAddrs[serverDetails.ipV6Address] = processPorts(serverDetails.ipV6Port); XhomeInterceptor.consoleAddrs[serverDetails.ipV6Address] = processPorts(serverDetails.ipV6Port);
} }
response.json = () => Promise.resolve(obj); response.json = () => Promise.resolve(obj);
@ -107,7 +107,7 @@ export class XhomeInterceptor {
return response; return response;
} }
static async #handleInputConfigs(request: Request | URL, opts: {[index: string]: any}) { private static async handleInputConfigs(request: Request | URL, opts: {[index: string]: any}) {
const response = await NATIVE_FETCH(request); const response = await NATIVE_FETCH(request);
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== StreamTouchController.ALL) { if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== StreamTouchController.ALL) {
@ -144,7 +144,7 @@ export class XhomeInterceptor {
return response; return response;
} }
static async #handleTitles(request: Request) { private static async handleTitles(request: Request) {
const clone = request.clone(); const clone = request.clone();
const headers: {[index: string]: any} = {}; const headers: {[index: string]: any} = {};
@ -163,7 +163,7 @@ export class XhomeInterceptor {
return NATIVE_FETCH(request); return NATIVE_FETCH(request);
} }
static async #handlePlay(request: RequestInfo | URL) { private static async handlePlay(request: RequestInfo | URL) {
const clone = (request as Request).clone(); const clone = (request as Request).clone();
const body = await clone.json(); const body = await clone.json();
@ -216,17 +216,17 @@ export class XhomeInterceptor {
// Get console IP // Get console IP
if (url.includes('/configuration')) { if (url.includes('/configuration')) {
return XhomeInterceptor.#handleConfiguration(request); return XhomeInterceptor.handleConfiguration(request);
} else if (url.endsWith('/sessions/home/play')) { } else if (url.endsWith('/sessions/home/play')) {
return XhomeInterceptor.#handlePlay(request); return XhomeInterceptor.handlePlay(request);
} else if (url.includes('inputconfigs')) { } else if (url.includes('inputconfigs')) {
return XhomeInterceptor.#handleInputConfigs(request, opts); return XhomeInterceptor.handleInputConfigs(request, opts);
} else if (url.includes('/login/user')) { } else if (url.includes('/login/user')) {
return XhomeInterceptor.#handleLogin(request); return XhomeInterceptor.handleLogin(request);
} else if (url.endsWith('/titles')) { } else if (url.endsWith('/titles')) {
return XhomeInterceptor.#handleTitles(request); return XhomeInterceptor.handleTitles(request);
} else if (url && url.endsWith('/ice') && url.includes('/sessions/') && (request as Request).method === 'GET') { } else if (url && url.endsWith('/ice') && url.includes('/sessions/') && (request as Request).method === 'GET') {
return patchIceCandidates(request, XhomeInterceptor.#consoleAddrs); return patchIceCandidates(request, XhomeInterceptor.consoleAddrs);
} }
return await NATIVE_FETCH(request); return await NATIVE_FETCH(request);