Port the rest of the code

This commit is contained in:
redphx
2024-04-23 17:55:52 +07:00
parent 27a277309b
commit be0cbff344
30 changed files with 4338 additions and 234 deletions

33
src/utils/gamepad.ts Normal file
View File

@@ -0,0 +1,33 @@
import { MkbHandler } from "../modules/mkb/mkb-handler";
import { PrefKey, getPref } from "../modules/preferences";
import { t } from "../modules/translation";
import { Toast } from "./toast";
// Show a toast when connecting/disconecting controller
export function showGamepadToast(gamepad: Gamepad) {
// Don't show Toast for virtual controller
if (gamepad.id === MkbHandler.VIRTUAL_GAMEPAD_ID) {
return;
}
console.log(gamepad);
let text = '🎮';
if (getPref(PrefKey.LOCAL_CO_OP_ENABLED)) {
text += ` #${gamepad.index + 1}`;
}
// Remove "(STANDARD GAMEPAD Vendor: xxx Product: xxx)" from ID
const gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, '');
text += ` - ${gamepadId}`;
let status;
if (gamepad.connected) {
const supportVibration = !!gamepad.vibrationActuator;
status = (supportVibration ? '✅' : '❌') + ' ' + t('vibration-status');
} else {
status = t('disconnected');
}
Toast.show(text, status, {instant: false});
}

40
src/utils/history.ts Normal file
View File

@@ -0,0 +1,40 @@
import { BxEvent } from "../modules/bx-event";
import { LoadingScreen } from "../modules/loading-screen";
import { RemotePlay } from "../modules/remote-play";
import { checkHeader } from "../modules/ui/header";
export function patchHistoryMethod(type: 'pushState' | 'replaceState') {
const orig = window.history[type];
return function(...args: any[]) {
BxEvent.dispatch(window, BxEvent.POPSTATE, {
arguments: args,
});
// @ts-ignore
return orig.apply(this, arguments);
};
};
export function onHistoryChanged(e: PopStateEvent) {
// @ts-ignore
if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === 'better-xcloud') {
return;
}
setTimeout(RemotePlay.detect, 10);
const $settings = document.querySelector('.bx-settings-container');
if ($settings) {
$settings.classList.add('bx-gone');
}
// Hide Remote Play popup
RemotePlay.detachPopup();
LoadingScreen.reset();
setTimeout(checkHeader, 2000);
BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
}

157
src/utils/monkey-patches.ts Normal file
View File

@@ -0,0 +1,157 @@
import { BxEvent } from "../modules/bx-event";
import { getPref, PrefKey } from "../modules/preferences";
import { UserAgent } from "./user-agent";
export function patchVideoApi() {
const PREF_SKIP_SPLASH_VIDEO = getPref(PrefKey.SKIP_SPLASH_VIDEO);
// Show video player when it's ready
const showFunc = function(this: HTMLVideoElement) {
this.style.visibility = 'visible';
this.removeEventListener('playing', showFunc);
if (!this.videoWidth) {
return;
}
BxEvent.dispatch(window, BxEvent.STREAM_PLAYING, {
$video: this,
});
}
const nativePlay = HTMLMediaElement.prototype.play;
HTMLMediaElement.prototype.play = function() {
if (this.className && this.className.startsWith('XboxSplashVideo')) {
if (PREF_SKIP_SPLASH_VIDEO) {
this.volume = 0;
this.style.display = 'none';
this.dispatchEvent(new Event('ended'));
return new Promise<void>(() => {});
}
return nativePlay.apply(this);
}
if (!!this.src) {
return nativePlay.apply(this);
}
this.addEventListener('playing', showFunc);
return nativePlay.apply(this);
};
}
export function patchRtcCodecs() {
const codecProfile = getPref(PrefKey.STREAM_CODEC_PROFILE);
if (codecProfile === 'default') {
return;
}
if (typeof RTCRtpTransceiver === 'undefined' || !('setCodecPreferences' in RTCRtpTransceiver.prototype)) {
return false;
}
const profilePrefix = codecProfile === 'high' ? '4d' : (codecProfile === 'low' ? '420' : '42e');
const profileLevelId = `profile-level-id=${profilePrefix}`;
const nativeSetCodecPreferences = RTCRtpTransceiver.prototype.setCodecPreferences;
RTCRtpTransceiver.prototype.setCodecPreferences = function(codecs) {
// Use the same codecs as desktop
const newCodecs = codecs.slice();
let pos = 0;
newCodecs.forEach((codec, i) => {
// Find high-quality codecs
if (codec.sdpFmtpLine && codec.sdpFmtpLine.includes(profileLevelId)) {
// Move it to the top of the array
newCodecs.splice(i, 1);
newCodecs.splice(pos, 0, codec);
++pos;
}
});
try {
nativeSetCodecPreferences.apply(this, [newCodecs]);
} catch (e) {
// Didn't work -> use default codecs
console.log(e);
nativeSetCodecPreferences.apply(this, [codecs]);
}
}
}
export function patchRtcPeerConnection() {
const nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;
RTCPeerConnection.prototype.createDataChannel = function() {
// @ts-ignore
const dataChannel = nativeCreateDataChannel.apply(this, arguments);
BxEvent.dispatch(window, BxEvent.DATA_CHANNEL_CREATED, {
dataChannel: dataChannel,
});
return dataChannel;
}
const OrgRTCPeerConnection = window.RTCPeerConnection;
// @ts-ignore
window.RTCPeerConnection = function() {
const conn = new OrgRTCPeerConnection();
States.currentStream.peerConnection = conn;
conn.addEventListener('connectionstatechange', e => {
if (conn.connectionState === 'connecting') {
States.currentStream.audioGainNode = null;
}
console.log('connectionState', conn.connectionState);
});
return conn;
}
}
export function patchAudioContext() {
if (UserAgent.isSafari(true)) {
const nativeCreateGain = window.AudioContext.prototype.createGain;
window.AudioContext.prototype.createGain = function() {
const gainNode = nativeCreateGain.apply(this);
gainNode.gain.value = getPref(PrefKey.AUDIO_VOLUME) / 100;
States.currentStream.audioGainNode = gainNode;
return gainNode;
}
}
const OrgAudioContext = window.AudioContext;
// @ts-ignore
window.AudioContext = function() {
const ctx = new OrgAudioContext();
States.currentStream.audioContext = ctx;
States.currentStream.audioGainNode = null;
return ctx;
}
const nativePlay = HTMLAudioElement.prototype.play;
HTMLAudioElement.prototype.play = function() {
this.muted = true;
const promise = nativePlay.apply(this);
if (States.currentStream.audioGainNode) {
return promise;
}
this.addEventListener('playing', e => (e.target as HTMLAudioElement).pause());
const audioCtx = States.currentStream.audioContext!;
// TOOD: check srcObject
const audioStream = audioCtx.createMediaStreamSource(this.srcObject as any);
const gainNode = audioCtx.createGain();
audioStream.connect(gainNode);
gainNode.connect(audioCtx.destination);
gainNode.gain.value = getPref(PrefKey.AUDIO_VOLUME) / 100;
States.currentStream.audioGainNode = gainNode;
return promise;
}
}

590
src/utils/network.ts Normal file
View File

@@ -0,0 +1,590 @@
import { BxEvent } from "../modules/bx-event";
import { BX_FLAGS } from "../modules/bx-flags";
import { LoadingScreen } from "../modules/loading-screen";
import { MouseCursorHider } from "../modules/mkb/mouse-cursor-hider";
import { PrefKey, getPref } from "../modules/preferences";
import { RemotePlay } from "../modules/remote-play";
import { StreamBadges } from "../modules/stream/stream-badges";
import { TouchController } from "../modules/touch-controller";
import { getPreferredServerRegion } from "./region";
import { TitlesInfo } from "./titles-info";
enum RequestType {
XCLOUD = 'xcloud',
XHOME = 'xhome',
};
function clearApplicationInsightsBuffers() {
window.sessionStorage.removeItem('AI_buffer');
window.sessionStorage.removeItem('AI_sentBuffer');
}
function clearDbLogs(dbName: string, table: string) {
const request = window.indexedDB.open(dbName);
request.onsuccess = e => {
const db = (e.target as any).result;
try {
const objectStore = db.transaction(table, 'readwrite').objectStore(table);
const objectStoreRequest = objectStore.clear();
objectStoreRequest.onsuccess = function() {
console.log(`[Better xCloud] Cleared ${dbName}.${table}`);
};
} catch (ex) {
console.log(ex);
}
}
}
function clearAllLogs() {
clearApplicationInsightsBuffers();
clearDbLogs('StreamClientLogHandler', 'logs');
clearDbLogs('XCloudAppLogs', 'logs');
}
function updateIceCandidates(candidates: any, options: any) {
const pattern = new RegExp(/a=candidate:(?<foundation>\d+) (?<component>\d+) UDP (?<priority>\d+) (?<ip>[^\s]+) (?<port>\d+) (?<the_rest>.*)/);
const lst = [];
for (let item of candidates) {
if (item.candidate == 'a=end-of-candidates') {
continue;
}
const groups: {[index: string]: string | number} = pattern.exec(item.candidate)!.groups!;
lst.push(groups);
}
if (options.preferIpv6Server) {
lst.sort((a, b) => {
const firstIp = a.ip as string;
const secondIp = b.ip as string;
return (!firstIp.includes(':') && secondIp.includes(':')) ? 1 : -1;
});
}
const newCandidates = [];
let foundation = 1;
const newCandidate = (candidate: string) => {
return {
'candidate': candidate,
'messageType': 'iceCandidate',
'sdpMLineIndex': '0',
'sdpMid': '0',
};
};
lst.forEach(item => {
item.foundation = foundation;
item.priority = (foundation == 1) ? 10000 : 1;
newCandidates.push(newCandidate(`a=candidate:${item.foundation} 1 UDP ${item.priority} ${item.ip} ${item.port} ${item.the_rest}`));
++foundation;
});
if (options.consoleAddrs) {
for (const ip in options.consoleAddrs) {
const port = options.consoleAddrs[ip];
newCandidates.push(newCandidate(`a=candidate:${newCandidates.length + 1} 1 UDP 1 ${ip} ${port} typ host`));
}
}
newCandidates.push(newCandidate('a=end-of-candidates'));
console.log(newCandidates);
return newCandidates;
}
async function patchIceCandidates(request: Request, consoleAddrs?: {[index: string]: number}) {
// ICE server candidates
const url = (typeof request === 'string') ? request : request.url;
if (url && url.endsWith('/ice') && url.includes('/sessions/') && request.method === 'GET') {
const response = await NATIVE_FETCH(request);
const text = await response.clone().text();
if (!text.length) {
return response;
}
const options = {
preferIpv6Server: getPref(PrefKey.PREFER_IPV6_SERVER),
consoleAddrs: consoleAddrs,
};
const obj = JSON.parse(text);
let exchangeResponse = JSON.parse(obj.exchangeResponse);
exchangeResponse = updateIceCandidates(exchangeResponse, options)
obj.exchangeResponse = JSON.stringify(exchangeResponse);
response.json = () => Promise.resolve(obj);
response.text = () => Promise.resolve(JSON.stringify(obj));
return response;
}
return null;
}
class XhomeInterceptor {
static #consoleAddrs: {[index: string]: number} = {};
static async #handleLogin(request: Request) {
try {
const clone = (request as Request).clone();
const obj = await clone.json();
obj.offeringId = 'xhome';
request = new Request('https://xhome.gssv-play-prod.xboxlive.com/v2/login/user', {
method: 'POST',
body: JSON.stringify(obj),
headers: {
'Content-Type': 'application/json',
},
});
} catch (e) {
alert(e);
console.log(e);
}
return NATIVE_FETCH(request);
}
static async #handleConfiguration(request: Request | URL) {
const response = await NATIVE_FETCH(request);
const obj = await response.clone().json()
console.log(obj);
const serverDetails = obj.serverDetails;
if (serverDetails.ipV4Address) {
XhomeInterceptor.#consoleAddrs[serverDetails.ipV4Address] = serverDetails.ipV4Port;
}
if (serverDetails.ipV6Address) {
XhomeInterceptor.#consoleAddrs[serverDetails.ipV6Address] = serverDetails.ipV6Port;
}
response.json = () => Promise.resolve(obj);
response.text = () => Promise.resolve(JSON.stringify(obj));
return response;
}
static async #handleInputConfigs(request: Request | URL, opts: {[index: string]: any}) {
const response = await NATIVE_FETCH(request);
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== 'all') {
return response;
}
const obj = await response.clone().json() as any;
const xboxTitleId = JSON.parse(opts.body).titleIds[0];
States.currentStream.xboxTitleId = xboxTitleId;
const inputConfigs = obj[0];
let hasTouchSupport = inputConfigs.supportedTabs.length > 0;
if (!hasTouchSupport) {
const supportedInputTypes = inputConfigs.supportedInputTypes;
hasTouchSupport = supportedInputTypes.includes('NativeTouch');
}
if (hasTouchSupport) {
TouchController.disable();
BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {
data: null,
});
} else {
TouchController.enable();
TouchController.getCustomLayouts(xboxTitleId);
}
response.json = () => Promise.resolve(obj);
response.text = () => Promise.resolve(JSON.stringify(obj));
return response;
}
static async #handleTitles(request: Request) {
const clone = request.clone();
const headers: {[index: string]: any} = {};
for (const pair of (clone.headers as any).entries()) {
headers[pair[0]] = pair[1];
}
headers.authorization = `Bearer ${RemotePlay.XCLOUD_TOKEN}`;
const index = request.url.indexOf('.xboxlive.com');
request = new Request('https://wus.core.gssv-play-prod' + request.url.substring(index), {
method: clone.method,
body: await clone.text(),
headers: headers,
});
return NATIVE_FETCH(request);
}
static async handle(request: Request) {
TouchController.disable();
const clone = request.clone();
const headers: {[index: string]: string} = {};
for (const pair of (clone.headers as any).entries()) {
headers[pair[0]] = pair[1];
}
// Add xHome token to headers
headers.authorization = `Bearer ${RemotePlay.XHOME_TOKEN}`;
// Patch resolution
const deviceInfo = RemotePlay.BASE_DEVICE_INFO;
if (getPref(PrefKey.REMOTE_PLAY_RESOLUTION) === '720p') {
deviceInfo.dev.os.name = 'android';
}
headers['x-ms-device-info'] = JSON.stringify(deviceInfo);
const opts: {[index: string]: any} = {
method: clone.method,
headers: headers,
};
if (clone.method === 'POST') {
opts.body = await clone.text();
}
const index = request.url.indexOf('.xboxlive.com');
let newUrl = States.remotePlay.server + request.url.substring(index + 13);
request = new Request(newUrl, opts);
let url = (typeof request === 'string') ? request : request.url;
// Get console IP
if (url.includes('/configuration')) {
return XhomeInterceptor.#handleConfiguration(request);
} else if (url.includes('inputconfigs')) {
return XhomeInterceptor.#handleInputConfigs(request, opts);
} else if (url.includes('/login/user')) {
return XhomeInterceptor.#handleLogin(request);
} else if (url.endsWith('/titles')) {
return XhomeInterceptor.#handleTitles(request);
}
return await patchIceCandidates(request, XhomeInterceptor.#consoleAddrs) || NATIVE_FETCH(request);
}
}
class XcloudInterceptor {
static async #handleLogin(request: RequestInfo | URL, init?: RequestInit) {
const response = await NATIVE_FETCH(request, init);
const obj = await response.clone().json();
// Preload Remote Play
BX_FLAGS.PreloadRemotePlay && RemotePlay.preload();
// Store xCloud token
RemotePlay.XCLOUD_TOKEN = obj.gsToken;
// Get server list
const serverEmojis = {
AustraliaEast: '🇦🇺',
AustraliaSouthEast: '🇦🇺',
BrazilSouth: '🇧🇷',
EastUS: '🇺🇸',
EastUS2: '🇺🇸',
JapanEast: '🇯🇵',
KoreaCentral: '🇰🇷',
MexicoCentral: '🇲🇽',
NorthCentralUs: '🇺🇸',
SouthCentralUS: '🇺🇸',
UKSouth: '🇬🇧',
WestEurope: '🇪🇺',
WestUS: '🇺🇸',
WestUS2: '🇺🇸',
};
const serverRegex = /\/\/(\w+)\./;
for (let region of obj.offeringSettings.regions) {
const regionName = region.name as keyof typeof serverEmojis;
let shortName = region.name;
let match = serverRegex.exec(region.baseUri);
if (match) {
shortName = match[1];
if (serverEmojis[regionName]) {
shortName = serverEmojis[regionName] + ' ' + shortName;
}
}
region.shortName = shortName.toUpperCase();
States.serverRegions[region.name] = Object.assign({}, region);
}
BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY);
const preferredRegion = getPreferredServerRegion();
if (preferredRegion in States.serverRegions) {
const tmp = Object.assign({}, States.serverRegions[preferredRegion]);
tmp.isDefault = true;
obj.offeringSettings.regions = [tmp];
}
response.json = () => Promise.resolve(obj);
return response;
}
static async #handlePlay(request: RequestInfo | URL, init?: RequestInit) {
const PREF_STREAM_TARGET_RESOLUTION = getPref(PrefKey.STREAM_TARGET_RESOLUTION);
const PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE);
const url = (typeof request === 'string') ? request : (request as Request).url;
const parsedUrl = new URL(url);
StreamBadges.region = parsedUrl.host.split('.', 1)[0];
for (let regionName in States.appContext) {
const region = States.appContext[regionName];
if (parsedUrl.origin == region.baseUri) {
StreamBadges.region = regionName;
break;
}
}
const clone = (request as Request).clone();
const body = await clone.json();
// Force stream's resolution
if (PREF_STREAM_TARGET_RESOLUTION !== 'auto') {
const osName = (PREF_STREAM_TARGET_RESOLUTION === '720p') ? 'android' : 'windows';
body.settings.osName = osName;
}
// Override "locale" value
if (PREF_STREAM_PREFERRED_LOCALE !== 'default') {
body.settings.locale = PREF_STREAM_PREFERRED_LOCALE;
}
const newRequest = new Request(request, {
body: JSON.stringify(body),
});
return NATIVE_FETCH(newRequest);
}
static async #handleWaitTime(request: RequestInfo | URL, init?: RequestInit) {
const response = await NATIVE_FETCH(request, init);
if (getPref(PrefKey.UI_LOADING_SCREEN_WAIT_TIME)) {
const json = await response.clone().json();
if (json.estimatedAllocationTimeInSeconds > 0) {
// Setup wait time overlay
LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds);
}
}
return response;
}
static async #handleConfiguration(request: RequestInfo | URL, init?: RequestInit) {
if ((request as Request).method !== 'GET') {
return NATIVE_FETCH(request, init);
}
// Touch controller for all games
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all') {
TouchController.disable();
// Get game ID from window.location
const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/);
// Check touch support
if (match) {
const titleId = match[1];
!TitlesInfo.hasTouchSupport(titleId) && TouchController.enable();
}
}
// Intercept configurations
const response = await NATIVE_FETCH(request, init);
const text = await response.clone().text();
if (!text.length) {
return response;
}
const obj = JSON.parse(text);
let overrides = JSON.parse(obj.clientStreamingConfigOverrides || '{}') || {};
overrides.inputConfiguration = overrides.inputConfiguration || {};
overrides.inputConfiguration.enableVibration = true;
// Enable touch controller
if (TouchController.isEnabled()) {
overrides.inputConfiguration.enableTouchInput = true;
overrides.inputConfiguration.maxTouchPoints = 10;
}
// Enable mic
if (getPref(PrefKey.AUDIO_MIC_ON_PLAYING)) {
overrides.audioConfiguration = overrides.audioConfiguration || {};
overrides.audioConfiguration.enableMicrophone = true;
}
obj.clientStreamingConfigOverrides = JSON.stringify(overrides);
response.json = () => Promise.resolve(obj);
response.text = () => Promise.resolve(JSON.stringify(obj));
return response;
}
static async #handleCatalog(request: RequestInfo | URL, init?: RequestInit) {
const response = await NATIVE_FETCH(request, init);
const json = await response.clone().json()
for (let productId in json.Products) {
TitlesInfo.saveFromCatalogInfo(json.Products[productId]);
}
return response;
}
static async #handleTitles(request: RequestInfo | URL, init?: RequestInit) {
const response = await NATIVE_FETCH(request, init);
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all') {
const json = await response.clone().json()
for (let game of json.results) {
TitlesInfo.saveFromTitleInfo(game);
}
}
return response;
}
static async handle(request: RequestInfo | URL, init?: RequestInit) {
let url = (typeof request === 'string') ? request : (request as Request).url;
// ICE server candidates
const patchedIpv6 = await patchIceCandidates(request as Request);
if (patchedIpv6) {
return patchedIpv6;
}
// Server list
if (url.endsWith('/v2/login/user')) {
return XcloudInterceptor.#handleLogin(request, init);
} else if (url.endsWith('/sessions/cloud/play')) { // Get session
return XcloudInterceptor.#handlePlay(request, init);
} else if (url.includes('xboxlive.com') && url.includes('/waittime/')) {
return XcloudInterceptor.#handleWaitTime(request, init);
} else if (url.endsWith('/configuration')) {
return XcloudInterceptor.#handleConfiguration(request, init);
} else if (url.startsWith('https://catalog.gamepass.com') && url.includes('/products')) {
return XcloudInterceptor.#handleCatalog(request, init);
} else if (url.includes('/v2/titles') || url.includes('/mru')) {
return XcloudInterceptor.#handleTitles(request, init);
}
return NATIVE_FETCH(request, init);
}
}
export function interceptHttpRequests() {
let BLOCKED_URLS: string[] = [];
if (getPref(PrefKey.BLOCK_TRACKING)) {
// Clear Applications Insight buffers
clearAllLogs();
BLOCKED_URLS = BLOCKED_URLS.concat([
'https://arc.msn.com',
'https://browser.events.data.microsoft.com',
'https://dc.services.visualstudio.com',
// 'https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io',
]);
}
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES)) {
BLOCKED_URLS = BLOCKED_URLS.concat([
'https://peoplehub.xboxlive.com/users/me/people/social',
'https://peoplehub.xboxlive.com/users/me/people/recommendations',
'https://notificationinbox.xboxlive.com',
// 'https://accounts.xboxlive.com/family/memberXuid',
]);
}
const xhrPrototype = XMLHttpRequest.prototype;
const nativeXhrOpen = xhrPrototype.open;
const nativeXhrSend = xhrPrototype.send;
xhrPrototype.open = function(method, url) {
// Save URL to use it later in send()
(this as any)._url = url;
// @ts-ignore
return nativeXhrOpen.apply(this, arguments);
};
xhrPrototype.send = function(...arg) {
for (const blocked of BLOCKED_URLS) {
if ((this as any)._url.startsWith(blocked)) {
if (blocked === 'https://dc.services.visualstudio.com') {
setTimeout(clearAllLogs, 1000);
}
return false;
}
}
// @ts-ignore
return nativeXhrSend.apply(this, arguments);
};
const PREF_UI_LOADING_SCREEN_GAME_ART = getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART);
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) {
if (!url.startsWith(blocked)) {
continue;
}
return new Response('{"acc":1,"webResult":{}}', {
status: 200,
statusText: '200 OK',
});
}
if (url.endsWith('/play')) {
BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
}
if (url.endsWith('/configuration')) {
BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
}
let requestType: RequestType;
if (States.remotePlay.isPlaying || url.includes('/sessions/home')) {
requestType = RequestType.XHOME;
} else {
requestType = RequestType.XCLOUD;
}
if (requestType === RequestType.XHOME) {
return XhomeInterceptor.handle(request as Request);
}
return XcloudInterceptor.handle(request, init);
}
}

30
src/utils/region.ts Normal file
View File

@@ -0,0 +1,30 @@
import { getPref, Preferences, PrefKey } from "../modules/preferences";
declare var States: BxStates;
export function getPreferredServerRegion(shortName = false) {
let preferredRegion = getPref(PrefKey.SERVER_REGION);
if (preferredRegion in States.serverRegions) {
if (shortName && States.serverRegions[preferredRegion].shortName) {
return States.serverRegions[preferredRegion].shortName;
} else {
return preferredRegion;
}
}
for (let regionName in States.serverRegions) {
const region = States.serverRegions[regionName];
if (!region.isDefault) {
continue;
}
if (shortName && region.shortName) {
return region.shortName;
} else {
return regionName;
}
}
return '???';
}

View File

@@ -2,32 +2,6 @@ import { PrefKey } from "../modules/preferences";
import { getPref } from "../modules/preferences";
import { UserAgent } from "./user-agent";
type TitleInfo = {
titleId?: string;
xboxTitleId?: string;
hasTouchSupport?: boolean;
imageHero?: string;
};
type ApiTitleInfo = {
titleId: string;
details: {
xboxTitleId: string;
productId: string;
supportedInputTypes: string[];
};
};
type ApiCatalogInfo = {
StoreId: string;
Image_Hero: {
URL: string;
};
Image_Tile: {
URL: string;
};
};
export class TitlesInfo {
static #INFO: {[index: string]: TitleInfo} = {};
@@ -44,7 +18,7 @@ export class TitlesInfo {
const details = titleInfo.details;
const info: TitleInfo = {
titleId: titleInfo.titleId,
xboxTitleId: details.xboxTitleId,
xboxTitleId: '' + details.xboxTitleId,
// Has more than one input type -> must have touch support
hasTouchSupport: (details.supportedInputTypes.length > 1),
};
@@ -85,7 +59,7 @@ export class TitlesInfo {
}
class PreloadedState {
export class PreloadedState {
static override() {
Object.defineProperty(window, '__PRELOADED_STATE__', {
configurable: true,

View File

@@ -14,7 +14,7 @@ export class Toast {
static #timeout?: number | null;
static #DURATION = 3000;
static show(msg: string, status: string, options: ToastOptions={}) {
static show(msg: string, status?: string, options: Partial<ToastOptions>={}) {
options = options || {};
const args = Array.from(arguments) as [string, string, ToastOptions];

40
src/utils/utils.ts Normal file
View File

@@ -0,0 +1,40 @@
import { PrefKey, getPref, setPref } from "../modules/preferences";
import { UserAgent } from "./user-agent";
export function checkForUpdate() {
const CHECK_INTERVAL_SECONDS = 2 * 3600; // check every 2 hours
const currentVersion = getPref(PrefKey.CURRENT_VERSION);
const lastCheck = getPref(PrefKey.LAST_UPDATE_CHECK);
const now = Math.round((+new Date) / 1000);
if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) {
return;
}
// Start checking
setPref(PrefKey.LAST_UPDATE_CHECK, now);
fetch('https://api.github.com/repos/redphx/better-xcloud/releases/latest')
.then(response => response.json())
.then(json => {
// Store the latest version
setPref(PrefKey.LATEST_VERSION, json.tag_name.substring(1));
setPref(PrefKey.CURRENT_VERSION, SCRIPT_VERSION);
});
}
export function disablePwa() {
const userAgent = ((window.navigator as any).orgUserAgent || window.navigator.userAgent || '').toLowerCase();
if (!userAgent) {
return;
}
// Check if it's Safari on mobile
if (UserAgent.isSafari(true)) {
// Disable the PWA prompt
Object.defineProperty(window.navigator, 'standalone', {
value: true,
});
}
}