Compare commits

..

12 Commits

Author SHA1 Message Date
f71904c30b Bump version to 4.7.1 2024-06-10 08:29:53 +07:00
3a16187504 Update Guide menu detection 2024-06-10 08:03:13 +07:00
00ebb3f672 Fix dispatching STREAM_PLAYING event when playing normal video 2024-06-09 18:31:57 +07:00
ebb4d3c141 Add FeatureGates 2024-06-09 18:31:15 +07:00
902918d7fb Update URLs 2024-06-09 15:56:32 +07:00
b780e4e63b Minor fix 2024-06-09 15:45:41 +07:00
32889e0cf1 Minor fix 2024-06-09 15:43:19 +07:00
d8b9fcc951 Update better-xcloud.user.js 2024-06-09 15:40:15 +07:00
c7734245ae Don't check update for beta version 2024-06-09 11:50:46 +07:00
35e7fdacb5 Disable context menu on devices with touch support by default 2024-06-09 11:48:12 +07:00
504f16b802 Rename "hasTouchSupport" to "userAgentHasTouchSupport" 2024-06-09 11:47:08 +07:00
a3a7a57b51 Get PointerServer's port from the app 2024-06-09 11:41:00 +07:00
22 changed files with 177 additions and 125 deletions

View File

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

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 4.7.0
// @version 4.7.1
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -104,23 +104,23 @@ class UserAgent {
}
// src/utils/global.ts
var SCRIPT_VERSION = "4.7.0";
var SCRIPT_HOME = "https://github.com/redphx/better-xcloud";
var SCRIPT_VERSION = "4.7.1";
var AppInterface = window.AppInterface;
UserAgent.init();
var userAgent = window.navigator.userAgent.toLowerCase();
var isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent);
var isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser");
var browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0;
var hasTouchSupport = !isTv && !isVr && browserHasTouchSupport;
var userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport;
var STATES = {
isPlaying: false,
appContext: {},
serverRegions: {},
hasTouchSupport,
userAgentHasTouchSupport,
browserHasTouchSupport,
currentStream: {},
remotePlay: {}
remotePlay: {},
pointerServerPort: 9269
};
// src/utils/bx-event.ts
@ -171,8 +171,8 @@ var XcloudEvent;
event[key] = data[key];
}
}
AppInterface && AppInterface.onEvent(eventName);
target.dispatchEvent(event);
AppInterface && AppInterface.onEvent(eventName);
}
BxEvent.dispatch = dispatch;
})(BxEvent || (BxEvent = {}));
@ -189,7 +189,8 @@ var DEFAULT_FLAGS = {
EnableXcloudLogging: false,
SafariWorkaround: true,
UseDevTouchLayout: false,
ForceNativeMkbTitles: []
ForceNativeMkbTitles: [],
FeatureGates: null
};
var BX_FLAGS = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {});
try {
@ -1590,7 +1591,7 @@ class Preferences {
all: t("tc-all-games"),
off: t("off")
},
unsupported: !STATES.hasTouchSupport,
unsupported: !STATES.userAgentHasTouchSupport,
ready: (setting) => {
if (setting.unsupported) {
setting.default = "default";
@ -1600,7 +1601,7 @@ class Preferences {
[PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF]: {
label: t("tc-auto-off"),
default: false,
unsupported: !STATES.hasTouchSupport
unsupported: !STATES.userAgentHasTouchSupport
},
[PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY]: {
type: SettingElementType.NUMBER_STEPPER,
@ -1614,7 +1615,7 @@ class Preferences {
ticks: 10,
hideSlider: true
},
unsupported: !STATES.hasTouchSupport
unsupported: !STATES.userAgentHasTouchSupport
},
[PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: {
label: t("tc-standard-layout-style"),
@ -1624,7 +1625,7 @@ class Preferences {
white: t("tc-all-white"),
muted: t("tc-muted-colors")
},
unsupported: !STATES.hasTouchSupport
unsupported: !STATES.userAgentHasTouchSupport
},
[PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM]: {
label: t("tc-custom-layout-style"),
@ -1633,7 +1634,7 @@ class Preferences {
default: t("default"),
muted: t("tc-muted-colors")
},
unsupported: !STATES.hasTouchSupport
unsupported: !STATES.userAgentHasTouchSupport
},
[PrefKey.STREAM_SIMPLIFY_MENU]: {
label: t("simplify-stream-menu"),
@ -1842,7 +1843,7 @@ class Preferences {
},
[PrefKey.UI_HOME_CONTEXT_MENU_DISABLED]: {
label: t("disable-home-context-menu"),
default: false
default: STATES.browserHasTouchSupport
},
[PrefKey.BLOCK_SOCIAL_FEATURES]: {
label: t("disable-social-features"),
@ -2968,7 +2969,6 @@ var PointerAction;
})(PointerAction || (PointerAction = {}));
class PointerClient {
static #PORT = 9269;
static instance;
static getInstance() {
if (!PointerClient.instance) {
@ -2978,9 +2978,12 @@ class PointerClient {
}
#socket;
#mkbHandler;
start(mkbHandler) {
start(port, mkbHandler) {
if (!port) {
throw new Error("PointerServer port is 0");
}
this.#mkbHandler = mkbHandler;
this.#socket = new WebSocket(`ws://localhost:${PointerClient.#PORT}`);
this.#socket = new WebSocket(`ws://localhost:${port}`);
this.#socket.binaryType = "arraybuffer";
this.#socket.addEventListener("open", (event) => {
BxLogger.info(LOG_TAG, "connected");
@ -3159,7 +3162,7 @@ class NativeMkbHandler extends MkbHandler {
this.#inputSink = window.BX_EXPOSED.inputSink;
this.#updateInputConfigurationAsync(false);
try {
this.#pointerClient.start(this);
this.#pointerClient.start(STATES.pointerServerPort, this);
} catch (e) {
Toast.show("Cannot enable Mouse & Keyboard feature");
}
@ -3313,7 +3316,7 @@ class WebSocketMouseDataProvider extends MouseDataProvider {
this.#pointerClient = PointerClient.getInstance();
this.#connected = false;
try {
this.#pointerClient.start(this.mkbHandler);
this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler);
this.#connected = true;
} catch (e) {
Toast.show("Cannot enable Mouse & Keyboard feature");
@ -3837,6 +3840,9 @@ class StreamUiShortcut {
// src/utils/utils.ts
function checkForUpdate() {
if (SCRIPT_VERSION.includes("beta")) {
return;
}
const CHECK_INTERVAL_SECONDS = 7200;
const currentVersion = getPref(PrefKey.CURRENT_VERSION);
const lastCheck = getPref(PrefKey.LAST_UPDATE_CHECK);
@ -4221,14 +4227,14 @@ var BxExposed = {
modifyTitleInfo: (titleInfo) => {
titleInfo = structuredClone(titleInfo);
let supportedInputTypes = titleInfo.details.supportedInputTypes;
if (BX_FLAGS.ForceNativeMkbTitles.includes(titleInfo.details.productId)) {
if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) {
supportedInputTypes.push(InputType.MKB);
}
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === "off") {
supportedInputTypes = supportedInputTypes.filter((i) => i !== InputType.MKB);
}
titleInfo.details.hasMkbSupport = supportedInputTypes.includes(InputType.MKB);
if (STATES.hasTouchSupport) {
if (STATES.userAgentHasTouchSupport) {
let touchControllerAvailability = getPref(PrefKey.STREAM_TOUCH_CONTROLLER);
if (touchControllerAvailability !== "off" && getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) {
const gamepads = window.navigator.getGamepads();
@ -5333,7 +5339,7 @@ var setupStreamSettingsDialog = function() {
}
]
},
STATES.hasTouchSupport && {
STATES.userAgentHasTouchSupport && {
group: "touch-controller",
label: t("touch-controller"),
items: [
@ -5899,6 +5905,20 @@ var GamePassCloudGallery;
GamePassCloudGallery2["TOUCH"] = "9c86f07a-f3e8-45ad-82a0-a1f759597059";
})(GamePassCloudGallery || (GamePassCloudGallery = {}));
// src/utils/feature-gates.ts
var FeatureGates = {
PwaPrompt: false
};
if (getPref(PrefKey.UI_HOME_CONTEXT_MENU_DISABLED)) {
FeatureGates["EnableHomeContextMenu"] = false;
}
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES)) {
FeatureGates["EnableGuideChatTab"] = false;
}
if (BX_FLAGS.FeatureGates) {
FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates);
}
// src/utils/network.ts
var clearApplicationInsightsBuffers = function() {
window.sessionStorage.removeItem("AI_buffer");
@ -6042,12 +6062,8 @@ function interceptHttpRequests() {
try {
const response = await NATIVE_FETCH(request, init);
const json = await response.json();
const overrideTreatments = {};
if (getPref(PrefKey.UI_HOME_CONTEXT_MENU_DISABLED)) {
overrideTreatments["EnableHomeContextMenu"] = false;
}
for (const key in overrideTreatments) {
json.exp.treatments[key] = overrideTreatments[key];
for (const key in FeatureGates) {
json.exp.treatments[key] = FeatureGates[key];
}
response.json = () => Promise.resolve(json);
return response;
@ -6055,7 +6071,7 @@ function interceptHttpRequests() {
console.log(e);
}
}
if (STATES.hasTouchSupport && url.includes("catalog.gamepass.com/sigls/")) {
if (STATES.userAgentHasTouchSupport && url.includes("catalog.gamepass.com/sigls/")) {
const response = await NATIVE_FETCH(request, init);
const obj = await response.clone().json();
if (url.includes(GamePassCloudGallery.ALL)) {
@ -6149,12 +6165,12 @@ class XhomeInterceptor {
const xboxTitleId = JSON.parse(opts.body).titleIds[0];
STATES.currentStream.xboxTitleId = xboxTitleId;
const inputConfigs = obj[0];
let hasTouchSupport2 = inputConfigs.supportedTabs.length > 0;
if (!hasTouchSupport2) {
let hasTouchSupport = inputConfigs.supportedTabs.length > 0;
if (!hasTouchSupport) {
const supportedInputTypes = inputConfigs.supportedInputTypes;
hasTouchSupport2 = supportedInputTypes.includes(InputType.NATIVE_TOUCH) || supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY);
hasTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH) || supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY);
}
if (hasTouchSupport2) {
if (hasTouchSupport) {
TouchController.disable();
BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {
data: null
@ -6340,7 +6356,7 @@ class XcloudInterceptor {
overrides.inputConfiguration = overrides.inputConfiguration || {};
overrides.inputConfiguration.enableVibration = true;
let overrideMkb = null;
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === "on" || BX_FLAGS.ForceNativeMkbTitles.includes(STATES.currentStream.titleInfo.details.productId)) {
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === "on" || BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) {
overrideMkb = true;
}
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === "off") {
@ -8023,10 +8039,9 @@ if (!!window.BX_REMOTE_PLAY_CONFIG) {
return false;
}
const endIndex = str2.indexOf("},", index);
const newSettings = [
"PwaPrompt: false"
];
const newCode = newSettings.join(",");
let newSettings = JSON.stringify(FeatureGates);
newSettings = newSettings.substring(1, newSettings.length - 1);
const newCode = newSettings;
str2 = str2.substring(0, endIndex) + "," + newCode + str2.substring(endIndex);
return str2;
},
@ -8356,7 +8371,7 @@ var PATCH_ORDERS = [
"remotePlayKeepAlive",
"remotePlayDirectConnectUrl",
"remotePlayDisableAchievementToast",
STATES.hasTouchSupport && "patchUpdateInputConfigurationAsync"
STATES.userAgentHasTouchSupport && "patchUpdateInputConfigurationAsync"
] : [],
...BX_FLAGS.EnableXcloudLogging ? [
"enableConsoleLogging",
@ -8371,7 +8386,7 @@ var PLAYING_PATCH_ORDERS = [
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && !getPref(PrefKey.STREAM_COMBINE_SOURCES) && "patchAudioMediaStream",
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && getPref(PrefKey.STREAM_COMBINE_SOURCES) && "patchCombinedAudioVideoMediaStream",
getPref(PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG) && "skipFeedbackDialog",
...STATES.hasTouchSupport ? [
...STATES.userAgentHasTouchSupport ? [
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all" && "patchShowSensorControls",
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all" && "exposeTouchLayoutManager",
(getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "off" || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && "disableTakRenderer",
@ -8560,7 +8575,7 @@ function setupSettingsUi() {
let $updateAvailable;
const $wrapper = CE("div", { class: "bx-settings-wrapper" }, CE("div", { class: "bx-settings-title-wrapper" }, CE("a", {
class: "bx-settings-title",
href: SCRIPT_HOME,
href: "https://github.com/redphx/better-xcloud/releases",
target: "_blank"
}, "Better xCloud " + SCRIPT_VERSION), createButton({
icon: BxIcon.QUESTION,
@ -8570,11 +8585,11 @@ function setupSettingsUi() {
})));
$updateAvailable = CE("a", {
class: "bx-settings-update bx-gone",
href: "https://github.com/redphx/better-xcloud/releases",
href: "https://github.com/redphx/better-xcloud/releases/latest",
target: "_blank"
});
$wrapper.appendChild($updateAvailable);
if (PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {
if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {
$updateAvailable.textContent = `🌟 Version ${PREF_LATEST_VERSION} available`;
$updateAvailable.classList.remove("bx-gone");
}
@ -8811,8 +8826,8 @@ var SETTINGS_UI = {
]
},
[t("touch-controller")]: {
note: !STATES.hasTouchSupport ? "⚠️ " + t("device-unsupported-touch") : null,
unsupported: !STATES.hasTouchSupport,
note: !STATES.userAgentHasTouchSupport ? "⚠️ " + t("device-unsupported-touch") : null,
unsupported: !STATES.userAgentHasTouchSupport,
items: [
PrefKey.STREAM_TOUCH_CONTROLLER,
PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF,
@ -8884,7 +8899,7 @@ var injectSettingsButton = function($parent) {
document.activeElement && document.activeElement.blur();
}
});
if (PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) {
if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) {
$settingsBtn.setAttribute("data-update-available", "true");
}
$headerFragment.appendChild($settingsBtn);
@ -8950,7 +8965,7 @@ function overridePreloadState() {
} catch (e) {
BxLogger.error(LOG_TAG6, e);
}
if (STATES.hasTouchSupport) {
if (STATES.userAgentHasTouchSupport) {
try {
const sigls = state.xcloud.sigls;
if (GamePassCloudGallery.TOUCH in sigls) {
@ -9050,10 +9065,10 @@ function patchVideoApi() {
}
return nativePlay.apply(this);
}
if (!!this.src) {
return nativePlay.apply(this);
const $parent = this.parentElement;
if (!this.src && $parent.dataset.testid === "media-container") {
this.addEventListener("playing", showFunc);
}
this.addEventListener("playing", showFunc);
return nativePlay.apply(this);
};
}
@ -9339,7 +9354,7 @@ class GameBar {
const $gameBar = CE("div", { id: "bx-game-bar", class: "bx-gone", "data-position": position }, $container = CE("div", { class: "bx-game-bar-container bx-offscreen" }), createSvgIcon(position === "bottom-left" ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT));
this.actions = [
new ScreenshotAction,
...STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== "off" ? [new TouchControlAction] : [],
...STATES.userAgentHasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== "off" ? [new TouchControlAction] : [],
new MicrophoneAction
];
if (position === "bottom-right") {
@ -9513,14 +9528,16 @@ var observeRootDialog = function($root) {
const $addedElm = mutation.addedNodes[0];
if ($addedElm instanceof HTMLElement && $addedElm.className) {
if ($addedElm.className.startsWith("NavigationAnimation") || $addedElm.className.startsWith("DialogRoutes") || $addedElm.className.startsWith("Dialog-module__container")) {
const $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true");
if ($selectedTab) {
let $elm = $selectedTab;
let index;
for (index = 0;$elm = $elm?.previousElementSibling; index++)
;
if (index === 0) {
BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: GuideMenuTab.HOME });
if (document.querySelector("#gamepass-dialog-root div[class*=GuideDialog]")) {
const $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true");
if ($selectedTab) {
let $elm = $selectedTab;
let index;
for (index = 0;$elm = $elm?.previousElementSibling; index++)
;
if (index === 0) {
BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: GuideMenuTab.HOME });
}
}
}
}
@ -9561,7 +9578,7 @@ var main = function() {
AppInterface && patchPointerLockApi();
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext();
getPref(PrefKey.BLOCK_TRACKING) && patchMeControl();
STATES.hasTouchSupport && TouchController.updateCustomList();
STATES.userAgentHasTouchSupport && TouchController.updateCustomList();
overridePreloadState();
VibrationManager.initialSetup();
BX_FLAGS.CheckForUpdate && checkForUpdate();
@ -9583,7 +9600,10 @@ var main = function() {
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all") {
TouchController.setup();
}
getPref(PrefKey.MKB_ENABLED) && AppInterface && AppInterface.startPointerServer();
if (getPref(PrefKey.MKB_ENABLED) && AppInterface) {
STATES.pointerServerPort = AppInterface.startPointerServer() || 9269;
BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString());
}
};
if (window.location.pathname.includes("/auth/msa")) {
window.addEventListener("load", (e) => {

View File

@ -219,15 +219,18 @@ function observeRootDialog($root: HTMLElement) {
const $addedElm = mutation.addedNodes[0];
if ($addedElm instanceof HTMLElement && $addedElm.className) {
if ($addedElm.className.startsWith('NavigationAnimation') || $addedElm.className.startsWith('DialogRoutes') || $addedElm.className.startsWith('Dialog-module__container')) {
// Find navigation bar
const $selectedTab = $addedElm.querySelector('div[class^=NavigationMenu] button[aria-selected=true');
if ($selectedTab) {
let $elm: Element | null = $selectedTab;
let index;
for (index = 0; ($elm = $elm?.previousElementSibling); index++);
// Make sure it's Guide dialog
if (document.querySelector('#gamepass-dialog-root div[class*=GuideDialog]')) {
// Find navigation bar
const $selectedTab = $addedElm.querySelector('div[class^=NavigationMenu] button[aria-selected=true');
if ($selectedTab) {
let $elm: Element | null = $selectedTab;
let index;
for (index = 0; ($elm = $elm?.previousElementSibling); index++);
if (index === 0) {
BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, {where: GuideMenuTab.HOME});
if (index === 0) {
BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, {where: GuideMenuTab.HOME});
}
}
}
}
@ -277,7 +280,7 @@ function main() {
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext();
getPref(PrefKey.BLOCK_TRACKING) && patchMeControl();
STATES.hasTouchSupport && TouchController.updateCustomList();
STATES.userAgentHasTouchSupport && TouchController.updateCustomList();
overridePreloadState();
VibrationManager.initialSetup();
@ -314,7 +317,10 @@ function main() {
}
// Start PointerProviderServer
(getPref(PrefKey.MKB_ENABLED)) && AppInterface && AppInterface.startPointerServer();
if (getPref(PrefKey.MKB_ENABLED) && AppInterface) {
STATES.pointerServerPort = AppInterface.startPointerServer() || 9269;
BxLogger.info('startPointerServer', 'Port', STATES.pointerServerPort.toString());
}
}
main();

View File

@ -41,7 +41,7 @@ export class GameBar {
this.actions = [
new ScreenshotAction(),
...(STATES.hasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== 'off') ? [new TouchControlAction()] : []),
...(STATES.userAgentHasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== 'off') ? [new TouchControlAction()] : []),
new MicrophoneAction(),
];

View File

@ -33,7 +33,7 @@ class WebSocketMouseDataProvider extends MouseDataProvider {
this.#pointerClient = PointerClient.getInstance();
this.#connected = false;
try {
this.#pointerClient.start(this.mkbHandler);
this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler);
this.#connected = true;
} catch (e) {
Toast.show('Cannot enable Mouse & Keyboard feature');

View File

@ -1,6 +1,6 @@
import { Toast } from "@/utils/toast";
import { PointerClient } from "./pointer-client";
import { AppInterface } from "@/utils/global";
import { AppInterface, STATES } from "@/utils/global";
import { MkbHandler } from "./base-mkb-handler";
import { t } from "@/utils/translation";
import { BxEvent } from "@/utils/bx-event";
@ -149,7 +149,7 @@ export class NativeMkbHandler extends MkbHandler {
this.#updateInputConfigurationAsync(false);
try {
this.#pointerClient.start(this);
this.#pointerClient.start(STATES.pointerServerPort, this);
} catch (e) {
Toast.show('Cannot enable Mouse & Keyboard feature');
}

View File

@ -14,8 +14,6 @@ enum PointerAction {
export class PointerClient {
static #PORT = 9269;
private static instance: PointerClient;
public static getInstance(): PointerClient {
if (!PointerClient.instance) {
@ -28,11 +26,15 @@ export class PointerClient {
#socket: WebSocket | undefined | null;
#mkbHandler: MkbHandler | undefined;
start(mkbHandler: MkbHandler) {
start(port: number, mkbHandler: MkbHandler) {
if (!port) {
throw new Error('PointerServer port is 0');
}
this.#mkbHandler = mkbHandler;
// Create WebSocket connection.
this.#socket = new WebSocket(`ws://localhost:${PointerClient.#PORT}`);
this.#socket = new WebSocket(`ws://localhost:${port}`);
this.#socket.binaryType = 'arraybuffer';
// Connection opened

View File

@ -11,6 +11,7 @@ import codeLocalCoOpEnable from "./patches/local-co-op-enable.js" with { type: "
import codeRemotePlayEnable from "./patches/remote-play-enable.js" with { type: "text" };
import codeRemotePlayKeepAlive from "./patches/remote-play-keep-alive.js" with { type: "text" };
import codeVibrationAdjust from "./patches/vibration-adjust.js" with { type: "text" };
import { FeatureGates } from "@/utils/feature-gates.js";
type PatchArray = (keyof typeof PATCHES)[];
@ -228,12 +229,10 @@ if (!!window.BX_REMOTE_PLAY_CONFIG) {
// Find the next "},"
const endIndex = str.indexOf('},', index);
const newSettings = [
// 'EnableStreamGate: false',
'PwaPrompt: false',
];
let newSettings = JSON.stringify(FeatureGates);
newSettings = newSettings.substring(1, newSettings.length - 1);
const newCode = newSettings.join(',');
const newCode = newSettings;
str = str.substring(0, endIndex) + ',' + newCode + str.substring(endIndex);
return str;
@ -672,7 +671,7 @@ let PATCH_ORDERS: PatchArray = [
'remotePlayKeepAlive',
'remotePlayDirectConnectUrl',
'remotePlayDisableAchievementToast',
STATES.hasTouchSupport && 'patchUpdateInputConfigurationAsync',
STATES.userAgentHasTouchSupport && 'patchUpdateInputConfigurationAsync',
] : []),
...(BX_FLAGS.EnableXcloudLogging ? [
@ -698,7 +697,7 @@ let PLAYING_PATCH_ORDERS: PatchArray = [
// Skip feedback dialog
getPref(PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG) && 'skipFeedbackDialog',
...(STATES.hasTouchSupport ? [
...(STATES.userAgentHasTouchSupport ? [
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all' && 'patchShowSensorControls',
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all' && 'exposeTouchLayoutManager',
(getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off' || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && 'disableTakRenderer',

View File

@ -1,4 +1,4 @@
import { STATES, AppInterface, SCRIPT_HOME, SCRIPT_VERSION } from "@utils/global";
import { STATES, AppInterface, SCRIPT_VERSION } from "@utils/global";
import { CE, createButton, ButtonStyle } from "@utils/html";
import { BxIcon } from "@utils/bx-icon";
import { getPreferredServerRegion } from "@utils/region";
@ -62,8 +62,8 @@ const SETTINGS_UI = {
},
[t('touch-controller')]: {
note: !STATES.hasTouchSupport ? '⚠️ ' + t('device-unsupported-touch') : null,
unsupported: !STATES.hasTouchSupport,
note: !STATES.userAgentHasTouchSupport ? '⚠️ ' + t('device-unsupported-touch') : null,
unsupported: !STATES.userAgentHasTouchSupport,
items: [
PrefKey.STREAM_TOUCH_CONTROLLER,
PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF,
@ -130,7 +130,7 @@ export function setupSettingsUi() {
CE<HTMLElement>('div', {'class': 'bx-settings-title-wrapper'},
CE('a', {
'class': 'bx-settings-title',
'href': SCRIPT_HOME,
'href': 'https://github.com/redphx/better-xcloud/releases',
'target': '_blank',
}, 'Better xCloud ' + SCRIPT_VERSION),
createButton({
@ -143,14 +143,14 @@ export function setupSettingsUi() {
);
$updateAvailable = CE('a', {
'class': 'bx-settings-update bx-gone',
'href': 'https://github.com/redphx/better-xcloud/releases',
'href': 'https://github.com/redphx/better-xcloud/releases/latest',
'target': '_blank',
});
$wrapper.appendChild($updateAvailable);
// Show new version indicator
if (PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {
if (!SCRIPT_VERSION.includes('beta') && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {
$updateAvailable.textContent = `🌟 Version ${PREF_LATEST_VERSION} available`;
$updateAvailable.classList.remove('bx-gone');
}

View File

@ -48,7 +48,7 @@ function injectSettingsButton($parent?: HTMLElement) {
});
// Show new update status
if (PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) {
if (!SCRIPT_VERSION.includes('beta') && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) {
$settingsBtn.setAttribute('data-update-available', 'true');
}

View File

@ -167,7 +167,7 @@ function setupStreamSettingsDialog() {
],
},
STATES.hasTouchSupport && {
STATES.userAgentHasTouchSupport && {
group: 'touch-controller',
label: t('touch-controller'),
items: [

View File

@ -28,7 +28,7 @@ type BxStates = {
appContext: any | null;
serverRegions: any;
hasTouchSupport: boolean;
userAgentHasTouchSupport: boolean;
browserHasTouchSupport: boolean;
currentStream: Partial<{
@ -51,6 +51,8 @@ type BxStates = {
serverId: string;
};
}>;
pointerServerPort: number;
}
type DualEnum = {[index: string]: number} & {[index: number]: string};

View File

@ -66,8 +66,8 @@ export namespace BxEvent {
}
}
AppInterface && AppInterface.onEvent(eventName);
target.dispatchEvent(event);
AppInterface && AppInterface.onEvent(eventName);
}
}

View File

@ -23,7 +23,7 @@ export const BxExposed = {
let supportedInputTypes = titleInfo.details.supportedInputTypes;
if (BX_FLAGS.ForceNativeMkbTitles.includes(titleInfo.details.productId)) {
if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) {
supportedInputTypes.push(InputType.MKB);
}
@ -34,7 +34,7 @@ export const BxExposed = {
titleInfo.details.hasMkbSupport = supportedInputTypes.includes(InputType.MKB);
if (STATES.hasTouchSupport) {
if (STATES.userAgentHasTouchSupport) {
let touchControllerAvailability = getPref(PrefKey.STREAM_TOUCH_CONTROLLER);
// Disable touch control when gamepad found

View File

@ -8,6 +8,7 @@ type BxFlags = Partial<{
UseDevTouchLayout: boolean;
ForceNativeMkbTitles: string[];
FeatureGates: {[key: string]: boolean} | null,
}>
// Setup flags
@ -21,9 +22,10 @@ const DEFAULT_FLAGS: BxFlags = {
UseDevTouchLayout: false,
ForceNativeMkbTitles: [],
FeatureGates: null,
}
export const BX_FLAGS = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {});
export const BX_FLAGS: BxFlags = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {});
try {
delete window.BX_FLAGS;
} catch (e) {}

View File

@ -0,0 +1,20 @@
import { BX_FLAGS } from "./bx-flags";
import { getPref, PrefKey } from "./preferences";
export let FeatureGates: {[key: string]: boolean} = {
'PwaPrompt': false,
};
// Disable context menu in Home page
if (getPref(PrefKey.UI_HOME_CONTEXT_MENU_DISABLED)) {
FeatureGates['EnableHomeContextMenu'] = false;
}
// Disable chat feature
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES)) {
FeatureGates['EnableGuideChatTab'] = false;
}
if (BX_FLAGS.FeatureGates) {
FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates);
}

View File

@ -1,7 +1,6 @@
import { UserAgent } from "./user-agent";
export const SCRIPT_VERSION = Bun.env.SCRIPT_VERSION;
export const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
export const SCRIPT_VERSION = Bun.env.SCRIPT_VERSION!;
export const AppInterface = window.AppInterface;
@ -11,15 +10,17 @@ const userAgent = window.navigator.userAgent.toLowerCase();
const isTv = userAgent.includes('smart-tv') || userAgent.includes('smarttv') || /\baft.*\b/.test(userAgent);
const isVr = window.navigator.userAgent.includes('VR') && window.navigator.userAgent.includes('OculusBrowser');
const browserHasTouchSupport = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const hasTouchSupport = !isTv && !isVr && browserHasTouchSupport;
const userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport;
export const STATES: BxStates = {
isPlaying: false,
appContext: {},
serverRegions: {},
hasTouchSupport: hasTouchSupport,
userAgentHasTouchSupport: userAgentHasTouchSupport,
browserHasTouchSupport: browserHasTouchSupport,
currentStream: {},
remotePlay: {},
pointerServerPort: 9269,
};

View File

@ -35,12 +35,12 @@ export function patchVideoApi() {
return nativePlay.apply(this);
}
if (!!this.src) {
return nativePlay.apply(this);
const $parent = this.parentElement!!;
// Video tag is stream player
if (!this.src && $parent.dataset.testid === 'media-container') {
this.addEventListener('playing', showFunc);
}
this.addEventListener('playing', showFunc);
return nativePlay.apply(this);
};
}

View File

@ -9,6 +9,7 @@ import { STATES } from "@utils/global";
import { getPreferredServerRegion } from "@utils/region";
import { GamePassCloudGallery } from "./gamepass-gallery";
import { InputType } from "./bx-exposed";
import { FeatureGates } from "./feature-gates";
enum RequestType {
XCLOUD = 'xcloud',
@ -440,7 +441,7 @@ class XcloudInterceptor {
let overrideMkb: boolean | null = null;
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' || BX_FLAGS.ForceNativeMkbTitles.includes(STATES.currentStream.titleInfo!.details.productId)) {
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' || BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo!.details.productId)) {
overrideMkb = true;
}
@ -578,14 +579,8 @@ export function interceptHttpRequests() {
const response = await NATIVE_FETCH(request, init);
const json = await response.json();
const overrideTreatments: {[key: string]: boolean} = {};
if (getPref(PrefKey.UI_HOME_CONTEXT_MENU_DISABLED)) {
overrideTreatments['EnableHomeContextMenu'] = false;
}
for (const key in overrideTreatments) {
json.exp.treatments[key] = overrideTreatments[key]
for (const key in FeatureGates) {
json.exp.treatments[key] = FeatureGates[key]
}
response.json = () => Promise.resolve(json);
@ -596,7 +591,7 @@ export function interceptHttpRequests() {
}
// Add list of games with custom layouts to the official list
if (STATES.hasTouchSupport && url.includes('catalog.gamepass.com/sigls/')) {
if (STATES.userAgentHasTouchSupport && url.includes('catalog.gamepass.com/sigls/')) {
const response = await NATIVE_FETCH(request, init);
const obj = await response.clone().json();

View File

@ -263,7 +263,7 @@ export class Preferences {
all: t('tc-all-games'),
off: t('off'),
},
unsupported: !STATES.hasTouchSupport,
unsupported: !STATES.userAgentHasTouchSupport,
ready: (setting: PreferenceSetting) => {
if (setting.unsupported) {
setting.default = 'default';
@ -273,7 +273,7 @@ export class Preferences {
[PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF]: {
label: t('tc-auto-off'),
default: false,
unsupported: !STATES.hasTouchSupport,
unsupported: !STATES.userAgentHasTouchSupport,
},
[PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY]: {
type: SettingElementType.NUMBER_STEPPER,
@ -287,7 +287,7 @@ export class Preferences {
ticks: 10,
hideSlider: true,
},
unsupported: !STATES.hasTouchSupport,
unsupported: !STATES.userAgentHasTouchSupport,
},
[PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: {
label: t('tc-standard-layout-style'),
@ -297,7 +297,7 @@ export class Preferences {
white: t('tc-all-white'),
muted: t('tc-muted-colors'),
},
unsupported: !STATES.hasTouchSupport,
unsupported: !STATES.userAgentHasTouchSupport,
},
[PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM]: {
label: t('tc-custom-layout-style'),
@ -306,7 +306,7 @@ export class Preferences {
default: t('default'),
muted: t('tc-muted-colors'),
},
unsupported: !STATES.hasTouchSupport,
unsupported: !STATES.userAgentHasTouchSupport,
},
[PrefKey.STREAM_SIMPLIFY_MENU]: {
@ -544,7 +544,7 @@ export class Preferences {
[PrefKey.UI_HOME_CONTEXT_MENU_DISABLED]: {
label: t('disable-home-context-menu'),
default: false,
default: STATES.browserHasTouchSupport,
},
[PrefKey.BLOCK_SOCIAL_FEATURES]: {

View File

@ -24,7 +24,7 @@ export function overridePreloadState() {
}
// Add list of games with custom layouts to the official list
if (STATES.hasTouchSupport) {
if (STATES.userAgentHasTouchSupport) {
try {
const sigls = state.xcloud.sigls;
if (GamePassCloudGallery.TOUCH in sigls) {

View File

@ -7,6 +7,11 @@ import { Translations } from "./translation";
* Check for update
*/
export function checkForUpdate() {
// Don't check update for beta version
if (SCRIPT_VERSION.includes('beta')) {
return;
}
const CHECK_INTERVAL_SECONDS = 2 * 3600; // check every 2 hours
const currentVersion = getPref(PrefKey.CURRENT_VERSION);