mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-29 10:51:44 +02:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
b66ca192b2 | |||
660aac4e8c | |||
3b1f5155c6 | |||
500f6671c6 | |||
26bf14eda6 | |||
d8fada8f5d | |||
4e8848d2fb | |||
8e23ca51de | |||
9ac988e894 | |||
c2efbd9c1d | |||
7eda0b61cc | |||
c948b63b8d | |||
fc56d486a7 | |||
7dacc8f23a | |||
2df3bb4611 | |||
b9355d5c01 | |||
d1b99705e6 | |||
52896c94ae | |||
cadc7987b7 | |||
8fb1787222 | |||
4231d7e9c6 | |||
ba05eab47b | |||
e852b246d3 | |||
23fb50cb6f | |||
443bf93c9a |
2
dist/better-xcloud.meta.js
vendored
2
dist/better-xcloud.meta.js
vendored
@ -1,5 +1,5 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 4.1.1
|
||||
// @version 4.2.0
|
||||
// ==/UserScript==
|
||||
|
386
dist/better-xcloud.user.js
vendored
386
dist/better-xcloud.user.js
vendored
@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 4.1.1
|
||||
// @version 4.2.0
|
||||
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
||||
// @author redphx
|
||||
// @license MIT
|
||||
@ -14,7 +14,7 @@
|
||||
// ==/UserScript==
|
||||
'use strict';
|
||||
// src/utils/global.ts
|
||||
var SCRIPT_VERSION = "4.1.1";
|
||||
var SCRIPT_VERSION = "4.2.0";
|
||||
var SCRIPT_HOME = "https://github.com/redphx/better-xcloud";
|
||||
var AppInterface = window.AppInterface;
|
||||
var STATES = {
|
||||
@ -115,6 +115,12 @@ var createElement = function(elmName, props = {}, ..._) {
|
||||
}
|
||||
return $elm;
|
||||
};
|
||||
function escapeHtml(html) {
|
||||
const text = document.createTextNode(html);
|
||||
const $span = document.createElement("span");
|
||||
$span.appendChild(text);
|
||||
return $span.innerHTML;
|
||||
}
|
||||
var CE = createElement;
|
||||
var svgParser = (svg) => new DOMParser().parseFromString(svg, "image/svg+xml").documentElement;
|
||||
var createSvgIcon = (icon) => {
|
||||
@ -1754,7 +1760,7 @@ var Texts = {
|
||||
"未找到主机"
|
||||
],
|
||||
normal: [
|
||||
"Mittel",
|
||||
"Normal",
|
||||
"Normal",
|
||||
"Normal",
|
||||
"Normal",
|
||||
@ -2909,6 +2915,23 @@ var Texts = {
|
||||
"Màu của bố cục tùy chọn",
|
||||
"特殊游戏按钮样式"
|
||||
],
|
||||
"tc-default-opacity": [
|
||||
"Standard Deckkraft",
|
||||
,
|
||||
"Default opacity",
|
||||
"Opacidad por defecto",
|
||||
,
|
||||
,
|
||||
"既定の透過度",
|
||||
,
|
||||
"Domyślna przezroczystość",
|
||||
"Opacidade padrão",
|
||||
"Прозрачность по умолчанию",
|
||||
"Varsayılan opaklık",
|
||||
"Непрозорість за замовчуванням",
|
||||
"Độ mờ mặc định",
|
||||
"默认不透明度"
|
||||
],
|
||||
"tc-muted-colors": [
|
||||
"Matte Farben",
|
||||
"Warna redup",
|
||||
@ -3028,6 +3051,23 @@ var Texts = {
|
||||
"Bố cục điều khiển cảm ứng",
|
||||
"触摸控制布局"
|
||||
],
|
||||
"touch-control-layout-by": [
|
||||
(e) => `Touch-Steuerungslayout von ${e.name}`,
|
||||
,
|
||||
(e) => `Touch control layout by ${e.name}`,
|
||||
,
|
||||
,
|
||||
,
|
||||
(e) => `タッチ操作レイアウト作成者: ${e.name}`,
|
||||
,
|
||||
(e) => `Układ sterowania dotykowego stworzony przez ${e.name}`,
|
||||
,
|
||||
,
|
||||
(e) => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,
|
||||
(e) => `Розташування сенсорного керування від ${e.name}`,
|
||||
(e) => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,
|
||||
,
|
||||
],
|
||||
"touch-controller": [
|
||||
"Touch-Controller",
|
||||
"Kontrol sentuhan",
|
||||
@ -3534,8 +3574,10 @@ class SettingElement {
|
||||
onChange && onChange(e, value2);
|
||||
};
|
||||
const onMouseDown = (e) => {
|
||||
e.preventDefault();
|
||||
isHolding = true;
|
||||
const args = arguments;
|
||||
interval && clearInterval(interval);
|
||||
interval = window.setInterval(() => {
|
||||
const event = new Event("click");
|
||||
event.arguments = args;
|
||||
@ -3543,23 +3585,23 @@ class SettingElement {
|
||||
}, 200);
|
||||
};
|
||||
const onMouseUp = (e) => {
|
||||
clearInterval(interval);
|
||||
e.preventDefault();
|
||||
interval && clearInterval(interval);
|
||||
isHolding = false;
|
||||
};
|
||||
const onContextMenu = (e) => e.preventDefault();
|
||||
$wrapper.setValue = (value2) => {
|
||||
$text.textContent = value2 + options.suffix;
|
||||
$range && ($range.value = value2);
|
||||
};
|
||||
$decBtn.addEventListener("click", onClick);
|
||||
$decBtn.addEventListener("mousedown", onMouseDown);
|
||||
$decBtn.addEventListener("mouseup", onMouseUp);
|
||||
$decBtn.addEventListener("touchstart", onMouseDown);
|
||||
$decBtn.addEventListener("touchend", onMouseUp);
|
||||
$decBtn.addEventListener("pointerdown", onMouseDown);
|
||||
$decBtn.addEventListener("pointerup", onMouseUp);
|
||||
$decBtn.addEventListener("contextmenu", onContextMenu);
|
||||
$incBtn.addEventListener("click", onClick);
|
||||
$incBtn.addEventListener("mousedown", onMouseDown);
|
||||
$incBtn.addEventListener("mouseup", onMouseUp);
|
||||
$incBtn.addEventListener("touchstart", onMouseDown);
|
||||
$incBtn.addEventListener("touchend", onMouseUp);
|
||||
$incBtn.addEventListener("pointerdown", onMouseDown);
|
||||
$incBtn.addEventListener("pointerup", onMouseUp);
|
||||
$incBtn.addEventListener("contextmenu", onContextMenu);
|
||||
return $wrapper;
|
||||
}
|
||||
static #METHOD_MAP = {
|
||||
@ -3593,21 +3635,19 @@ var UserAgentProfile;
|
||||
UserAgentProfile2["CUSTOM"] = "custom";
|
||||
})(UserAgentProfile || (UserAgentProfile = {}));
|
||||
var CHROMIUM_VERSION = "123.0.0.0";
|
||||
if (!!window.chrome) {
|
||||
if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) {
|
||||
const match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/);
|
||||
if (match) {
|
||||
CHROMIUM_VERSION = match[1];
|
||||
}
|
||||
}
|
||||
var EDGE_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/[[VERSION]] Safari/537.36 Edg/[[VERSION]]";
|
||||
EDGE_USER_AGENT = EDGE_USER_AGENT.replaceAll("[[VERSION]]", CHROMIUM_VERSION);
|
||||
|
||||
class UserAgent {
|
||||
static #USER_AGENTS = {
|
||||
[UserAgentProfile.EDGE_WINDOWS]: EDGE_USER_AGENT,
|
||||
[UserAgentProfile.EDGE_WINDOWS]: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,
|
||||
[UserAgentProfile.SAFARI_MACOS]: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1",
|
||||
[UserAgentProfile.SMARTTV]: window.navigator.userAgent + " SmartTV",
|
||||
[UserAgentProfile.SMARTTV_TIZEN]: "Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) 94.0.4606.31/7.0 TV Safari/537.36",
|
||||
[UserAgentProfile.SMARTTV_TIZEN]: `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36`,
|
||||
[UserAgentProfile.VR_OCULUS]: window.navigator.userAgent + " OculusBrowser VR",
|
||||
[UserAgentProfile.KIWI_V123]: "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.118 Mobile Safari/537.36"
|
||||
};
|
||||
@ -3862,7 +3902,7 @@ class BxLogger {
|
||||
BxLogger.#log(TextColor.ERROR, tag, ...args);
|
||||
}
|
||||
static #log(color, tag, ...args) {
|
||||
console.log("%c" + BxLogger.#PREFIX, "color:" + color + ";font-weight:bold;", tag, "-", ...args);
|
||||
console.log(`%c${BxLogger.#PREFIX}`, `color:${color};font-weight:bold;`, tag, "//", ...args);
|
||||
}
|
||||
}
|
||||
window.BxLogger = BxLogger;
|
||||
@ -4122,6 +4162,7 @@ var PrefKey;
|
||||
PrefKey2["STREAM_COMBINE_SOURCES"] = "stream_combine_sources";
|
||||
PrefKey2["STREAM_TOUCH_CONTROLLER"] = "stream_touch_controller";
|
||||
PrefKey2["STREAM_TOUCH_CONTROLLER_AUTO_OFF"] = "stream_touch_controller_auto_off";
|
||||
PrefKey2["STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY"] = "stream_touch_controller_default_opacity";
|
||||
PrefKey2["STREAM_TOUCH_CONTROLLER_STYLE_STANDARD"] = "stream_touch_controller_style_standard";
|
||||
PrefKey2["STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM"] = "stream_touch_controller_style_custom";
|
||||
PrefKey2["STREAM_DISABLE_FEEDBACK_DIALOG"] = "stream_disable_feedback_dialog";
|
||||
@ -4343,6 +4384,20 @@ class Preferences {
|
||||
default: false,
|
||||
unsupported: !STATES.hasTouchSupport
|
||||
},
|
||||
[PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY]: {
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
label: t("tc-default-opacity"),
|
||||
default: 100,
|
||||
min: 10,
|
||||
max: 100,
|
||||
steps: 10,
|
||||
params: {
|
||||
suffix: "%",
|
||||
ticks: 10,
|
||||
hideSlider: true
|
||||
},
|
||||
unsupported: !STATES.hasTouchSupport
|
||||
},
|
||||
[PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: {
|
||||
label: t("tc-standard-layout-style"),
|
||||
default: "default",
|
||||
@ -4814,6 +4869,24 @@ var BxExposed = {
|
||||
STATES.currentStream.titleInfo = titleInfo;
|
||||
BxEvent.dispatch(window, BxEvent.TITLE_INFO_READY);
|
||||
return titleInfo;
|
||||
},
|
||||
setupGainNode: ($media, audioStream) => {
|
||||
if ($media instanceof HTMLAudioElement) {
|
||||
$media.muted = true;
|
||||
$media.addEventListener("playing", (e) => {
|
||||
$media.muted = true;
|
||||
$media.pause();
|
||||
});
|
||||
} else {
|
||||
$media.muted = true;
|
||||
$media.addEventListener("playing", (e) => {
|
||||
$media.muted = true;
|
||||
});
|
||||
}
|
||||
const audioCtx = STATES.currentStream.audioContext;
|
||||
const source = audioCtx.createMediaStreamSource(audioStream);
|
||||
const gainNode = audioCtx.createGain();
|
||||
source.connect(gainNode).connect(audioCtx.destination);
|
||||
}
|
||||
};
|
||||
|
||||
@ -5054,8 +5127,12 @@ class Toast {
|
||||
Toast.#isShowing = true;
|
||||
Toast.#timeout && clearTimeout(Toast.#timeout);
|
||||
Toast.#timeout = window.setTimeout(Toast.#hide, Toast.#DURATION);
|
||||
const [msg, status, _] = Toast.#stack.shift();
|
||||
Toast.#$msg.textContent = msg;
|
||||
const [msg, status, options] = Toast.#stack.shift();
|
||||
if (options.html) {
|
||||
Toast.#$msg.innerHTML = msg;
|
||||
} else {
|
||||
Toast.#$msg.textContent = msg;
|
||||
}
|
||||
if (status) {
|
||||
Toast.#$status.classList.remove("bx-gone");
|
||||
Toast.#$status.textContent = status;
|
||||
@ -5875,7 +5952,7 @@ class MkbHandler {
|
||||
e.stopPropagation();
|
||||
showStreamSettings("mkb");
|
||||
}
|
||||
}), CE("div", {}, CE("p", {}, t("mkb-click-to-activate")), CE("p", {}, t("press-key-to-toggle-mkb")({ key: "F8" }))));
|
||||
}), CE("div", {}, CE("p", {}, t("mkb-click-to-activate")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))));
|
||||
this.#$message.addEventListener("click", this.#onActivatePointerLock);
|
||||
document.documentElement.appendChild(this.#$message);
|
||||
window.addEventListener(BxEvent.STREAM_MENU_SHOWN, this.#onStreamMenuShown);
|
||||
@ -6436,7 +6513,10 @@ function takeScreenshot(callback) {
|
||||
if (!$video || !$canvas) {
|
||||
return;
|
||||
}
|
||||
const $canvasContext = $canvas.getContext("2d");
|
||||
const $canvasContext = $canvas.getContext("2d", {
|
||||
alpha: false,
|
||||
willReadFrequently: false
|
||||
});
|
||||
$canvasContext.drawImage($video, 0, 0, $canvas.width, $canvas.height);
|
||||
if (AppInterface) {
|
||||
const data = $canvas.toDataURL("image/png").split(";base64,")[1];
|
||||
@ -6604,7 +6684,7 @@ class TouchController {
|
||||
}
|
||||
}
|
||||
static loadCustomLayout(xboxTitleId, layoutId, delay = 0) {
|
||||
if (!window.BX_EXPOSED.touch_layout_manager) {
|
||||
if (!window.BX_EXPOSED.touchLayoutManager) {
|
||||
return;
|
||||
}
|
||||
const layoutChanged = TouchController.#currentLayoutId !== layoutId;
|
||||
@ -6618,9 +6698,18 @@ class TouchController {
|
||||
if (!layout) {
|
||||
return;
|
||||
}
|
||||
layoutChanged && Toast.show(t("touch-control-layout"), layout.name);
|
||||
let msg;
|
||||
let html13 = false;
|
||||
if (layout.author) {
|
||||
const author = `<b>${escapeHtml(layout.author)}</b>`;
|
||||
msg = t("touch-control-layout-by", { name: author });
|
||||
html13 = true;
|
||||
} else {
|
||||
msg = t("touch-control-layout");
|
||||
}
|
||||
layoutChanged && Toast.show(msg, layout.name, { html: html13 });
|
||||
window.setTimeout(() => {
|
||||
window.BX_EXPOSED.touch_layout_manager.changeLayoutForScope({
|
||||
window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({
|
||||
type: "showLayout",
|
||||
scope: xboxTitleId,
|
||||
subscope: "base",
|
||||
@ -6632,10 +6721,18 @@ class TouchController {
|
||||
});
|
||||
}, delay);
|
||||
}
|
||||
static updateCustomList() {
|
||||
NATIVE_FETCH("https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/ids.json").then((response) => response.json()).then((json) => {
|
||||
window.localStorage.setItem("better_xcloud_custom_touch_layouts", JSON.stringify(json));
|
||||
});
|
||||
}
|
||||
static getCustomList() {
|
||||
return JSON.parse(window.localStorage.getItem("better_xcloud_custom_touch_layouts") || "[]");
|
||||
}
|
||||
static setup() {
|
||||
window.BX_EXPOSED.test_touch_control = (layout) => {
|
||||
const { touch_layout_manager } = window.BX_EXPOSED;
|
||||
touch_layout_manager && touch_layout_manager.changeLayoutForScope({
|
||||
window.testTouchLayout = (layout) => {
|
||||
const { touchLayoutManager } = window.BX_EXPOSED;
|
||||
touchLayoutManager && touchLayoutManager.changeLayoutForScope({
|
||||
type: "showLayout",
|
||||
scope: "" + STATES.currentStream?.xboxTitleId,
|
||||
subscope: "base",
|
||||
@ -7010,7 +7107,13 @@ var setupQuickSettingsBar = function() {
|
||||
const $fragment = document.createDocumentFragment();
|
||||
for (const key in data.layouts) {
|
||||
const layout = data.layouts[key];
|
||||
const $option = CE("option", { value: key }, layout.name);
|
||||
let name;
|
||||
if (layout.author) {
|
||||
name = `${layout.name} (${layout.author})`;
|
||||
} else {
|
||||
name = layout.name;
|
||||
}
|
||||
const $option = CE("option", { value: key }, name);
|
||||
$fragment.appendChild($option);
|
||||
}
|
||||
$elm.appendChild($fragment);
|
||||
@ -7470,6 +7573,13 @@ class RemotePlay {
|
||||
}
|
||||
}
|
||||
|
||||
// src/utils/gamepass-gallery.ts
|
||||
var GamePassCloudGallery;
|
||||
(function(GamePassCloudGallery2) {
|
||||
GamePassCloudGallery2["TOUCH"] = "9c86f07a-f3e8-45ad-82a0-a1f759597059";
|
||||
GamePassCloudGallery2["ALL"] = "29a81209-df6f-41fd-a528-2ae6b91f719c";
|
||||
})(GamePassCloudGallery || (GamePassCloudGallery = {}));
|
||||
|
||||
// src/utils/network.ts
|
||||
var clearApplicationInsightsBuffers = function() {
|
||||
window.sessionStorage.removeItem("AI_buffer");
|
||||
@ -7591,6 +7701,7 @@ function interceptHttpRequests() {
|
||||
}
|
||||
return nativeXhrSend.apply(this, arguments);
|
||||
};
|
||||
let gamepassAllGames = [];
|
||||
window.BX_FETCH = window.fetch = async (request, init) => {
|
||||
let url = typeof request === "string" ? request : request.url;
|
||||
for (let blocked of BLOCKED_URLS) {
|
||||
@ -7608,6 +7719,25 @@ function interceptHttpRequests() {
|
||||
if (url.endsWith("/configuration")) {
|
||||
BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
|
||||
}
|
||||
if (STATES.hasTouchSupport && url.includes("catalog.gamepass.com/sigls/")) {
|
||||
const response = await NATIVE_FETCH(request, init);
|
||||
const obj = await response.clone().json();
|
||||
if (url.includes(GamePassCloudGallery.ALL)) {
|
||||
for (let i = 1;i < obj.length; i++) {
|
||||
gamepassAllGames.push(obj[i].id);
|
||||
}
|
||||
} else if (url.includes(GamePassCloudGallery.TOUCH)) {
|
||||
try {
|
||||
let customList = TouchController.getCustomList();
|
||||
customList = customList.filter((id2) => gamepassAllGames.includes(id2));
|
||||
const newCustomList = customList.map((item2) => ({ id: item2 }));
|
||||
obj.push(...newCustomList);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
response.json = () => Promise.resolve(obj);
|
||||
return response;
|
||||
}
|
||||
let requestType;
|
||||
if (url.includes("/sessions/home") || url.includes("xhome.") || STATES.remotePlay.isPlaying && url.endsWith("/inputconfigs")) {
|
||||
requestType = RequestType.XHOME;
|
||||
@ -8527,9 +8657,15 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
||||
.bx-stream-refresh-button {
|
||||
top: calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important;
|
||||
}
|
||||
body[data-media-type=default] .bx-stream-refresh-button {
|
||||
left: calc(env(safe-area-inset-left, 0px) + 11px) !important;
|
||||
}
|
||||
body[data-media-type=tv] .bx-stream-refresh-button {
|
||||
top: calc(var(--gds-focus-borderSize) + 80px) !important;
|
||||
}
|
||||
.bx-number-stepper {
|
||||
text-align: center;
|
||||
}
|
||||
.bx-number-stepper span {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
@ -8561,6 +8697,12 @@ body[data-media-type=tv] .bx-stream-refresh-button {
|
||||
.bx-number-stepper button:disabled + span {
|
||||
font-family: var(--bx-title-font);
|
||||
}
|
||||
.bx-number-stepper input[type="range"] {
|
||||
display: block;
|
||||
margin: 12px auto 2px;
|
||||
width: 180px;
|
||||
color: #959595 !important;
|
||||
}
|
||||
.bx-number-stepper input[type=range]:disabled,
|
||||
.bx-number-stepper button:disabled {
|
||||
display: none;
|
||||
@ -8807,12 +8949,6 @@ body[data-media-type=tv] .bx-stream-refresh-button {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bx-quick-settings-tab-contents input[type="range"] {
|
||||
display: block;
|
||||
margin: 12px auto 2px;
|
||||
width: 180px;
|
||||
color: #959595 !important;
|
||||
}
|
||||
.bx-quick-settings-row {
|
||||
display: flex;
|
||||
border-bottom: 1px solid rgba(64,64,64,0.502);
|
||||
@ -9206,11 +9342,12 @@ var PATCHES = {
|
||||
return str2.replace(text, newCode + ";" + text);
|
||||
},
|
||||
disableIndexDbLogging(str2) {
|
||||
const text = "async addLog(e,t=1e4){";
|
||||
const text = ",this.logsDb=new";
|
||||
if (!str2.includes(text)) {
|
||||
return false;
|
||||
}
|
||||
return str2.replace(text, text + "return;");
|
||||
let newCode = ",this.log=()=>{}";
|
||||
return str2.replace(text, newCode + text);
|
||||
},
|
||||
websiteLayout(str2) {
|
||||
const text = '?"tv":"default"';
|
||||
@ -9374,7 +9511,7 @@ if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) {
|
||||
if (!str2.includes(text)) {
|
||||
return false;
|
||||
}
|
||||
str2 = str2.replace(text, 'window.BX_EXPOSED["touch_layout_manager"] = this,' + text);
|
||||
str2 = str2.replace(text, 'window.BX_EXPOSED["touchLayoutManager"] = this,' + text);
|
||||
return str2;
|
||||
},
|
||||
supportLocalCoOp(str2) {
|
||||
@ -9515,6 +9652,34 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar});
|
||||
`;
|
||||
str2 = str2.substring(0, backetIndex + 1) + newCode + str2.substring(backetIndex + 1);
|
||||
return str2;
|
||||
},
|
||||
patchAudioMediaStream(str2) {
|
||||
const text = ".srcObject=this.audioMediaStream,";
|
||||
if (!str2.includes(text)) {
|
||||
return false;
|
||||
}
|
||||
const newCode = `window.BX_EXPOSED.setupGainNode(arguments[1], this.audioMediaStream),`;
|
||||
str2 = str2.replace(text, text + newCode);
|
||||
return str2;
|
||||
},
|
||||
patchCombinedAudioVideoMediaStream(str2) {
|
||||
const text = ".srcObject=this.combinedAudioVideoStream";
|
||||
if (!str2.includes(text)) {
|
||||
return false;
|
||||
}
|
||||
const newCode = `,window.BX_EXPOSED.setupGainNode(arguments[0], this.combinedAudioVideoStream)`;
|
||||
str2 = str2.replace(text, text + newCode);
|
||||
return str2;
|
||||
},
|
||||
patchTouchControlDefaultOpacity(str2) {
|
||||
const text = "opacityMultiplier:1";
|
||||
if (!str2.includes(text)) {
|
||||
return false;
|
||||
}
|
||||
const opacity = (getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) / 100).toFixed(1);
|
||||
const newCode = `opacityMultiplier: ${opacity}`;
|
||||
str2 = str2.replace(text, newCode);
|
||||
return str2;
|
||||
}
|
||||
};
|
||||
var PATCH_ORDERS = [
|
||||
@ -9548,8 +9713,11 @@ var PLAYING_PATCH_ORDERS = [
|
||||
"disableGamepadDisconnectedScreen",
|
||||
"patchStreamHud",
|
||||
"playVibration",
|
||||
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && !getPref(PrefKey.STREAM_COMBINE_SOURCES) && "patchAudioMediaStream",
|
||||
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && getPref(PrefKey.STREAM_COMBINE_SOURCES) && "patchCombinedAudioVideoMediaStream",
|
||||
STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all" && "exposeTouchLayoutManager",
|
||||
STATES.hasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "off" || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && "disableTakRenderer",
|
||||
STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && "patchTouchControlDefaultOpacity",
|
||||
BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging",
|
||||
getPref(PrefKey.BLOCK_TRACKING) && "blockGamepadStatsCollector",
|
||||
getPref(PrefKey.STREAM_COMBINE_SOURCES) && "streamCombineSources",
|
||||
@ -9619,7 +9787,7 @@ class Patcher {
|
||||
}
|
||||
modified = true;
|
||||
str = patchedStr;
|
||||
BxLogger.info(LOG_TAG4, `Applied "${patchName}" patch`);
|
||||
BxLogger.info(LOG_TAG4, `✅ ${patchName}`);
|
||||
appliedPatches.push(patchName);
|
||||
patchesToCheck.splice(patchIndex, 1);
|
||||
patchIndex--;
|
||||
@ -9944,6 +10112,7 @@ var SETTINGS_UI = {
|
||||
items: [
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM
|
||||
]
|
||||
@ -10062,25 +10231,42 @@ function onHistoryChanged(e) {
|
||||
BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
|
||||
}
|
||||
|
||||
// src/utils/titles-info.ts
|
||||
class PreloadedState {
|
||||
static override() {
|
||||
Object.defineProperty(window, "__PRELOADED_STATE__", {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
const userAgent = UserAgent.spoof();
|
||||
if (userAgent) {
|
||||
this._state.appContext.requestInfo.userAgent = userAgent;
|
||||
// src/utils/preload-state.ts
|
||||
function overridePreloadState() {
|
||||
let _state;
|
||||
Object.defineProperty(window, "__PRELOADED_STATE__", {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
return _state;
|
||||
},
|
||||
set: (state) => {
|
||||
const userAgent = UserAgent.spoof();
|
||||
if (userAgent) {
|
||||
try {
|
||||
state.appContext.requestInfo.userAgent = userAgent;
|
||||
} catch (e) {
|
||||
BxLogger.error(LOG_TAG5, e);
|
||||
}
|
||||
return this._state;
|
||||
},
|
||||
set: (state) => {
|
||||
this._state = state;
|
||||
STATES.appContext = structuredClone(state.appContext);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (STATES.hasTouchSupport) {
|
||||
try {
|
||||
const sigls = state.xcloud.sigls;
|
||||
if (GamePassCloudGallery.TOUCH in sigls) {
|
||||
let customList = TouchController.getCustomList();
|
||||
const allGames = sigls[GamePassCloudGallery.ALL].data.products;
|
||||
customList = customList.filter((id2) => allGames.includes(id2));
|
||||
sigls[GamePassCloudGallery.TOUCH]?.data.products.push(...customList);
|
||||
}
|
||||
} catch (e) {
|
||||
BxLogger.error(LOG_TAG5, e);
|
||||
}
|
||||
}
|
||||
_state = state;
|
||||
STATES.appContext = structuredClone(state.appContext);
|
||||
}
|
||||
});
|
||||
}
|
||||
var LOG_TAG5 = "PreloadState";
|
||||
|
||||
// src/utils/monkey-patches.ts
|
||||
function patchVideoApi() {
|
||||
@ -10157,47 +10343,76 @@ function patchRtcPeerConnection() {
|
||||
const conn = new OrgRTCPeerConnection;
|
||||
STATES.currentStream.peerConnection = conn;
|
||||
conn.addEventListener("connectionstatechange", (e) => {
|
||||
if (conn.connectionState === "connecting") {
|
||||
STATES.currentStream.audioGainNode = null;
|
||||
}
|
||||
BxLogger.info("connectionstatechange", conn.connectionState);
|
||||
});
|
||||
return conn;
|
||||
};
|
||||
}
|
||||
function patchAudioContext() {
|
||||
if (UserAgent.isSafari(true)) {
|
||||
const nativeCreateGain = window.AudioContext.prototype.createGain;
|
||||
window.AudioContext.prototype.createGain = function() {
|
||||
const OrgAudioContext = window.AudioContext;
|
||||
const nativeCreateGain = OrgAudioContext.prototype.createGain;
|
||||
window.AudioContext = function(options) {
|
||||
const ctx = new OrgAudioContext(options);
|
||||
BxLogger.info("patchAudioContext", ctx, options);
|
||||
ctx.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;
|
||||
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;
|
||||
}
|
||||
function patchMeControl() {
|
||||
const overrideConfigs = {
|
||||
enableAADTelemetry: false,
|
||||
enableTelemetry: false,
|
||||
telEvs: "",
|
||||
oneDSUrl: ""
|
||||
};
|
||||
const MSA = {
|
||||
MeControl: {}
|
||||
};
|
||||
const MeControl = {};
|
||||
const MsaHandler = {
|
||||
get(target, prop, receiver) {
|
||||
return target[prop];
|
||||
},
|
||||
set(obj, prop, value) {
|
||||
if (prop === "MeControl" && value.Config) {
|
||||
value.Config = Object.assign(value.Config, overrideConfigs);
|
||||
}
|
||||
obj[prop] = value;
|
||||
return true;
|
||||
}
|
||||
this.addEventListener("playing", (e) => e.target.pause());
|
||||
const audioCtx = STATES.currentStream.audioContext;
|
||||
const audioStream = audioCtx.createMediaStreamSource(this.srcObject);
|
||||
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;
|
||||
};
|
||||
const MeControlHandler = {
|
||||
get(target, prop, receiver) {
|
||||
return target[prop];
|
||||
},
|
||||
set(obj, prop, value) {
|
||||
if (prop === "Config") {
|
||||
value = Object.assign(value, overrideConfigs);
|
||||
}
|
||||
obj[prop] = value;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
window.MSA = new Proxy(MSA, MsaHandler);
|
||||
window.MeControl = new Proxy(MeControl, MeControlHandler);
|
||||
}
|
||||
function patchCanvasContext() {
|
||||
const nativeGetContext = HTMLCanvasElement.prototype.getContext;
|
||||
HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) {
|
||||
if (contextType.includes("webgl")) {
|
||||
contextAttributes = contextAttributes || {};
|
||||
contextAttributes.antialias = false;
|
||||
if (contextAttributes.powerPreference === "high-performance") {
|
||||
contextAttributes.powerPreference = "low-power";
|
||||
}
|
||||
}
|
||||
return nativeGetContext.apply(this, [contextType, contextAttributes]);
|
||||
};
|
||||
}
|
||||
|
||||
@ -10207,10 +10422,11 @@ var main = function() {
|
||||
patchRtcCodecs();
|
||||
interceptHttpRequests();
|
||||
patchVideoApi();
|
||||
if (getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL)) {
|
||||
patchAudioContext();
|
||||
}
|
||||
PreloadedState.override();
|
||||
patchCanvasContext();
|
||||
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext();
|
||||
getPref(PrefKey.BLOCK_TRACKING) && patchMeControl();
|
||||
STATES.hasTouchSupport && TouchController.updateCustomList();
|
||||
overridePreloadState();
|
||||
VibrationManager.initialSetup();
|
||||
BX_FLAGS.CheckForUpdate && checkForUpdate();
|
||||
addCss();
|
||||
|
@ -1,4 +1,6 @@
|
||||
.bx-number-stepper {
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
@ -35,6 +37,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
display: block;
|
||||
margin: 12px auto 2px;
|
||||
width: 180px;
|
||||
color: #959595 !important;
|
||||
}
|
||||
|
||||
input[type=range]:disabled, button:disabled {
|
||||
display: none;
|
||||
}
|
||||
|
@ -86,13 +86,6 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
display: block;
|
||||
margin: 12px auto 2px;
|
||||
width: 180px;
|
||||
color: #959595 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -12,6 +12,10 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
||||
top: calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important;
|
||||
}
|
||||
|
||||
body[data-media-type=default] .bx-stream-refresh-button {
|
||||
left: calc(env(safe-area-inset-left, 0px) + 11px) !important;
|
||||
}
|
||||
|
||||
body[data-media-type=tv] .bx-stream-refresh-button {
|
||||
top: calc(var(--gds-focus-borderSize) + 80px) !important;
|
||||
}
|
||||
|
13
src/index.ts
13
src/index.ts
@ -22,8 +22,8 @@ import { Patcher } from "@modules/patcher";
|
||||
import { RemotePlay } from "@modules/remote-play";
|
||||
import { onHistoryChanged, patchHistoryMethod } from "@utils/history";
|
||||
import { VibrationManager } from "@modules/vibration-manager";
|
||||
import { PreloadedState } from "@utils/titles-info";
|
||||
import { patchAudioContext, patchRtcCodecs, patchRtcPeerConnection, patchVideoApi } from "@utils/monkey-patches";
|
||||
import { overridePreloadState } from "@utils/preload-state";
|
||||
import { patchAudioContext, patchCanvasContext, patchMeControl, patchRtcCodecs, patchRtcPeerConnection, patchVideoApi } from "@utils/monkey-patches";
|
||||
import { STATES } from "@utils/global";
|
||||
import { injectStreamMenuButtons } from "@modules/stream/stream-ui";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
@ -215,12 +215,13 @@ function main() {
|
||||
patchRtcCodecs();
|
||||
interceptHttpRequests();
|
||||
patchVideoApi();
|
||||
patchCanvasContext();
|
||||
|
||||
if (getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL)) {
|
||||
patchAudioContext();
|
||||
}
|
||||
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext();
|
||||
getPref(PrefKey.BLOCK_TRACKING) && patchMeControl();
|
||||
|
||||
PreloadedState.override();
|
||||
STATES.hasTouchSupport && TouchController.updateCustomList();
|
||||
overridePreloadState();
|
||||
|
||||
VibrationManager.initialSetup();
|
||||
|
||||
|
@ -393,7 +393,7 @@ export class MkbHandler {
|
||||
}),
|
||||
CE('div', {},
|
||||
CE('p', {}, t('mkb-click-to-activate')),
|
||||
CE('p', {}, t<any>('press-key-to-toggle-mkb')({key: 'F8'})),
|
||||
CE('p', {}, t('press-key-to-toggle-mkb', {key: 'F8'})),
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { BX_FLAGS } from "@utils/bx-flags";
|
||||
import { getPref, PrefKey } from "@utils/preferences";
|
||||
import { VibrationManager } from "@modules/vibration-manager";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
import { hashCode } from "@/utils/utils";
|
||||
import { hashCode } from "@utils/utils";
|
||||
|
||||
type PatchArray = (keyof typeof PATCHES)[];
|
||||
|
||||
@ -59,12 +59,14 @@ const PATCHES = {
|
||||
|
||||
// Disable IndexDB logging
|
||||
disableIndexDbLogging(str: string) {
|
||||
const text = 'async addLog(e,t=1e4){';
|
||||
const text = ',this.logsDb=new';
|
||||
if (!str.includes(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str.replace(text, text + 'return;');
|
||||
// Replace log() with an empty function
|
||||
let newCode = ',this.log=()=>{}';
|
||||
return str.replace(text, newCode + text);
|
||||
},
|
||||
|
||||
// Set custom website layout
|
||||
@ -283,7 +285,7 @@ if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
str = str.replace(text, 'window.BX_EXPOSED["touch_layout_manager"] = this,' + text);
|
||||
str = str.replace(text, 'window.BX_EXPOSED["touchLayoutManager"] = this,' + text);
|
||||
return str;
|
||||
},
|
||||
|
||||
@ -459,6 +461,41 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar});
|
||||
return str;
|
||||
|
||||
},
|
||||
|
||||
patchAudioMediaStream(str: string) {
|
||||
const text = '.srcObject=this.audioMediaStream,';
|
||||
if (!str.includes(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newCode = `window.BX_EXPOSED.setupGainNode(arguments[1], this.audioMediaStream),`;
|
||||
|
||||
str = str.replace(text, text + newCode);
|
||||
return str;
|
||||
},
|
||||
|
||||
patchCombinedAudioVideoMediaStream(str: string) {
|
||||
const text = '.srcObject=this.combinedAudioVideoStream';
|
||||
if (!str.includes(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newCode = `,window.BX_EXPOSED.setupGainNode(arguments[0], this.combinedAudioVideoStream)`;
|
||||
str = str.replace(text, text + newCode);
|
||||
return str;
|
||||
},
|
||||
|
||||
patchTouchControlDefaultOpacity(str: string) {
|
||||
const text = 'opacityMultiplier:1';
|
||||
if (!str.includes(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const opacity = (getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) / 100).toFixed(1);
|
||||
const newCode = `opacityMultiplier: ${opacity}`;
|
||||
str = str.replace(text, newCode);
|
||||
return str;
|
||||
},
|
||||
};
|
||||
|
||||
let PATCH_ORDERS: PatchArray = [
|
||||
@ -501,8 +538,15 @@ let PLAYING_PATCH_ORDERS: PatchArray = [
|
||||
'patchStreamHud',
|
||||
'playVibration',
|
||||
|
||||
// Patch volume control for normal stream
|
||||
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && !getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchAudioMediaStream',
|
||||
// Patch volume control for combined audio+video stream
|
||||
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchCombinedAudioVideoMediaStream',
|
||||
|
||||
|
||||
STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all' && 'exposeTouchLayoutManager',
|
||||
STATES.hasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off' || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && 'disableTakRenderer',
|
||||
STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity',
|
||||
|
||||
BX_FLAGS.EnableXcloudLogging && 'enableConsoleLogging',
|
||||
|
||||
@ -564,7 +608,7 @@ export class Patcher {
|
||||
let patchesToCheck: PatchArray;
|
||||
let appliedPatches: PatchArray;
|
||||
|
||||
const patchesMap: { [key: string]: PatchArray } = {};
|
||||
const patchesMap: Record<string, PatchArray> = {};
|
||||
|
||||
for (let id in item[1]) {
|
||||
appliedPatches = [];
|
||||
@ -608,7 +652,7 @@ export class Patcher {
|
||||
modified = true;
|
||||
str = patchedStr;
|
||||
|
||||
BxLogger.info(LOG_TAG, `Applied "${patchName}" patch`);
|
||||
BxLogger.info(LOG_TAG, `✅ ${patchName}`);
|
||||
appliedPatches.push(patchName);
|
||||
|
||||
// Remove patch
|
||||
@ -698,7 +742,7 @@ export class PatcherCache {
|
||||
return PatcherCache.#CACHE[id];
|
||||
}
|
||||
|
||||
static saveToCache(subCache: { [key: string]: PatchArray }) {
|
||||
static saveToCache(subCache: Record<string, PatchArray>) {
|
||||
for (const id in subCache) {
|
||||
const patchNames = subCache[id];
|
||||
|
||||
|
@ -9,7 +9,10 @@ export function takeScreenshot(callback: any) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $canvasContext = $canvas.getContext('2d')!;
|
||||
const $canvasContext = $canvas.getContext('2d', {
|
||||
alpha: false,
|
||||
willReadFrequently: false,
|
||||
})!;
|
||||
|
||||
$canvasContext.drawImage($video, 0, 0, $canvas.width, $canvas.height);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { STATES } from "@utils/global";
|
||||
import { CE } from "@utils/html";
|
||||
import { CE, escapeHtml } from "@utils/html";
|
||||
import { Toast } from "@utils/toast";
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { BX_FLAGS } from "@utils/bx-flags";
|
||||
@ -103,7 +103,7 @@ export class TouchController {
|
||||
retries = retries || 1;
|
||||
if (retries > 2) {
|
||||
TouchController.#customLayouts[xboxTitleId] = null;
|
||||
// Wait for BX_EXPOSED.touch_layout_manager
|
||||
// Wait for BX_EXPOSED.touchLayoutManager
|
||||
window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000);
|
||||
return;
|
||||
}
|
||||
@ -139,7 +139,7 @@ export class TouchController {
|
||||
json.layouts = layouts;
|
||||
TouchController.#customLayouts[xboxTitleId] = json;
|
||||
|
||||
// Wait for BX_EXPOSED.touch_layout_manager
|
||||
// Wait for BX_EXPOSED.touchLayoutManager
|
||||
window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000);
|
||||
} catch (e) {
|
||||
// Retry
|
||||
@ -148,7 +148,7 @@ export class TouchController {
|
||||
}
|
||||
|
||||
static loadCustomLayout(xboxTitleId: string, layoutId: string, delay: number=0) {
|
||||
if (!window.BX_EXPOSED.touch_layout_manager) {
|
||||
if (!window.BX_EXPOSED.touchLayoutManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -168,10 +168,20 @@ export class TouchController {
|
||||
}
|
||||
|
||||
// Show a toast with layout's name
|
||||
layoutChanged && Toast.show(t('touch-control-layout'), layout.name);
|
||||
let msg: string;
|
||||
let html = false;
|
||||
if (layout.author) {
|
||||
const author = `<b>${escapeHtml(layout.author)}</b>`;
|
||||
msg = t('touch-control-layout-by', {name: author});
|
||||
html = true;
|
||||
} else {
|
||||
msg = t('touch-control-layout');
|
||||
}
|
||||
|
||||
layoutChanged && Toast.show(msg, layout.name, {html: html});
|
||||
|
||||
window.setTimeout(() => {
|
||||
window.BX_EXPOSED.touch_layout_manager.changeLayoutForScope({
|
||||
window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({
|
||||
type: 'showLayout',
|
||||
scope: xboxTitleId,
|
||||
subscope: 'base',
|
||||
@ -184,12 +194,24 @@ export class TouchController {
|
||||
}, delay);
|
||||
}
|
||||
|
||||
static updateCustomList() {
|
||||
NATIVE_FETCH('https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/ids.json')
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
window.localStorage.setItem('better_xcloud_custom_touch_layouts', JSON.stringify(json));
|
||||
});
|
||||
}
|
||||
|
||||
static getCustomList(): string[] {
|
||||
return JSON.parse(window.localStorage.getItem('better_xcloud_custom_touch_layouts') || '[]');
|
||||
}
|
||||
|
||||
static setup() {
|
||||
// Function for testing touch control
|
||||
window.BX_EXPOSED.test_touch_control = (layout: any) => {
|
||||
const { touch_layout_manager } = window.BX_EXPOSED;
|
||||
(window as any).testTouchLayout = (layout: any) => {
|
||||
const { touchLayoutManager } = window.BX_EXPOSED;
|
||||
|
||||
touch_layout_manager && touch_layout_manager.changeLayoutForScope({
|
||||
touchLayoutManager && touchLayoutManager.changeLayoutForScope({
|
||||
type: 'showLayout',
|
||||
scope: '' + STATES.currentStream?.xboxTitleId,
|
||||
subscope: 'base',
|
||||
|
@ -58,6 +58,7 @@ const SETTINGS_UI = {
|
||||
items: [
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM,
|
||||
],
|
||||
|
@ -217,7 +217,14 @@ function setupQuickSettingsBar() {
|
||||
for (const key in data.layouts) {
|
||||
const layout = data.layouts[key];
|
||||
|
||||
const $option = CE('option', {value: key}, layout.name);
|
||||
let name;
|
||||
if (layout.author) {
|
||||
name = `${layout.name} (${layout.author})`;
|
||||
} else {
|
||||
name = layout.name;
|
||||
}
|
||||
|
||||
const $option = CE('option', {value: key}, name);
|
||||
$fragment.appendChild($option);
|
||||
}
|
||||
|
||||
|
2
src/types/index.d.ts
vendored
2
src/types/index.d.ts
vendored
@ -1,6 +1,8 @@
|
||||
// Get type of an array's element
|
||||
type ArrayElement<ArrayType extends readonly unknown[]> = ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
|
||||
|
||||
type PartialRecord<K extends keyof any, T> = Partial<Record<K, T>>
|
||||
|
||||
interface Window {
|
||||
AppInterface: any;
|
||||
BX_FLAGS?: BxFlags;
|
||||
|
@ -91,5 +91,26 @@ export const BxExposed = {
|
||||
BxEvent.dispatch(window, BxEvent.TITLE_INFO_READY);
|
||||
|
||||
return titleInfo;
|
||||
},
|
||||
|
||||
setupGainNode: ($media: HTMLMediaElement, audioStream: MediaStream) => {
|
||||
if ($media instanceof HTMLAudioElement) {
|
||||
$media.muted = true;
|
||||
$media.addEventListener('playing', e => {
|
||||
$media.muted = true;
|
||||
$media.pause();
|
||||
});
|
||||
} else {
|
||||
$media.muted = true;
|
||||
$media.addEventListener('playing', e => {
|
||||
$media.muted = true;
|
||||
});
|
||||
}
|
||||
|
||||
const audioCtx = STATES.currentStream.audioContext!;
|
||||
const source = audioCtx.createMediaStreamSource(audioStream);
|
||||
|
||||
const gainNode = audioCtx.createGain(); // call monkey-patched createGain() in BxAudioContext
|
||||
source.connect(gainNode).connect(audioCtx.destination);
|
||||
}
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ export class BxLogger {
|
||||
}
|
||||
|
||||
static #log(color: TextColor, tag: string, ...args: any) {
|
||||
console.log('%c' + BxLogger.#PREFIX, 'color:' + color + ';font-weight:bold;', tag, '-', ...args);
|
||||
console.log(`%c${BxLogger.#PREFIX}`, `color:${color};font-weight:bold;`, tag, '//', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
|
4
src/utils/gamepass-gallery.ts
Normal file
4
src/utils/gamepass-gallery.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum GamePassCloudGallery {
|
||||
TOUCH = '9c86f07a-f3e8-45ad-82a0-a1f759597059',
|
||||
ALL = '29a81209-df6f-41fd-a528-2ae6b91f719c',
|
||||
}
|
@ -96,5 +96,13 @@ export const createButton = <T=HTMLButtonElement>(options: BxButton): T => {
|
||||
return $btn as T;
|
||||
}
|
||||
|
||||
export function escapeHtml(html: string): string {
|
||||
const text = document.createTextNode(html);
|
||||
const $span = document.createElement('span');
|
||||
$span.appendChild(text);
|
||||
|
||||
return $span.innerHTML;
|
||||
}
|
||||
|
||||
export const CTN = document.createTextNode.bind(document);
|
||||
window.BX_CE = createElement;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { getPref, PrefKey } from "@utils/preferences";
|
||||
import { STATES } from "@utils/global";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
|
||||
export function patchVideoApi() {
|
||||
@ -104,10 +103,6 @@ export function patchRtcPeerConnection() {
|
||||
STATES.currentStream.peerConnection = conn;
|
||||
|
||||
conn.addEventListener('connectionstatechange', e => {
|
||||
if (conn.connectionState === 'connecting') {
|
||||
STATES.currentStream.audioGainNode = null;
|
||||
}
|
||||
|
||||
BxLogger.info('connectionstatechange', conn.connectionState);
|
||||
});
|
||||
return conn;
|
||||
@ -115,46 +110,95 @@ export function patchRtcPeerConnection() {
|
||||
}
|
||||
|
||||
export function patchAudioContext() {
|
||||
if (UserAgent.isSafari(true)) {
|
||||
const nativeCreateGain = window.AudioContext.prototype.createGain;
|
||||
window.AudioContext.prototype.createGain = function() {
|
||||
const OrgAudioContext = window.AudioContext;
|
||||
const nativeCreateGain = OrgAudioContext.prototype.createGain;
|
||||
|
||||
// @ts-ignore
|
||||
window.AudioContext = function(options?: AudioContextOptions | undefined): AudioContext {
|
||||
const ctx = new OrgAudioContext(options);
|
||||
BxLogger.info('patchAudioContext', ctx, options);
|
||||
|
||||
ctx.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;
|
||||
/**
|
||||
* Disable telemetry flags in meversion.js
|
||||
*/
|
||||
export function patchMeControl() {
|
||||
const overrideConfigs = {
|
||||
enableAADTelemetry: false,
|
||||
enableTelemetry: false,
|
||||
telEvs: '',
|
||||
oneDSUrl: '',
|
||||
};
|
||||
|
||||
const promise = nativePlay.apply(this);
|
||||
if (STATES.currentStream.audioGainNode) {
|
||||
return promise;
|
||||
const MSA = {
|
||||
MeControl: {},
|
||||
};
|
||||
const MeControl = {};
|
||||
|
||||
const MsaHandler: ProxyHandler<any> = {
|
||||
get(target, prop, receiver) {
|
||||
return target[prop];
|
||||
},
|
||||
|
||||
set(obj, prop, value) {
|
||||
if (prop === 'MeControl' && value.Config) {
|
||||
value.Config = Object.assign(value.Config, overrideConfigs);
|
||||
}
|
||||
|
||||
obj[prop] = value;
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const MeControlHandler: ProxyHandler<any> = {
|
||||
get(target, prop, receiver) {
|
||||
return target[prop];
|
||||
},
|
||||
|
||||
set(obj, prop, value) {
|
||||
if (prop === 'Config') {
|
||||
value = Object.assign(value, overrideConfigs);
|
||||
}
|
||||
|
||||
obj[prop] = value;
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
(window as any).MSA = new Proxy(MSA, MsaHandler);
|
||||
(window as any).MeControl = new Proxy(MeControl, MeControlHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use power-saving flags for touch control
|
||||
*/
|
||||
export function patchCanvasContext() {
|
||||
const nativeGetContext = HTMLCanvasElement.prototype.getContext;
|
||||
// @ts-ignore
|
||||
HTMLCanvasElement.prototype.getContext = function(contextType: string, contextAttributes?: any) {
|
||||
if (contextType.includes('webgl')) {
|
||||
contextAttributes = contextAttributes || {};
|
||||
|
||||
contextAttributes.antialias = false;
|
||||
|
||||
// Use low-power profile for touch controller
|
||||
if (contextAttributes.powerPreference === 'high-performance') {
|
||||
contextAttributes.powerPreference = 'low-power';
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return nativeGetContext.apply(this, [contextType, contextAttributes]);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { StreamBadges } from "@modules/stream/stream-badges";
|
||||
import { TouchController } from "@modules/touch-controller";
|
||||
import { STATES } from "@utils/global";
|
||||
import { getPreferredServerRegion } from "@utils/region";
|
||||
import { GamePassCloudGallery } from "./gamepass-gallery";
|
||||
|
||||
export const NATIVE_FETCH = window.fetch;
|
||||
|
||||
@ -526,6 +527,8 @@ export function interceptHttpRequests() {
|
||||
return nativeXhrSend.apply(this, arguments);
|
||||
};
|
||||
|
||||
let gamepassAllGames: string[] = [];
|
||||
|
||||
(window as any).BX_FETCH = window.fetch = async (request: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
let url = (typeof request === 'string') ? request : (request as Request).url;
|
||||
|
||||
@ -549,6 +552,31 @@ export function interceptHttpRequests() {
|
||||
BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
|
||||
}
|
||||
|
||||
// Add list of games with custom layouts to the official list
|
||||
if (STATES.hasTouchSupport && url.includes('catalog.gamepass.com/sigls/')) {
|
||||
const response = await NATIVE_FETCH(request, init);
|
||||
const obj = await response.clone().json();
|
||||
|
||||
if (url.includes(GamePassCloudGallery.ALL)) {
|
||||
for (let i = 1; i < obj.length; i++) {
|
||||
gamepassAllGames.push(obj[i].id);
|
||||
}
|
||||
} else if (url.includes(GamePassCloudGallery.TOUCH)) {
|
||||
try {
|
||||
let customList = TouchController.getCustomList();
|
||||
|
||||
// Remove non-cloud games from the list
|
||||
customList = customList.filter(id => gamepassAllGames.includes(id));
|
||||
|
||||
const newCustomList = customList.map(item => ({ id: item }));
|
||||
obj.push(...newCustomList);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
response.json = () => Promise.resolve(obj);
|
||||
return response;
|
||||
}
|
||||
|
||||
let requestType: RequestType;
|
||||
if (url.includes('/sessions/home') || url.includes('xhome.') || (STATES.remotePlay.isPlaying && url.endsWith('/inputconfigs'))) {
|
||||
requestType = RequestType.XHOME;
|
||||
|
@ -27,6 +27,7 @@ export enum PrefKey {
|
||||
|
||||
STREAM_TOUCH_CONTROLLER = 'stream_touch_controller',
|
||||
STREAM_TOUCH_CONTROLLER_AUTO_OFF = 'stream_touch_controller_auto_off',
|
||||
STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY = 'stream_touch_controller_default_opacity',
|
||||
STREAM_TOUCH_CONTROLLER_STYLE_STANDARD = 'stream_touch_controller_style_standard',
|
||||
STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM = 'stream_touch_controller_style_custom',
|
||||
|
||||
@ -277,6 +278,20 @@ export class Preferences {
|
||||
default: false,
|
||||
unsupported: !STATES.hasTouchSupport,
|
||||
},
|
||||
[PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY]: {
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
label: t('tc-default-opacity'),
|
||||
default: 100,
|
||||
min: 10,
|
||||
max: 100,
|
||||
steps: 10,
|
||||
params: {
|
||||
suffix: '%',
|
||||
ticks: 10,
|
||||
hideSlider: true,
|
||||
},
|
||||
unsupported: !STATES.hasTouchSupport,
|
||||
},
|
||||
[PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: {
|
||||
label: t('tc-standard-layout-style'),
|
||||
default: 'default',
|
||||
|
55
src/utils/preload-state.ts
Normal file
55
src/utils/preload-state.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { STATES } from "@utils/global";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
import { TouchController } from "@modules/touch-controller";
|
||||
import { GamePassCloudGallery } from "./gamepass-gallery";
|
||||
|
||||
const LOG_TAG = 'PreloadState';
|
||||
|
||||
export function overridePreloadState() {
|
||||
let _state: any;
|
||||
|
||||
Object.defineProperty(window, '__PRELOADED_STATE__', {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
// @ts-ignore
|
||||
return _state;
|
||||
},
|
||||
set: state => {
|
||||
// Override User-Agent
|
||||
const userAgent = UserAgent.spoof();
|
||||
if (userAgent) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
state.appContext.requestInfo.userAgent = userAgent;
|
||||
} catch (e) {
|
||||
BxLogger.error(LOG_TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add list of games with custom layouts to the official list
|
||||
if (STATES.hasTouchSupport) {
|
||||
try {
|
||||
const sigls = state.xcloud.sigls;
|
||||
if (GamePassCloudGallery.TOUCH in sigls) {
|
||||
let customList = TouchController.getCustomList();
|
||||
|
||||
const allGames = sigls[GamePassCloudGallery.ALL].data.products;
|
||||
|
||||
// Remove non-cloud games from the list
|
||||
customList = customList.filter(id => allGames.includes(id));
|
||||
|
||||
// Add to the official list
|
||||
sigls[GamePassCloudGallery.TOUCH]?.data.products.push(...customList);
|
||||
}
|
||||
} catch (e) {
|
||||
BxLogger.error(LOG_TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
_state = state;
|
||||
STATES.appContext = structuredClone(state.appContext);
|
||||
}
|
||||
});
|
||||
}
|
@ -211,10 +211,13 @@ export class SettingElement {
|
||||
onChange && onChange(e, value);
|
||||
}
|
||||
|
||||
const onMouseDown = (e: MouseEvent | TouchEvent) => {
|
||||
const onMouseDown = (e: PointerEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
isHolding = true;
|
||||
|
||||
const args = arguments;
|
||||
interval && clearInterval(interval);
|
||||
interval = window.setInterval(() => {
|
||||
const event = new Event('click');
|
||||
(event as any).arguments = args;
|
||||
@ -223,11 +226,15 @@ export class SettingElement {
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const onMouseUp = (e: MouseEvent | TouchEvent) => {
|
||||
clearInterval(interval);
|
||||
const onMouseUp = (e: PointerEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
interval && clearInterval(interval);
|
||||
isHolding = false;
|
||||
};
|
||||
|
||||
const onContextMenu = (e: Event) => e.preventDefault();
|
||||
|
||||
// Custom method
|
||||
($wrapper as any).setValue = (value: any) => {
|
||||
$text.textContent = value + options.suffix;
|
||||
@ -235,16 +242,14 @@ export class SettingElement {
|
||||
};
|
||||
|
||||
$decBtn.addEventListener('click', onClick);
|
||||
$decBtn.addEventListener('mousedown', onMouseDown);
|
||||
$decBtn.addEventListener('mouseup', onMouseUp);
|
||||
$decBtn.addEventListener('touchstart', onMouseDown);
|
||||
$decBtn.addEventListener('touchend', onMouseUp);
|
||||
$decBtn.addEventListener('pointerdown', onMouseDown);
|
||||
$decBtn.addEventListener('pointerup', onMouseUp);
|
||||
$decBtn.addEventListener('contextmenu', onContextMenu);
|
||||
|
||||
$incBtn.addEventListener('click', onClick);
|
||||
$incBtn.addEventListener('mousedown', onMouseDown);
|
||||
$incBtn.addEventListener('mouseup', onMouseUp);
|
||||
$incBtn.addEventListener('touchstart', onMouseDown);
|
||||
$incBtn.addEventListener('touchend', onMouseUp);
|
||||
$incBtn.addEventListener('pointerdown', onMouseDown);
|
||||
$incBtn.addEventListener('pointerup', onMouseUp);
|
||||
$incBtn.addEventListener('contextmenu', onContextMenu);
|
||||
|
||||
return $wrapper;
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
import { STATES } from "@utils/global";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
|
||||
|
||||
export class PreloadedState {
|
||||
static override() {
|
||||
Object.defineProperty(window, '__PRELOADED_STATE__', {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
// Override User-Agent
|
||||
const userAgent = UserAgent.spoof();
|
||||
if (userAgent) {
|
||||
(this as any)._state.appContext.requestInfo.userAgent = userAgent;
|
||||
}
|
||||
|
||||
return (this as any)._state;
|
||||
},
|
||||
set: state => {
|
||||
(this as any)._state = state;
|
||||
STATES.appContext = structuredClone(state.appContext);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import { CE } from "@utils/html";
|
||||
|
||||
type ToastOptions = {
|
||||
instant?: boolean;
|
||||
html?: boolean;
|
||||
}
|
||||
|
||||
export class Toast {
|
||||
@ -40,9 +41,13 @@ export class Toast {
|
||||
Toast.#timeout = window.setTimeout(Toast.#hide, Toast.#DURATION);
|
||||
|
||||
// Get values from item
|
||||
const [msg, status, _] = Toast.#stack.shift()!;
|
||||
const [msg, status, options] = Toast.#stack.shift()!;
|
||||
|
||||
Toast.#$msg.textContent = msg;
|
||||
if (options.html) {
|
||||
Toast.#$msg.innerHTML = msg;
|
||||
} else {
|
||||
Toast.#$msg.textContent = msg;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
Toast.#$status.classList.remove('bx-gone');
|
||||
|
@ -1599,7 +1599,7 @@ const Texts = {
|
||||
"未找到主机",
|
||||
],
|
||||
"normal": [
|
||||
"Mittel",
|
||||
"Normal",
|
||||
"Normal",
|
||||
"Normal",
|
||||
"Normal",
|
||||
@ -2754,6 +2754,23 @@ const Texts = {
|
||||
"Màu của bố cục tùy chọn",
|
||||
"特殊游戏按钮样式",
|
||||
],
|
||||
"tc-default-opacity": [
|
||||
"Standard Deckkraft",
|
||||
,
|
||||
"Default opacity",
|
||||
"Opacidad por defecto",
|
||||
,
|
||||
,
|
||||
"既定の透過度",
|
||||
,
|
||||
"Domyślna przezroczystość",
|
||||
"Opacidade padrão",
|
||||
"Прозрачность по умолчанию",
|
||||
"Varsayılan opaklık",
|
||||
"Непрозорість за замовчуванням",
|
||||
"Độ mờ mặc định",
|
||||
"默认不透明度",
|
||||
],
|
||||
"tc-muted-colors": [
|
||||
"Matte Farben",
|
||||
"Warna redup",
|
||||
@ -2873,6 +2890,23 @@ const Texts = {
|
||||
"Bố cục điều khiển cảm ứng",
|
||||
"触摸控制布局",
|
||||
],
|
||||
"touch-control-layout-by": [
|
||||
(e: any) => `Touch-Steuerungslayout von ${e.name}`,
|
||||
,
|
||||
(e: any) => `Touch control layout by ${e.name}`,
|
||||
,
|
||||
,
|
||||
,
|
||||
(e: any) => `タッチ操作レイアウト作成者: ${e.name}`,
|
||||
,
|
||||
(e: any) => `Układ sterowania dotykowego stworzony przez ${e.name}`,
|
||||
,
|
||||
,
|
||||
(e: any) => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,
|
||||
(e: any) => `Розташування сенсорного керування від ${e.name}`,
|
||||
(e: any) => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,
|
||||
,
|
||||
],
|
||||
"touch-controller": [
|
||||
"Touch-Controller",
|
||||
"Kontrol sentuhan",
|
||||
|
@ -12,7 +12,7 @@ export enum UserAgentProfile {
|
||||
}
|
||||
|
||||
let CHROMIUM_VERSION = '123.0.0.0';
|
||||
if (!!(window as any).chrome) {
|
||||
if (!!(window as any).chrome || window.navigator.userAgent.includes('Chrome')) {
|
||||
// Get Chromium version in the original User-Agent value
|
||||
const match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/);
|
||||
if (match) {
|
||||
@ -20,16 +20,12 @@ if (!!(window as any).chrome) {
|
||||
}
|
||||
}
|
||||
|
||||
// Repace Chromium version
|
||||
let EDGE_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/[[VERSION]] Safari/537.36 Edg/[[VERSION]]';
|
||||
EDGE_USER_AGENT = EDGE_USER_AGENT.replaceAll('[[VERSION]]', CHROMIUM_VERSION);
|
||||
|
||||
export class UserAgent {
|
||||
static #USER_AGENTS = {
|
||||
[UserAgentProfile.EDGE_WINDOWS]: EDGE_USER_AGENT,
|
||||
static #USER_AGENTS: PartialRecord<UserAgentProfile, string> = {
|
||||
[UserAgentProfile.EDGE_WINDOWS]: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,
|
||||
[UserAgentProfile.SAFARI_MACOS]: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1',
|
||||
[UserAgentProfile.SMARTTV]: window.navigator.userAgent + ' SmartTV',
|
||||
[UserAgentProfile.SMARTTV_TIZEN]: 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) 94.0.4606.31/7.0 TV Safari/537.36',
|
||||
[UserAgentProfile.SMARTTV_TIZEN]: `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36`,
|
||||
[UserAgentProfile.VR_OCULUS]: window.navigator.userAgent + ' OculusBrowser VR',
|
||||
[UserAgentProfile.KIWI_V123]: 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.118 Mobile Safari/537.36',
|
||||
}
|
||||
|
Reference in New Issue
Block a user