mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-30 19:31:44 +02:00
Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
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.1.2
|
||||
// ==/UserScript==
|
||||
|
129
dist/better-xcloud.user.js
vendored
129
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.1.2
|
||||
// @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.1.2";
|
||||
var SCRIPT_HOME = "https://github.com/redphx/better-xcloud";
|
||||
var AppInterface = window.AppInterface;
|
||||
var STATES = {
|
||||
@ -3862,7 +3862,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;
|
||||
@ -4814,6 +4814,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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -8527,6 +8545,9 @@ 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;
|
||||
}
|
||||
@ -9206,11 +9227,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"';
|
||||
@ -9515,6 +9537,24 @@ 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;
|
||||
}
|
||||
};
|
||||
var PATCH_ORDERS = [
|
||||
@ -9548,6 +9588,8 @@ 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",
|
||||
BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging",
|
||||
@ -9619,7 +9661,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--;
|
||||
@ -10157,48 +10199,64 @@ 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
// src/index.ts
|
||||
@ -10207,9 +10265,8 @@ var main = function() {
|
||||
patchRtcCodecs();
|
||||
interceptHttpRequests();
|
||||
patchVideoApi();
|
||||
if (getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL)) {
|
||||
patchAudioContext();
|
||||
}
|
||||
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext();
|
||||
getPref(PrefKey.BLOCK_TRACKING) && patchMeControl();
|
||||
PreloadedState.override();
|
||||
VibrationManager.initialSetup();
|
||||
BX_FLAGS.CheckForUpdate && checkForUpdate();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ 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 { patchAudioContext, 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";
|
||||
@ -216,9 +216,8 @@ function main() {
|
||||
interceptHttpRequests();
|
||||
patchVideoApi();
|
||||
|
||||
if (getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL)) {
|
||||
patchAudioContext();
|
||||
}
|
||||
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext();
|
||||
getPref(PrefKey.BLOCK_TRACKING) && patchMeControl();
|
||||
|
||||
PreloadedState.override();
|
||||
|
||||
|
@ -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
|
||||
@ -459,6 +461,29 @@ 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;
|
||||
},
|
||||
};
|
||||
|
||||
let PATCH_ORDERS: PatchArray = [
|
||||
@ -501,6 +526,12 @@ 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',
|
||||
|
||||
@ -564,7 +595,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 +639,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 +729,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];
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,73 @@ 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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable telemetry flags in meversion.js
|
||||
*/
|
||||
export function patchMeControl() {
|
||||
const overrideConfigs = {
|
||||
enableAADTelemetry: false,
|
||||
enableTelemetry: false,
|
||||
telEvs: '',
|
||||
oneDSUrl: '',
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user