diff --git a/dist/better-xcloud.lite.user.js b/dist/better-xcloud.lite.user.js
index 60e9c06..a30e557 100644
--- a/dist/better-xcloud.lite.user.js
+++ b/dist/better-xcloud.lite.user.js
@@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud (Lite)
// @namespace https://github.com/redphx
-// @version 5.8.6
+// @version 5.9.0-beta
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@@ -16,7 +16,7 @@ class BxLogger {
static warning = (tag, ...args) => BxLogger.log("#c1a404", tag, ...args);
static error = (tag, ...args) => BxLogger.log("#c10404", tag, ...args);
static log(color, tag, ...args) {
- console.log("%c[BxC]", `color:${color};font-weight:bold;`, tag, "//", ...args);
+ BX_FLAGS.Debug && console.log("%c[BxC]", `color:${color};font-weight:bold;`, tag, "//", ...args);
}
}
window.BxLogger = BxLogger;
@@ -105,7 +105,7 @@ class UserAgent {
});
}
}
-var SCRIPT_VERSION = "5.8.6", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface;
+var SCRIPT_VERSION = "5.9.0-beta", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface;
UserAgent.init();
var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = {
supportedRegion: !0,
@@ -935,6 +935,7 @@ class BaseSettingsStore {
class StreamStatsCollector {
static instance;
static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector);
+ LOG_TAG = "StreamStatsCollector";
static INTERVAL_BACKGROUND = 60000;
calculateGrade(value, grades) {
return value > grades[2] ? "bad" : value > grades[1] ? "ok" : value > grades[0] ? "good" : "";
@@ -1034,6 +1035,9 @@ class StreamStatsCollector {
}
};
lastVideoStat;
+ constructor() {
+ BxLogger.info(this.LOG_TAG, "constructor()");
+ }
async collect() {
let stats = await STATES.currentStream.peerConnection?.getStats();
if (!stats) return;
@@ -1331,7 +1335,7 @@ class GlobalSettingsStorage extends BaseSettingsStore {
requiredVariants: "full",
label: t("enable-local-co-op-support"),
default: !1,
- note: CE("a", {
+ note: () => CE("a", {
href: "https://github.com/redphx/better-xcloud/discussions/275",
target: "_blank"
}, t("enable-local-co-op-support-note"))
@@ -1381,7 +1385,7 @@ class GlobalSettingsStorage extends BaseSettingsStore {
let note, url;
if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657";
else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer";
- setting.unsupportedNote = CE("a", {
+ setting.unsupportedNote = () => CE("a", {
href: url,
target: "_blank"
}, "⚠️ " + note);
@@ -1798,6 +1802,7 @@ var MouseMapTo;
class StreamStats {
static instance;
static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats);
+ LOG_TAG = "StreamStats";
intervalId;
REFRESH_INTERVAL = 1000;
stats = {
@@ -1853,7 +1858,7 @@ class StreamStats {
$container;
quickGlanceObserver;
constructor() {
- this.render();
+ BxLogger.info(this.LOG_TAG, "constructor()"), this.render();
}
async start(glancing = !1) {
if (!this.isHidden() || glancing && this.isGlancing()) return;
@@ -1938,43 +1943,52 @@ class StreamStats {
}
}
class Toast {
- static $wrapper;
- static $msg;
- static $status;
- static stack = [];
- static isShowing = !1;
- static timeout;
- static DURATION = 3000;
- static show(msg, status, options = {}) {
+ static instance;
+ static getInstance = () => Toast.instance ?? (Toast.instance = new Toast);
+ LOG_TAG = "Toast";
+ $wrapper;
+ $msg;
+ $status;
+ stack = [];
+ isShowing = !1;
+ timeoutId;
+ DURATION = 3000;
+ constructor() {
+ BxLogger.info(this.LOG_TAG, "constructor()"), this.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, this.$msg = CE("span", { class: "bx-toast-msg" }), this.$status = CE("span", { class: "bx-toast-status" })), this.$wrapper.addEventListener("transitionend", (e) => {
+ let classList = this.$wrapper.classList;
+ if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), this.showNext();
+ }), document.documentElement.appendChild(this.$wrapper);
+ }
+ show(msg, status, options = {}) {
options = options || {};
let args = Array.from(arguments);
- if (options.instant) Toast.stack = [args], Toast.showNext();
- else Toast.stack.push(args), !Toast.isShowing && Toast.showNext();
+ if (options.instant) this.stack = [args], this.showNext();
+ else this.stack.push(args), !this.isShowing && this.showNext();
}
- static showNext() {
- if (!Toast.stack.length) {
- Toast.isShowing = !1;
+ showNext() {
+ if (!this.stack.length) {
+ this.isShowing = !1;
return;
}
- Toast.isShowing = !0, Toast.timeout && clearTimeout(Toast.timeout), Toast.timeout = window.setTimeout(Toast.hide, Toast.DURATION);
- let [msg, status, options] = Toast.stack.shift();
- if (options && options.html) Toast.$msg.innerHTML = msg;
- else Toast.$msg.textContent = msg;
- if (status) Toast.$status.classList.remove("bx-gone"), Toast.$status.textContent = status;
- else Toast.$status.classList.add("bx-gone");
- let classList = Toast.$wrapper.classList;
+ this.isShowing = !0, this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide.bind(this), this.DURATION);
+ let [msg, status, options] = this.stack.shift();
+ if (options && options.html) this.$msg.innerHTML = msg;
+ else this.$msg.textContent = msg;
+ if (status) this.$status.classList.remove("bx-gone"), this.$status.textContent = status;
+ else this.$status.classList.add("bx-gone");
+ let classList = this.$wrapper.classList;
classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show");
}
- static hide() {
- Toast.timeout = null;
- let classList = Toast.$wrapper.classList;
+ hide() {
+ this.timeoutId = null;
+ let classList = this.$wrapper.classList;
classList.remove("bx-show"), classList.add("bx-hide");
}
- static setup() {
- Toast.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, Toast.$msg = CE("span", { class: "bx-toast-msg" }), Toast.$status = CE("span", { class: "bx-toast-status" })), Toast.$wrapper.addEventListener("transitionend", (e) => {
- let classList = Toast.$wrapper.classList;
- if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), Toast.showNext();
- }), document.documentElement.appendChild(Toast.$wrapper);
+ static show(msg, status, options = {}) {
+ Toast.getInstance().show(msg, status, options);
+ }
+ static showNext() {
+ Toast.getInstance().showNext();
}
}
function ceilToNearest(value, interval) {
@@ -2028,8 +2042,7 @@ class SoundShortcut {
});
return;
}
- let $media;
- if ($media = document.querySelector("div[data-testid=media-container] audio"), !$media) $media = document.querySelector("div[data-testid=media-container] video");
+ let $media = document.querySelector("div[data-testid=media-container] audio") ?? document.querySelector("div[data-testid=media-container] video");
if ($media) {
$media.muted = !$media.muted;
let status = $media.muted ? t("muted") : t("unmuted");
@@ -2237,102 +2250,7 @@ class MkbPreset {
let mouseMapTo = MouseMapTo[mouse["map_to"]];
if (typeof mouseMapTo !== "undefined") mouse["map_to"] = mouseMapTo;
else mouse["map_to"] = MkbPreset.MOUSE_SETTINGS["map_to"].default;
- return console.log(obj), obj;
- }
-}
-class LocalDb {
- static #instance;
- static get INSTANCE() {
- if (!LocalDb.#instance) LocalDb.#instance = new LocalDb;
- return LocalDb.#instance;
- }
- static DB_NAME = "BetterXcloud";
- static DB_VERSION = 1;
- static TABLE_PRESETS = "mkb_presets";
- #DB;
- #open() {
- return new Promise((resolve, reject) => {
- if (this.#DB) {
- resolve();
- return;
- }
- let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
- request.onupgradeneeded = (e) => {
- let db = e.target.result;
- switch (e.oldVersion) {
- case 0: {
- db.createObjectStore(LocalDb.TABLE_PRESETS, { keyPath: "id", autoIncrement: !0 }).createIndex("name_idx", "name");
- break;
- }
- }
- }, request.onerror = (e) => {
- console.log(e), alert(e.target.error.message), reject && reject();
- }, request.onsuccess = (e) => {
- this.#DB = e.target.result, resolve();
- };
- });
- }
- #table(name, type) {
- let table = this.#DB.transaction(name, type || "readonly").objectStore(name);
- return new Promise((resolve) => resolve(table));
- }
- #call(method) {
- let table = arguments[1];
- return new Promise((resolve) => {
- let request = method.call(table, ...Array.from(arguments).slice(2));
- request.onsuccess = (e) => {
- resolve([table, e.target.result]);
- };
- });
- }
- #count(table) {
- return this.#call(table.count, ...arguments);
- }
- #add(table, data) {
- return this.#call(table.add, ...arguments);
- }
- #put(table, data) {
- return this.#call(table.put, ...arguments);
- }
- #delete(table, data) {
- return this.#call(table.delete, ...arguments);
- }
- #get(table, id) {
- return this.#call(table.get, ...arguments);
- }
- #getAll(table) {
- return this.#call(table.getAll, ...arguments);
- }
- newPreset(name, data) {
- return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#add(table, { name, data })).then(([table, id]) => new Promise((resolve) => resolve(id)));
- }
- updatePreset(preset) {
- return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#put(table, preset)).then(([table, id]) => new Promise((resolve) => resolve(id)));
- }
- deletePreset(id) {
- return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#delete(table, id)).then(([table, id2]) => new Promise((resolve) => resolve(id2)));
- }
- getPreset(id) {
- return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#get(table, id)).then(([table, preset]) => new Promise((resolve) => resolve(preset)));
- }
- getPresets() {
- return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#count(table)).then(([table, count]) => {
- if (count > 0) return new Promise((resolve) => {
- this.#getAll(table).then(([table2, items]) => {
- let presets = {};
- items.forEach((item) => presets[item.id] = item), resolve(presets);
- });
- });
- let preset = {
- name: t("default"),
- data: MkbPreset.DEFAULT_PRESET
- };
- return new Promise((resolve) => {
- this.#add(table, preset).then(([table2, id]) => {
- preset.id = id, setPref("mkb_default_preset_id", id), resolve({ [id]: preset });
- });
- });
- });
+ return obj;
}
}
class KeyHelper {
@@ -2362,18 +2280,21 @@ class KeyHelper {
return KeyHelper.#NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code;
}
}
-var LOG_TAG = "PointerClient";
class PointerClient {
static instance;
static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient);
+ LOG_TAG = "PointerClient";
socket;
mkbHandler;
+ constructor() {
+ BxLogger.info(this.LOG_TAG, "constructor()");
+ }
start(port, mkbHandler) {
if (!port) throw new Error("PointerServer port is 0");
this.mkbHandler = mkbHandler, this.socket = new WebSocket(`ws://localhost:${port}`), this.socket.binaryType = "arraybuffer", this.socket.addEventListener("open", (event) => {
- BxLogger.info(LOG_TAG, "connected");
+ BxLogger.info(this.LOG_TAG, "connected");
}), this.socket.addEventListener("error", (event) => {
- BxLogger.error(LOG_TAG, event), Toast.show("Cannot setup mouse: " + event);
+ BxLogger.error(this.LOG_TAG, event), Toast.show("Cannot setup mouse: " + event);
}), this.socket.addEventListener("close", (event) => {
this.socket = null;
}), this.socket.addEventListener("message", (event) => {
@@ -2436,6 +2357,112 @@ class MouseDataProvider {
}
}
class MkbHandler {}
+class LocalDb {
+ static DB_NAME = "BetterXcloud";
+ static DB_VERSION = 1;
+ db;
+ open() {
+ return new Promise((resolve, reject) => {
+ if (this.db) {
+ resolve();
+ return;
+ }
+ let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
+ request.onupgradeneeded = this.onUpgradeNeeded, request.onerror = (e) => {
+ console.log(e), alert(e.target.error.message), reject && reject();
+ }, request.onsuccess = (e) => {
+ this.db = e.target.result, resolve();
+ };
+ });
+ }
+ table(name, type) {
+ let table = this.db.transaction(name, type || "readonly").objectStore(name);
+ return new Promise((resolve) => resolve(table));
+ }
+ call(method) {
+ let table = arguments[1];
+ return new Promise((resolve) => {
+ let request = method.call(table, ...Array.from(arguments).slice(2));
+ request.onsuccess = (e) => {
+ resolve([table, e.target.result]);
+ };
+ });
+ }
+ count(table) {
+ return this.call(table.count, ...arguments);
+ }
+ add(table, data) {
+ return this.call(table.add, ...arguments);
+ }
+ put(table, data) {
+ return this.call(table.put, ...arguments);
+ }
+ delete(table, data) {
+ return this.call(table.delete, ...arguments);
+ }
+ get(table, id) {
+ return this.call(table.get, ...arguments);
+ }
+ getAll(table) {
+ return this.call(table.getAll, ...arguments);
+ }
+}
+class MkbPresetsDb extends LocalDb {
+ static instance;
+ static getInstance = () => MkbPresetsDb.instance ?? (MkbPresetsDb.instance = new MkbPresetsDb);
+ LOG_TAG = "MkbPresetsDb";
+ TABLE_PRESETS = "mkb_presets";
+ constructor() {
+ super();
+ BxLogger.info(this.LOG_TAG, "constructor()");
+ }
+ onUpgradeNeeded(e) {
+ let db = e.target.result;
+ switch (e.oldVersion) {
+ case 0: {
+ db.createObjectStore(this.TABLE_PRESETS, {
+ keyPath: "id",
+ autoIncrement: !0
+ }).createIndex("name_idx", "name");
+ break;
+ }
+ }
+ }
+ presetsTable() {
+ return this.open().then(() => this.table(this.TABLE_PRESETS, "readwrite"));
+ }
+ newPreset(name, data) {
+ return this.presetsTable().then((table) => this.add(table, { name, data })).then(([table, id]) => new Promise((resolve) => resolve(id)));
+ }
+ updatePreset(preset) {
+ return this.presetsTable().then((table) => this.put(table, preset)).then(([table, id]) => new Promise((resolve) => resolve(id)));
+ }
+ deletePreset(id) {
+ return this.presetsTable().then((table) => this.delete(table, id)).then(([table, id2]) => new Promise((resolve) => resolve(id2)));
+ }
+ getPreset(id) {
+ return this.presetsTable().then((table) => this.get(table, id)).then(([table, preset]) => new Promise((resolve) => resolve(preset)));
+ }
+ getPresets() {
+ return this.presetsTable().then((table) => this.count(table)).then(([table, count]) => {
+ if (count > 0) return new Promise((resolve) => {
+ this.getAll(table).then(([table2, items]) => {
+ let presets = {};
+ items.forEach((item) => presets[item.id] = item), resolve(presets);
+ });
+ });
+ let preset = {
+ name: t("default"),
+ data: MkbPreset.DEFAULT_PRESET
+ };
+ return new Promise((resolve) => {
+ this.add(table, preset).then(([table2, id]) => {
+ preset.id = id, setPref("mkb_default_preset_id", id), resolve({ [id]: preset });
+ });
+ });
+ });
+ }
+}
var PointerToMouseButton = {
1: 0,
2: 2,
@@ -2498,6 +2525,7 @@ class PointerLockMouseDataProvider extends MouseDataProvider {
class EmulatedMkbHandler extends MkbHandler {
static instance;
static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler);
+ static LOG_TAG = "EmulatedMkbHandler";
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
static DEFAULT_PANNING_SENSITIVITY = 0.001;
static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01;
@@ -2529,7 +2557,7 @@ class EmulatedMkbHandler extends MkbHandler {
#RIGHT_STICK_Y = [];
constructor() {
super();
- this.#STICK_MAP = {
+ BxLogger.info(EmulatedMkbHandler.LOG_TAG, "constructor()"), this.#STICK_MAP = {
102: [this.#LEFT_STICK_X, 0, -1],
103: [this.#LEFT_STICK_X, 0, 1],
100: [this.#LEFT_STICK_Y, 1, -1],
@@ -2646,7 +2674,7 @@ class EmulatedMkbHandler extends MkbHandler {
#getCurrentPreset = () => {
return new Promise((resolve) => {
let presetId = getPref("mkb_default_preset_id");
- LocalDb.INSTANCE.getPreset(presetId).then((preset) => {
+ MkbPresetsDb.getInstance().getPreset(presetId).then((preset) => {
resolve(preset);
});
});
@@ -2775,6 +2803,7 @@ class NavigationDialog {
class NavigationDialogManager {
static instance;
static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager);
+ LOG_TAG = "NavigationDialogManager";
static GAMEPAD_POLLING_INTERVAL = 50;
static GAMEPAD_KEYS = [
12,
@@ -2815,7 +2844,7 @@ class NavigationDialogManager {
$container;
dialog = null;
constructor() {
- if (this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => {
+ if (BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => {
e.preventDefault(), e.stopPropagation(), this.hide();
}), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), getPref("ui_controller_friendly"))
new MutationObserver((mutationList) => {
@@ -3055,6 +3084,293 @@ var BxIcon = {
UPLOAD: "",
AUDIO: ""
};
+class Dialog {
+ $dialog;
+ $title;
+ $content;
+ $overlay;
+ onClose;
+ constructor(options) {
+ let {
+ title,
+ className,
+ content,
+ hideCloseButton,
+ onClose,
+ helpUrl
+ } = options, $overlay = document.querySelector(".bx-dialog-overlay");
+ if (!$overlay) this.$overlay = CE("div", { class: "bx-dialog-overlay bx-gone" }), this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$overlay);
+ else this.$overlay = $overlay;
+ let $close;
+ this.onClose = onClose, this.$dialog = CE("div", { class: `bx-dialog ${className || ""} bx-gone` }, this.$title = CE("h2", {}, CE("b", {}, title), helpUrl && createButton({
+ icon: BxIcon.QUESTION,
+ style: 4,
+ title: t("help"),
+ url: helpUrl
+ })), this.$content = CE("div", { class: "bx-dialog-content" }, content), !hideCloseButton && ($close = CE("button", { type: "button" }, t("close")))), $close && $close.addEventListener("click", (e) => {
+ this.hide(e);
+ }), !title && this.$title.classList.add("bx-gone"), !content && this.$content.classList.add("bx-gone"), this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$dialog);
+ }
+ show(newOptions) {
+ if (document.activeElement && document.activeElement.blur(), newOptions && newOptions.title) this.$title.querySelector("b").textContent = newOptions.title, this.$title.classList.remove("bx-gone");
+ this.$dialog.classList.remove("bx-gone"), this.$overlay.classList.remove("bx-gone"), document.body.classList.add("bx-no-scroll");
+ }
+ hide(e) {
+ this.$dialog.classList.add("bx-gone"), this.$overlay.classList.add("bx-gone"), document.body.classList.remove("bx-no-scroll"), this.onClose && this.onClose(e);
+ }
+ toggle() {
+ this.$dialog.classList.toggle("bx-gone"), this.$overlay.classList.toggle("bx-gone");
+ }
+}
+class MkbRemapper {
+ BUTTON_ORDERS = [
+ 12,
+ 13,
+ 14,
+ 15,
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 16,
+ 10,
+ 100,
+ 101,
+ 102,
+ 103,
+ 11,
+ 200,
+ 201,
+ 202,
+ 203
+ ];
+ static instance;
+ static getInstance = () => MkbRemapper.instance ?? (MkbRemapper.instance = new MkbRemapper);
+ LOG_TAG = "MkbRemapper";
+ STATE = {
+ currentPresetId: 0,
+ presets: {},
+ editingPresetData: null,
+ isEditing: !1
+ };
+ $wrapper;
+ $presetsSelect;
+ $activateButton;
+ $currentBindingKey;
+ allKeyElements = [];
+ allMouseElements = {};
+ bindingDialog;
+ constructor() {
+ BxLogger.info(this.LOG_TAG, "constructor()"), this.STATE.currentPresetId = getPref("mkb_default_preset_id"), this.bindingDialog = new Dialog({
+ className: "bx-binding-dialog",
+ content: CE("div", {}, CE("p", {}, t("press-to-bind")), CE("i", {}, t("press-esc-to-cancel"))),
+ hideCloseButton: !0
+ });
+ }
+ clearEventListeners = () => {
+ window.removeEventListener("keydown", this.onKeyDown), window.removeEventListener("mousedown", this.onMouseDown), window.removeEventListener("wheel", this.onWheel);
+ };
+ bindKey = ($elm, key) => {
+ let buttonIndex = parseInt($elm.dataset.buttonIndex), keySlot = parseInt($elm.dataset.keySlot);
+ if ($elm.dataset.keyCode === key.code) return;
+ for (let $otherElm of this.allKeyElements)
+ if ($otherElm.dataset.keyCode === key.code) this.unbindKey($otherElm);
+ this.STATE.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.dataset.keyCode = key.code;
+ };
+ unbindKey = ($elm) => {
+ let buttonIndex = parseInt($elm.dataset.buttonIndex), keySlot = parseInt($elm.dataset.keySlot);
+ this.STATE.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", delete $elm.dataset.keyCode;
+ };
+ onWheel = (e) => {
+ e.preventDefault(), this.clearEventListeners(), this.bindKey(this.$currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200);
+ };
+ onMouseDown = (e) => {
+ e.preventDefault(), this.clearEventListeners(), this.bindKey(this.$currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200);
+ };
+ onKeyDown = (e) => {
+ if (e.preventDefault(), e.stopPropagation(), this.clearEventListeners(), e.code !== "Escape") this.bindKey(this.$currentBindingKey, KeyHelper.getKeyFromEvent(e));
+ window.setTimeout(() => this.bindingDialog.hide(), 200);
+ };
+ onBindingKey = (e) => {
+ if (!this.STATE.isEditing || e.button !== 0) return;
+ console.log(e), this.$currentBindingKey = e.target, window.addEventListener("keydown", this.onKeyDown), window.addEventListener("mousedown", this.onMouseDown), window.addEventListener("wheel", this.onWheel), this.bindingDialog.show({ title: this.$currentBindingKey.dataset.prompt });
+ };
+ onContextMenu = (e) => {
+ if (e.preventDefault(), !this.STATE.isEditing) return;
+ this.unbindKey(e.target);
+ };
+ getPreset = (presetId) => {
+ return this.STATE.presets[presetId];
+ };
+ getCurrentPreset = () => {
+ return this.getPreset(this.STATE.currentPresetId);
+ };
+ switchPreset = (presetId) => {
+ this.STATE.currentPresetId = presetId;
+ let presetData = this.getCurrentPreset().data;
+ for (let $elm of this.allKeyElements) {
+ let buttonIndex = parseInt($elm.dataset.buttonIndex), keySlot = parseInt($elm.dataset.keySlot), buttonKeys = presetData.mapping[buttonIndex];
+ if (buttonKeys && buttonKeys[keySlot]) $elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]), $elm.dataset.keyCode = buttonKeys[keySlot];
+ else $elm.textContent = "", delete $elm.dataset.keyCode;
+ }
+ let key;
+ for (key in this.allMouseElements) {
+ let $elm = this.allMouseElements[key], value = presetData.mouse[key];
+ if (typeof value === "undefined") value = MkbPreset.MOUSE_SETTINGS[key].default;
+ "setValue" in $elm && $elm.setValue(value);
+ }
+ let activated = getPref("mkb_default_preset_id") === this.STATE.currentPresetId;
+ this.$activateButton.disabled = activated, this.$activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate");
+ };
+ refresh() {
+ while (this.$presetsSelect.firstChild)
+ this.$presetsSelect.removeChild(this.$presetsSelect.firstChild);
+ MkbPresetsDb.getInstance().getPresets().then((presets) => {
+ this.STATE.presets = presets;
+ let $fragment = document.createDocumentFragment(), defaultPresetId;
+ if (this.STATE.currentPresetId === 0) this.STATE.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.STATE.currentPresetId, setPref("mkb_default_preset_id", defaultPresetId), EmulatedMkbHandler.getInstance().refreshPresetData();
+ else defaultPresetId = getPref("mkb_default_preset_id");
+ for (let id in presets) {
+ let name = presets[id].name;
+ if (id === defaultPresetId) name = "🎮 " + name;
+ let $options = CE("option", { value: id }, name);
+ $options.selected = parseInt(id) === this.STATE.currentPresetId, $fragment.appendChild($options);
+ }
+ this.$presetsSelect.appendChild($fragment);
+ let activated = defaultPresetId === this.STATE.currentPresetId;
+ this.$activateButton.disabled = activated, this.$activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.STATE.isEditing && this.switchPreset(this.STATE.currentPresetId);
+ });
+ }
+ toggleEditing = (force) => {
+ if (this.STATE.isEditing = typeof force !== "undefined" ? force : !this.STATE.isEditing, this.$wrapper.classList.toggle("bx-editing", this.STATE.isEditing), this.STATE.isEditing) this.STATE.editingPresetData = deepClone(this.getCurrentPreset().data);
+ else this.STATE.editingPresetData = null;
+ let childElements = this.$wrapper.querySelectorAll("select, button, input");
+ for (let $elm of Array.from(childElements)) {
+ if ($elm.parentElement.parentElement.classList.contains("bx-mkb-action-buttons")) continue;
+ let disable = !this.STATE.isEditing;
+ if ($elm.parentElement.classList.contains("bx-mkb-preset-tools")) disable = !disable;
+ $elm.disabled = disable;
+ }
+ };
+ render() {
+ this.$wrapper = CE("div", { class: "bx-mkb-settings" }), this.$presetsSelect = CE("select", { tabindex: -1 }), this.$presetsSelect.addEventListener("change", (e) => {
+ this.switchPreset(parseInt(e.target.value));
+ });
+ let promptNewName = (value) => {
+ let newName = "";
+ while (!newName) {
+ if (newName = prompt(t("prompt-preset-name"), value), newName === null) return !1;
+ newName = newName.trim();
+ }
+ return newName ? newName : !1;
+ }, $header = CE("div", { class: "bx-mkb-preset-tools" }, this.$presetsSelect, createButton({
+ title: t("rename"),
+ icon: BxIcon.CURSOR_TEXT,
+ tabIndex: -1,
+ onClick: (e) => {
+ let preset = this.getCurrentPreset(), newName = promptNewName(preset.name);
+ if (!newName || newName === preset.name) return;
+ preset.name = newName, MkbPresetsDb.getInstance().updatePreset(preset).then((id) => this.refresh());
+ }
+ }), createButton({
+ icon: BxIcon.NEW,
+ title: t("new"),
+ tabIndex: -1,
+ onClick: (e) => {
+ let newName = promptNewName("");
+ if (!newName) return;
+ MkbPresetsDb.getInstance().newPreset(newName, MkbPreset.DEFAULT_PRESET).then((id) => {
+ this.STATE.currentPresetId = id, this.refresh();
+ });
+ }
+ }), createButton({
+ icon: BxIcon.COPY,
+ title: t("copy"),
+ tabIndex: -1,
+ onClick: (e) => {
+ let preset = this.getCurrentPreset(), newName = promptNewName(`${preset.name} (2)`);
+ if (!newName) return;
+ MkbPresetsDb.getInstance().newPreset(newName, preset.data).then((id) => {
+ this.STATE.currentPresetId = id, this.refresh();
+ });
+ }
+ }), createButton({
+ icon: BxIcon.TRASH,
+ style: 2,
+ title: t("delete"),
+ tabIndex: -1,
+ onClick: (e) => {
+ if (!confirm(t("confirm-delete-preset"))) return;
+ MkbPresetsDb.getInstance().deletePreset(this.STATE.currentPresetId).then((id) => {
+ this.STATE.currentPresetId = 0, this.refresh();
+ });
+ }
+ }));
+ this.$wrapper.appendChild($header);
+ let $rows = CE("div", { class: "bx-mkb-settings-rows" }, CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind"))), keysPerButton = 2;
+ for (let buttonIndex of this.BUTTON_ORDERS) {
+ let [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex], $elm, $fragment = document.createDocumentFragment();
+ for (let i = 0;i < keysPerButton; i++)
+ $elm = CE("button", {
+ type: "button",
+ "data-prompt": buttonPrompt,
+ "data-button-index": buttonIndex,
+ "data-key-slot": i
+ }, " "), $elm.addEventListener("mouseup", this.onBindingKey), $elm.addEventListener("contextmenu", this.onContextMenu), $fragment.appendChild($elm), this.allKeyElements.push($elm);
+ let $keyRow = CE("div", { class: "bx-mkb-key-row" }, CE("label", { title: buttonName }, buttonPrompt), $fragment);
+ $rows.appendChild($keyRow);
+ }
+ $rows.appendChild(CE("i", { class: "bx-mkb-note" }, t("mkb-adjust-ingame-settings")));
+ let $mouseSettings = document.createDocumentFragment();
+ for (let key in MkbPreset.MOUSE_SETTINGS) {
+ let setting = MkbPreset.MOUSE_SETTINGS[key], value = setting.default, $elm, onChange = (e, value2) => {
+ this.STATE.editingPresetData.mouse[key] = value2;
+ }, $row = CE("label", {
+ class: "bx-settings-row",
+ for: `bx_setting_${key}`
+ }, CE("span", { class: "bx-settings-label" }, setting.label), $elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params));
+ $mouseSettings.appendChild($row), this.allMouseElements[key] = $elm;
+ }
+ $rows.appendChild($mouseSettings), this.$wrapper.appendChild($rows);
+ let $actionButtons = CE("div", { class: "bx-mkb-action-buttons" }, CE("div", {}, createButton({
+ label: t("edit"),
+ tabIndex: -1,
+ onClick: (e) => this.toggleEditing(!0)
+ }), this.$activateButton = createButton({
+ label: t("activate"),
+ style: 1,
+ tabIndex: -1,
+ onClick: (e) => {
+ setPref("mkb_default_preset_id", this.STATE.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.refresh();
+ }
+ })), CE("div", {}, createButton({
+ label: t("cancel"),
+ style: 4,
+ tabIndex: -1,
+ onClick: (e) => {
+ this.switchPreset(this.STATE.currentPresetId), this.toggleEditing(!1);
+ }
+ }), createButton({
+ label: t("save"),
+ style: 1,
+ tabIndex: -1,
+ onClick: (e) => {
+ let updatedPreset = deepClone(this.getCurrentPreset());
+ updatedPreset.data = this.STATE.editingPresetData, MkbPresetsDb.getInstance().updatePreset(updatedPreset).then((id) => {
+ if (id === getPref("mkb_default_preset_id")) EmulatedMkbHandler.getInstance().refreshPresetData();
+ this.toggleEditing(!1), this.refresh();
+ });
+ }
+ })));
+ return this.$wrapper.appendChild($actionButtons), this.toggleEditing(!1), this.refresh(), this.$wrapper;
+ }
+}
var VIBRATION_DATA_MAP = {
gamepadIndex: 8,
leftMotorPercent: 8,
@@ -3136,9 +3452,10 @@ if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, F
class FullscreenText {
static instance;
static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText);
+ LOG_TAG = "FullscreenText";
$text;
constructor() {
- this.$text = CE("div", {
+ BxLogger.info(this.LOG_TAG, "constructor()"), this.$text = CE("div", {
class: "bx-fullscreen-text bx-gone"
}), document.documentElement.appendChild(this.$text);
}
@@ -3152,9 +3469,10 @@ class FullscreenText {
class SettingsNavigationDialog extends NavigationDialog {
static instance;
static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog);
+ LOG_TAG = "SettingsNavigationDialog";
$container;
$tabs;
- $settings;
+ $tabContents;
$btnReload;
$btnGlobalReload;
$noteGlobalReload;
@@ -3466,11 +3784,11 @@ class SettingsNavigationDialog extends NavigationDialog {
},
!1
];
- TAB_VIRTUAL_CONTROLLER_ITEMS = [{
+ TAB_VIRTUAL_CONTROLLER_ITEMS = () => [{
group: "mkb",
label: t("virtual-controller"),
helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/",
- content: !1
+ content: MkbRemapper.getInstance().render()
}];
TAB_NATIVE_MKB_ITEMS = [{
requiredVariants: "full",
@@ -3478,7 +3796,7 @@ class SettingsNavigationDialog extends NavigationDialog {
label: t("native-mkb"),
items: []
}];
- TAB_SHORTCUTS_ITEMS = [{
+ TAB_SHORTCUTS_ITEMS = () => [{
requiredVariants: "full",
group: "controller-shortcuts",
label: t("controller-shortcuts"),
@@ -3525,40 +3843,41 @@ class SettingsNavigationDialog extends NavigationDialog {
}
]
}];
- SETTINGS_UI = [
- {
- icon: BxIcon.HOME,
+ SETTINGS_UI = {
+ global: {
group: "global",
+ icon: BxIcon.HOME,
items: this.TAB_GLOBAL_ITEMS
},
- {
- icon: BxIcon.DISPLAY,
+ stream: {
group: "stream",
+ icon: BxIcon.DISPLAY,
items: this.TAB_DISPLAY_ITEMS
},
- {
- icon: BxIcon.CONTROLLER,
+ controller: {
group: "controller",
+ icon: BxIcon.CONTROLLER,
items: this.TAB_CONTROLLER_ITEMS,
requiredVariants: "full"
},
- !1,
- !1,
- {
- icon: BxIcon.COMMAND,
+ mkb: !1,
+ "native-mkb": !1,
+ shortcuts: {
group: "shortcuts",
+ icon: BxIcon.COMMAND,
items: this.TAB_SHORTCUTS_ITEMS,
+ lazyContent: !0,
requiredVariants: "full"
},
- {
- icon: BxIcon.STREAM_STATS,
+ stats: {
group: "stats",
+ icon: BxIcon.STREAM_STATS,
items: this.TAB_STATS_ITEMS
}
- ];
+ };
constructor() {
super();
- this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog();
+ BxLogger.info(this.LOG_TAG, "constructor()"), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog();
}
getDialog() {
return this;
@@ -3619,8 +3938,10 @@ class SettingsNavigationDialog extends NavigationDialog {
BxEvent.dispatch($content.querySelector("select"), "input");
return;
}
- for (let settingTab of this.SETTINGS_UI) {
- if (!settingTab || !settingTab.items) continue;
+ let settingTabGroup;
+ for (settingTabGroup in this.SETTINGS_UI) {
+ let settingTab = this.SETTINGS_UI[settingTabGroup];
+ if (!settingTab || !settingTab.items || typeof settingTab.items === "function") continue;
for (let settingTabContent of settingTab.items) {
if (!settingTabContent || !settingTabContent.items) continue;
for (let setting of settingTabContent.items) {
@@ -3716,19 +4037,27 @@ class SettingsNavigationDialog extends NavigationDialog {
href: "https://github.com/redphx/better-xcloud-devices",
target: "_blank",
tabindex: 0
- }, t("suggest-settings-link"))), $btnSuggest?.insertAdjacentElement("afterend", $content);
+ }, t("suggest-settings-link"))), $btnSuggest.insertAdjacentElement("afterend", $content);
+ }
+ onTabClicked(e) {
+ let $svg = e.target.closest("svg");
+ if ($svg.dataset.lazy) {
+ delete $svg.dataset.lazy;
+ let settingTab = this.SETTINGS_UI[$svg.dataset.group], items = settingTab.items(), $tabContent = this.renderTabContent.call(this, settingTab, items);
+ this.$tabContents.appendChild($tabContent);
+ }
+ let $child, children = Array.from(this.$tabContents.children);
+ for ($child of children)
+ if ($child.dataset.tabGroup === $svg.dataset.group) {
+ if ($child.classList.remove("bx-gone"), getPref("ui_controller_friendly")) this.dialogManager.calculateSelectBoxes($child);
+ } else $child.classList.add("bx-gone");
+ for (let $child2 of Array.from(this.$tabs.children))
+ $child2.classList.remove("bx-active");
+ $svg.classList.add("bx-active");
}
renderTab(settingTab) {
let $svg = createSvgIcon(settingTab.icon);
- return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", (e) => {
- for (let $child of Array.from(this.$settings.children))
- if ($child.getAttribute("data-tab-group") === settingTab.group) {
- if ($child.classList.remove("bx-gone"), getPref("ui_controller_friendly")) this.dialogManager.calculateSelectBoxes($child);
- } else $child.classList.add("bx-gone");
- for (let $child of Array.from(this.$tabs.children))
- $child.classList.remove("bx-active");
- $svg.classList.add("bx-active");
- }), $svg;
+ return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, settingTab.lazyContent && ($svg.dataset.lazy = settingTab.lazyContent.toString()), $svg.addEventListener("click", this.onTabClicked.bind(this)), $svg;
}
onGlobalSettingChanged(e) {
this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger");
@@ -3792,6 +4121,8 @@ class SettingsNavigationDialog extends NavigationDialog {
if (pref) prefDefinition = getPrefDefinition(pref);
if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return;
let label = prefDefinition?.label || setting.label, note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental;
+ if (typeof note === "function") note = note();
+ if (typeof unsupportedNote === "function") unsupportedNote = unsupportedNote();
if (settingTabContent.label && setting.pref) {
if (prefDefinition?.suggest) typeof prefDefinition.suggest.lowest !== "undefined" && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest), typeof prefDefinition.suggest.highest !== "undefined" && (this.suggestedSettings.highest[setting.pref] = prefDefinition.suggest.highest);
}
@@ -3813,8 +4144,60 @@ class SettingsNavigationDialog extends NavigationDialog {
});
$tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control);
}
+ renderTabContent(settingTab, items) {
+ let $tabContent = CE("div", {
+ class: "bx-gone",
+ "data-tab-group": settingTab.group
+ });
+ for (let settingTabContent of items) {
+ if (!settingTabContent) continue;
+ if (!this.isSupportedVariant(settingTabContent.requiredVariants)) continue;
+ if (!this.renderFullSettings && settingTab.group === "global" && settingTabContent.group !== "general" && settingTabContent.group !== "footer") continue;
+ let label = settingTabContent.label;
+ if (label === t("better-xcloud")) {
+ if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)";
+ label = createButton({
+ label,
+ url: "https://github.com/redphx/better-xcloud/releases",
+ style: 1024 | 8 | 32
+ });
+ }
+ if (label) {
+ let $title = CE("h2", {
+ _nearby: {
+ orientation: "horizontal"
+ }
+ }, CE("span", {}, label), settingTabContent.helpUrl && createButton({
+ icon: BxIcon.QUESTION,
+ style: 4 | 32,
+ url: settingTabContent.helpUrl,
+ title: t("help")
+ }));
+ $tabContent.appendChild($title);
+ }
+ if (settingTabContent.unsupportedNote) {
+ let $note = CE("b", { class: "bx-note-unsupported" }, settingTabContent.unsupportedNote);
+ $tabContent.appendChild($note);
+ }
+ if (settingTabContent.unsupported) continue;
+ if (settingTabContent.content) {
+ $tabContent.appendChild(settingTabContent.content);
+ continue;
+ }
+ settingTabContent.items = settingTabContent.items || [];
+ for (let setting of settingTabContent.items) {
+ if (setting === !1) continue;
+ if (typeof setting === "function") {
+ setting.apply(this, [$tabContent]);
+ continue;
+ }
+ this.renderSettingRow(settingTab, $tabContent, settingTabContent, setting);
+ }
+ }
+ return $tabContent;
+ }
setupDialog() {
- let $tabs, $settings, $container = CE("div", {
+ let $tabs, $tabContents, $container = CE("div", {
class: "bx-settings-dialog",
_nearby: {
orientation: "horizontal"
@@ -3848,7 +4231,7 @@ class SettingsNavigationDialog extends NavigationDialog {
onClick: (e) => {
this.dialogManager.hide();
}
- }))), $settings = CE("div", {
+ }))), $tabContents = CE("div", {
class: "bx-settings-tab-contents",
_nearby: {
orientation: "vertical",
@@ -3859,65 +4242,19 @@ class SettingsNavigationDialog extends NavigationDialog {
}
}
}));
- this.$container = $container, this.$tabs = $tabs, this.$settings = $settings, $container.addEventListener("click", (e) => {
+ this.$container = $container, this.$tabs = $tabs, this.$tabContents = $tabContents, $container.addEventListener("click", (e) => {
if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide();
});
- for (let settingTab of this.SETTINGS_UI) {
+ let settingTabGroup;
+ for (settingTabGroup in this.SETTINGS_UI) {
+ let settingTab = this.SETTINGS_UI[settingTabGroup];
if (!settingTab) continue;
if (!this.isSupportedVariant(settingTab.requiredVariants)) continue;
if (settingTab.group !== "global" && !this.renderFullSettings) continue;
let $svg = this.renderTab(settingTab);
- $tabs.appendChild($svg);
- let $tabContent = CE("div", {
- class: "bx-gone",
- "data-tab-group": settingTab.group
- });
- for (let settingTabContent of settingTab.items) {
- if (settingTabContent === !1) continue;
- if (!this.isSupportedVariant(settingTabContent.requiredVariants)) continue;
- if (!this.renderFullSettings && settingTab.group === "global" && settingTabContent.group !== "general" && settingTabContent.group !== "footer") continue;
- let label = settingTabContent.label;
- if (label === t("better-xcloud")) {
- if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)";
- label = createButton({
- label,
- url: "https://github.com/redphx/better-xcloud/releases",
- style: 1024 | 8 | 32
- });
- }
- if (label) {
- let $title = CE("h2", {
- _nearby: {
- orientation: "horizontal"
- }
- }, CE("span", {}, label), settingTabContent.helpUrl && createButton({
- icon: BxIcon.QUESTION,
- style: 4 | 32,
- url: settingTabContent.helpUrl,
- title: t("help")
- }));
- $tabContent.appendChild($title);
- }
- if (settingTabContent.unsupportedNote) {
- let $note = CE("b", { class: "bx-note-unsupported" }, settingTabContent.unsupportedNote);
- $tabContent.appendChild($note);
- }
- if (settingTabContent.unsupported) continue;
- if (settingTabContent.content) {
- $tabContent.appendChild(settingTabContent.content);
- continue;
- }
- settingTabContent.items = settingTabContent.items || [];
- for (let setting of settingTabContent.items) {
- if (setting === !1) continue;
- if (typeof setting === "function") {
- setting.apply(this, [$tabContent]);
- continue;
- }
- this.renderSettingRow(settingTab, $tabContent, settingTabContent, setting);
- }
- }
- $settings.appendChild($tabContent);
+ if ($tabs.appendChild($svg), typeof settingTab.items === "function") continue;
+ let $tabContent = this.renderTabContent.call(this, settingTab, settingTab.items);
+ $tabContents.appendChild($tabContent);
}
$tabs.firstElementChild.dispatchEvent(new Event("click"));
}
@@ -3933,7 +4270,7 @@ class SettingsNavigationDialog extends NavigationDialog {
return $currentTab && $currentTab.focus(), !0;
}
focusVisibleSetting(type = "first") {
- let controls = Array.from(this.$settings.querySelectorAll("div[data-tab-group]:not(.bx-gone) > *"));
+ let controls = Array.from(this.$tabContents.querySelectorAll("div[data-tab-group]:not(.bx-gone) > *"));
if (!controls.length) return !1;
if (type === "last") controls.reverse();
for (let $control of controls) {
@@ -3954,7 +4291,7 @@ class SettingsNavigationDialog extends NavigationDialog {
return !1;
}
jumpToSettingGroup(direction) {
- let $tabContent = this.$settings.querySelector("div[data-tab-group]:not(.bx-gone)");
+ let $tabContent = this.$tabContents.querySelector("div[data-tab-group]:not(.bx-gone)");
if (!$tabContent) return !1;
let $header, $focusing = document.activeElement;
if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2");
@@ -4085,52 +4422,58 @@ function getPreferredServerRegion(shortName = !1) {
return null;
}
class HeaderSection {
- static #$remotePlayBtn = createButton({
- classes: ["bx-header-remote-play-button", "bx-gone"],
- icon: BxIcon.REMOTE_PLAY,
- title: t("remote-play"),
- style: 4 | 32 | 512,
- onClick: (e) => {
- RemotePlayManager.getInstance().togglePopup();
- }
- });
- static #$settingsBtn = createButton({
- classes: ["bx-header-settings-button"],
- label: "???",
- style: 8 | 16 | 32 | 128,
- onClick: (e) => {
- SettingsNavigationDialog.getInstance().show();
- }
- });
- static #$buttonsWrapper = CE("div", {}, getPref("xhome_enabled") ? HeaderSection.#$remotePlayBtn : null, HeaderSection.#$settingsBtn);
- static #observer;
- static #timeout;
- static #injectSettingsButton($parent) {
- if (!$parent) return;
- let PREF_LATEST_VERSION = getPref("version_latest"), $btnSettings = HeaderSection.#$settingsBtn;
- if (isElementVisible(HeaderSection.#$buttonsWrapper)) return;
- if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true");
- $parent.appendChild(HeaderSection.#$buttonsWrapper);
+ static instance;
+ static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection);
+ LOG_TAG = "HeaderSection";
+ $btnRemotePlay;
+ $btnSettings;
+ $buttonsWrapper;
+ observer;
+ timeoutId;
+ constructor() {
+ BxLogger.info(this.LOG_TAG, "constructor()"), this.$btnRemotePlay = createButton({
+ classes: ["bx-header-remote-play-button", "bx-gone"],
+ icon: BxIcon.REMOTE_PLAY,
+ title: t("remote-play"),
+ style: 4 | 32 | 512,
+ onClick: (e) => RemotePlayManager.getInstance().togglePopup()
+ }), this.$btnSettings = createButton({
+ classes: ["bx-header-settings-button"],
+ label: "???",
+ style: 8 | 16 | 32 | 128,
+ onClick: (e) => SettingsNavigationDialog.getInstance().show()
+ }), this.$buttonsWrapper = CE("div", {}, getPref("xhome_enabled") ? this.$btnRemotePlay : null, this.$btnSettings);
}
- static checkHeader() {
+ injectSettingsButton($parent) {
+ if (!$parent) return;
+ let PREF_LATEST_VERSION = getPref("version_latest"), $btnSettings = this.$btnSettings;
+ if (isElementVisible(this.$buttonsWrapper)) return;
+ if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true");
+ $parent.appendChild(this.$buttonsWrapper);
+ }
+ checkHeader() {
let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]");
if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]");
- $target && HeaderSection.#injectSettingsButton($target);
+ $target && this.injectSettingsButton($target);
}
- static showRemotePlayButton() {
- HeaderSection.#$remotePlayBtn.classList.remove("bx-gone");
- }
- static watchHeader() {
+ watchHeader() {
let $root = document.querySelector("#PageContent header") || document.querySelector("#root");
if (!$root) return;
- HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout), HeaderSection.#timeout = null, HeaderSection.#observer && HeaderSection.#observer.disconnect(), HeaderSection.#observer = new MutationObserver((mutationList) => {
- HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout), HeaderSection.#timeout = window.setTimeout(HeaderSection.checkHeader, 2000);
- }), HeaderSection.#observer.observe($root, { subtree: !0, childList: !0 }), HeaderSection.checkHeader();
+ this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, this.observer && this.observer.disconnect(), this.observer = new MutationObserver((mutationList) => {
+ this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader.bind(this), 2000);
+ }), this.observer.observe($root, { subtree: !0, childList: !0 }), this.checkHeader();
+ }
+ showRemotePlayButton() {
+ this.$btnRemotePlay.classList.remove("bx-gone");
+ }
+ static watchHeader() {
+ HeaderSection.getInstance().watchHeader();
}
}
class RemotePlayNavigationDialog extends NavigationDialog {
static instance;
static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog);
+ LOG_TAG = "RemotePlayNavigationDialog";
STATE_LABELS = {
On: t("powered-on"),
Off: t("powered-off"),
@@ -4140,7 +4483,7 @@ class RemotePlayNavigationDialog extends NavigationDialog {
$container;
constructor() {
super();
- this.setupDialog();
+ BxLogger.info(this.LOG_TAG, "constructor()"), this.setupDialog();
}
setupDialog() {
let $fragment = CE("div", { class: "bx-remote-play-container" }), $settingNote = CE("p", {}), currentResolution = getPref("xhome_resolution"), $resolutions = CE("select", {}, CE("option", { value: "1080p" }, "1080p"), CE("option", { value: "720p" }, "720p"));
@@ -4192,20 +4535,23 @@ class RemotePlayNavigationDialog extends NavigationDialog {
$btnConnect && $btnConnect.focus();
}
}
-var LOG_TAG2 = "RemotePlay";
class RemotePlayManager {
static instance;
static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager);
+ LOG_TAG = "RemotePlayManager";
isInitialized = !1;
XCLOUD_TOKEN;
XHOME_TOKEN;
consoles;
regions = [];
+ constructor() {
+ BxLogger.info(this.LOG_TAG, "constructor()");
+ }
initialize() {
if (this.isInitialized) return;
this.isInitialized = !0, this.getXhomeToken(() => {
this.getConsolesList(() => {
- BxLogger.info(LOG_TAG2, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY);
+ BxLogger.info(this.LOG_TAG, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.getInstance().showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY);
});
});
}
@@ -4366,54 +4712,10 @@ class LoadingScreen {
}
}
class GuideMenu {
- static #BUTTONS = {
- scriptSettings: createButton({
- label: t("better-xcloud"),
- style: 64 | 32 | 1,
- onClick: (e) => {
- window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, (e2) => {
- setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50);
- }, { once: !0 }), GuideMenu.#closeGuideMenu();
- }
- }),
- closeApp: AppInterface && createButton({
- icon: BxIcon.POWER,
- label: t("close-app"),
- title: t("close-app"),
- style: 64 | 32 | 2,
- onClick: (e) => {
- AppInterface.closeApp();
- },
- attributes: {
- "data-state": "normal"
- }
- }),
- reloadPage: createButton({
- icon: BxIcon.REFRESH,
- label: t("reload-page"),
- title: t("reload-page"),
- style: 64 | 32,
- onClick: (e) => {
- if (STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload();
- else window.location.reload();
- GuideMenu.#closeGuideMenu();
- }
- }),
- backToHome: createButton({
- icon: BxIcon.HOME,
- label: t("back-to-home"),
- title: t("back-to-home"),
- style: 64 | 32,
- onClick: (e) => {
- confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)), GuideMenu.#closeGuideMenu();
- },
- attributes: {
- "data-state": "playing"
- }
- })
- };
- static #$renderedButtons;
- static #closeGuideMenu() {
+ static instance;
+ static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu);
+ $renderedButtons;
+ closeGuideMenu() {
if (window.BX_EXPOSED.dialogRoutes) {
window.BX_EXPOSED.dialogRoutes.closeAll();
return;
@@ -4421,19 +4723,63 @@ class GuideMenu {
let $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]");
$btnClose && $btnClose.click();
}
- static #renderButtons() {
- if (GuideMenu.#$renderedButtons) return GuideMenu.#$renderedButtons;
- let $div = CE("div", {
- class: "bx-guide-home-buttons"
- }), buttons = [
- GuideMenu.#BUTTONS.scriptSettings,
+ renderButtons() {
+ if (this.$renderedButtons) return this.$renderedButtons;
+ let buttons = {
+ scriptSettings: createButton({
+ label: t("better-xcloud"),
+ style: 64 | 32 | 1,
+ onClick: (() => {
+ window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, (e) => {
+ setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50);
+ }, { once: !0 }), this.closeGuideMenu();
+ }).bind(this)
+ }),
+ closeApp: AppInterface && createButton({
+ icon: BxIcon.POWER,
+ label: t("close-app"),
+ title: t("close-app"),
+ style: 64 | 32 | 2,
+ onClick: (e) => {
+ AppInterface.closeApp();
+ },
+ attributes: {
+ "data-state": "normal"
+ }
+ }),
+ reloadPage: createButton({
+ icon: BxIcon.REFRESH,
+ label: t("reload-page"),
+ title: t("reload-page"),
+ style: 64 | 32,
+ onClick: (() => {
+ if (this.closeGuideMenu(), STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload();
+ else window.location.reload();
+ }).bind(this)
+ }),
+ backToHome: createButton({
+ icon: BxIcon.HOME,
+ label: t("back-to-home"),
+ title: t("back-to-home"),
+ style: 64 | 32,
+ onClick: (() => {
+ this.closeGuideMenu(), confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));
+ }).bind(this),
+ attributes: {
+ "data-state": "playing"
+ }
+ })
+ }, buttonsLayout = [
+ buttons.scriptSettings,
[
- GuideMenu.#BUTTONS.backToHome,
- GuideMenu.#BUTTONS.reloadPage,
- GuideMenu.#BUTTONS.closeApp
+ buttons.backToHome,
+ buttons.reloadPage,
+ buttons.closeApp
]
- ];
- for (let $button of buttons) {
+ ], $div = CE("div", {
+ class: "bx-guide-home-buttons"
+ });
+ for (let $button of buttonsLayout) {
if (!$button) continue;
if ($button instanceof HTMLElement) $div.appendChild($button);
else if (Array.isArray($button)) {
@@ -4443,9 +4789,9 @@ class GuideMenu {
$div.appendChild($wrapper);
}
}
- return GuideMenu.#$renderedButtons = $div, $div;
+ return this.$renderedButtons = $div, $div;
}
- static #injectHome($root, isPlaying = !1) {
+ injectHome($root, isPlaying = !1) {
let $target = null;
if (isPlaying) {
$target = $root.querySelector("a[class*=QuitGameButton]");
@@ -4456,19 +4802,19 @@ class GuideMenu {
if ($dividers) $target = $dividers[$dividers.length - 1];
}
if (!$target) return !1;
- let $buttons = GuideMenu.#renderButtons();
+ let $buttons = this.renderButtons();
$buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons);
}
- static async#onShown(e) {
+ async onShown(e) {
if (e.where === "home") {
let $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]");
- $root && GuideMenu.#injectHome($root, STATES.isPlaying);
+ $root && this.injectHome($root, STATES.isPlaying);
}
}
- static addEventListeners() {
- window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown);
+ addEventListeners() {
+ window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown.bind(this));
}
- static observe($addedElm) {
+ observe($addedElm) {
let className = $addedElm.className;
if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return;
let $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true");
@@ -4483,6 +4829,7 @@ class GuideMenu {
class StreamBadges {
static instance;
static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges);
+ LOG_TAG = "StreamBadges";
serverInfo = {};
badges = {
playtime: {
@@ -4524,6 +4871,9 @@ class StreamBadges {
$container;
intervalId;
REFRESH_INTERVAL = 3000;
+ constructor() {
+ BxLogger.info(this.LOG_TAG, "constructor()");
+ }
setRegion(region) {
this.serverInfo.server = {
region
@@ -4688,6 +5038,7 @@ class XcloudInterceptor {
return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response;
}
static async handlePlay(request, init) {
+ BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
let PREF_STREAM_TARGET_RESOLUTION = getPref("stream_target_resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream_preferred_locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0];
for (let regionName in STATES.serverRegions) {
let region = STATES.serverRegions[regionName];
@@ -4720,6 +5071,7 @@ class XcloudInterceptor {
if (request.method !== "GET") return NATIVE_FETCH(request, init);
let response = await NATIVE_FETCH(request, init), text = await response.clone().text();
if (!text.length) return response;
+ BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {};
overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0;
let overrideMkb = null;
@@ -4751,9 +5103,7 @@ function clearDbLogs(dbName, table) {
let db = e.target.result;
try {
let objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear();
- objectStoreRequest.onsuccess = function() {
- console.log(`[Better xCloud] Cleared ${dbName}.${table}`);
- };
+ objectStoreRequest.onsuccess = () => BxLogger.info("clearDbLogs", `Cleared ${dbName}.${table}`);
} catch (ex) {}
};
}
@@ -4802,7 +5152,8 @@ function interceptHttpRequests() {
"https://arc.msn.com",
"https://browser.events.data.microsoft.com",
"https://dc.services.visualstudio.com",
- "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io"
+ "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io",
+ "https://mscom.demdex.net"
]);
if (getPref("block_social_features")) BLOCKED_URLS = BLOCKED_URLS.concat([
"https://peoplehub.xboxlive.com/users/me/people/social",
@@ -4820,25 +5171,36 @@ function interceptHttpRequests() {
}
return nativeXhrSend.apply(this, arguments);
};
- let gamepassAllGames = [];
+ let gamepassAllGames = [], IGNORED_DOMAINS = [
+ "accounts.xboxlive.com",
+ "chat.xboxlive.com",
+ "notificationinbox.xboxlive.com",
+ "peoplehub.xboxlive.com",
+ "rta.xboxlive.com",
+ "userpresence.xboxlive.com",
+ "xblmessaging.xboxlive.com",
+ "consent.config.office.com",
+ "arc.msn.com",
+ "browser.events.data.microsoft.com",
+ "dc.services.visualstudio.com",
+ "2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io"
+ ];
window.BX_FETCH = window.fetch = async (request, init) => {
let url = typeof request === "string" ? request : request.url;
- for (let blocked of BLOCKED_URLS) {
- if (!url.startsWith(blocked)) continue;
- return new Response('{"acc":1,"webResult":{}}', {
- status: 200,
- statusText: "200 OK"
- });
- }
- if (url.endsWith("/play")) BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
- if (url.endsWith("/configuration")) BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
+ for (let blocked of BLOCKED_URLS)
+ if (url.startsWith(blocked)) return new Response('{"acc":1,"webResult":{}}', {
+ status: 200,
+ statusText: "200 OK"
+ });
+ let domain = new URL(url).hostname;
+ if (IGNORED_DOMAINS.includes(domain)) return NATIVE_FETCH(request, init);
if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try {
let response = await NATIVE_FETCH(request, init), json = await response.json();
if (json && json.exp && json.exp.treatments) for (let key in FeatureGates)
json.exp.treatments[key] = FeatureGates[key];
return response.json = () => Promise.resolve(json), response;
} catch (e) {
- console.log(e);
+ return console.log(e), NATIVE_FETCH(request, init);
}
if (STATES.userAgent.capabilities.touch && url.includes("catalog.gamepass.com/sigls/")) {
let response = await NATIVE_FETCH(request, init), obj = await response.clone().json();
@@ -5511,7 +5873,7 @@ class RootDialogObserver {
if (AppInterface && $addedElm.className.startsWith("SlideSheet-module__container")) {
let $gameCardMenu = $addedElm.querySelector("div[class^=MruContextMenu],div[class^=GameCardContextMenu]");
if ($gameCardMenu) return RootDialogObserver.handleGameCardMenu($gameCardMenu), !0;
- } else if ($root.querySelector("div[class*=GuideDialog]")) return GuideMenu.observe($addedElm), !0;
+ } else if ($root.querySelector("div[class*=GuideDialog]")) return GuideMenu.getInstance().observe($addedElm), !0;
return !1;
}
static observe($root) {
@@ -5624,6 +5986,6 @@ window.addEventListener("pagehide", (e) => {
function main() {
if (getPref("game_msfs2020_force_native_mkb")) BX_FLAGS.ForceNativeMkbTitles.push("9PMQDM08SNK9");
if (patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), getPref("audio_enable_volume_control") && patchAudioContext(), getPref("block_tracking")) patchMeControl(), disableAdobeAudienceManager();
- if (RootDialogObserver.waitForRootDialog(), addCss(), Toast.setup(), GuideMenu.addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), getPref("controller_show_connection_status")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));
+ if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), getPref("controller_show_connection_status")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));
}
main();
diff --git a/src/index.ts b/src/index.ts
index 6adb998..643b905 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,7 +12,6 @@ import { EmulatedMkbHandler } from "@modules/mkb/mkb-handler";
import { StreamBadges } from "@modules/stream/stream-badges";
import { StreamStats } from "@modules/stream/stream-stats";
import { addCss, preloadFonts } from "@utils/css";
-import { Toast } from "@utils/toast";
import { LoadingScreen } from "@modules/loading-screen";
import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider";
import { TouchController } from "@modules/touch-controller";
@@ -26,7 +25,7 @@ import { disableAdobeAudienceManager, patchAudioContext, patchCanvasContext, pat
import { AppInterface, STATES } from "@utils/global";
import { BxLogger } from "@utils/bx-logger";
import { GameBar } from "./modules/game-bar/game-bar";
-import { Screenshot } from "./utils/screenshot";
+import { ScreenshotManager } from "./utils/screenshot-manager";
import { NativeMkbHandler } from "./modules/mkb/native-mkb-handler";
import { GuideMenu } from "./modules/ui/guide-menu";
import { updateVideoPlayer } from "./modules/stream/stream-settings-utils";
@@ -170,7 +169,7 @@ document.addEventListener('readystatechange', e => {
// Hide "Play with Friends" skeleton section
if (getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS)) {
- const $parent = document.querySelector('div[class*=PlayWithFriendsSkeleton]')?.closest('div[class*=HomePage-module]') as HTMLElement;
+ const $parent = document.querySelector('div[class*=PlayWithFriendsSkeleton]')?.closest('div[class*=HomePage-module]');
$parent && ($parent.style.display = 'none');
}
@@ -194,7 +193,7 @@ window.addEventListener(BxEvent.XCLOUD_SERVERS_UNAVAILABLE, e => {
window.setTimeout(HeaderSection.watchHeader, 2000);
// Open Settings dialog on Unsupported page
- const $unsupportedPage = document.querySelector('div[class^=UnsupportedMarketPage-module__container]') as HTMLElement;
+ const $unsupportedPage = document.querySelector('div[class^=UnsupportedMarketPage-module__container]');
if ($unsupportedPage) {
SettingsNavigationDialog.getInstance().show();
}
@@ -241,7 +240,7 @@ window.addEventListener(BxEvent.STREAM_PLAYING, e => {
if (isFullVersion()) {
const $video = (e as any).$video as HTMLVideoElement;
- Screenshot.updateCanvasSize($video.videoWidth, $video.videoHeight);
+ ScreenshotManager.getInstance().updateCanvasSize($video.videoWidth, $video.videoHeight);
}
updateVideoPlayer();
@@ -316,7 +315,7 @@ function unload() {
if (isFullVersion()) {
MouseCursorHider.stop();
TouchController.reset();
- GameBar.getInstance().disable();
+ (getPref(PrefKey.GAME_BAR_POSITION) !== 'off') && GameBar.getInstance().disable();
}
}
@@ -326,7 +325,7 @@ window.addEventListener('pagehide', e => {
});
isFullVersion() && window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, e => {
- Screenshot.takeScreenshot();
+ ScreenshotManager.getInstance().takeScreenshot();
});
@@ -354,17 +353,13 @@ function main() {
// Setup UI
addCss();
- Toast.setup();
- GuideMenu.addEventListeners();
+ GuideMenu.getInstance().addEventListeners();
StreamStatsCollector.setupEvents();
StreamBadges.setupEvents();
StreamStats.setupEvents();
if (isFullVersion()) {
- (getPref(PrefKey.GAME_BAR_POSITION) !== 'off') && GameBar.getInstance();
- Screenshot.setup();
-
STATES.userAgent.capabilities.touch && TouchController.updateCustomList();
overridePreloadState();
diff --git a/src/modules/controller-shortcut.ts b/src/modules/controller-shortcut.ts
index 70c1cf6..13acace 100644
--- a/src/modules/controller-shortcut.ts
+++ b/src/modules/controller-shortcut.ts
@@ -1,4 +1,4 @@
-import { Screenshot } from "@utils/screenshot";
+import { ScreenshotManager } from "@/utils/screenshot-manager";
import { GamepadKey } from "@enums/mkb";
import { PrompFont } from "@enums/prompt-font";
import { CE, removeChildElements } from "@utils/html";
@@ -97,7 +97,7 @@ export class ControllerShortcut {
break;
case ShortcutAction.STREAM_SCREENSHOT_CAPTURE:
- Screenshot.takeScreenshot();
+ ScreenshotManager.getInstance().takeScreenshot();
break;
case ShortcutAction.STREAM_STATS_TOGGLE:
@@ -163,8 +163,6 @@ export class ControllerShortcut {
// Save to storage
window.localStorage.setItem(ControllerShortcut.STORAGE_KEY, JSON.stringify(ControllerShortcut.ACTIONS));
-
- console.log(ControllerShortcut.ACTIONS);
}
private static updateProfileList(e?: GamepadEvent) {
diff --git a/src/modules/dialog.ts b/src/modules/dialog.ts
index 1920822..96b8d14 100644
--- a/src/modules/dialog.ts
+++ b/src/modules/dialog.ts
@@ -30,7 +30,7 @@ export class Dialog {
} = options;
// Create dialog overlay
- const $overlay = document.querySelector('.bx-dialog-overlay') as HTMLElement;
+ const $overlay = document.querySelector('.bx-dialog-overlay');
if (!$overlay) {
this.$overlay = CE('div', {'class': 'bx-dialog-overlay bx-gone'});
diff --git a/src/modules/game-bar/action-screenshot.ts b/src/modules/game-bar/action-screenshot.ts
index 4275618..073e66f 100644
--- a/src/modules/game-bar/action-screenshot.ts
+++ b/src/modules/game-bar/action-screenshot.ts
@@ -2,7 +2,7 @@ import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle } from "@utils/html";
import { BaseGameBarAction } from "./action-base";
import { t } from "@utils/translation";
-import { Screenshot } from "@/utils/screenshot";
+import { ScreenshotManager } from "@/utils/screenshot-manager";
export class ScreenshotAction extends BaseGameBarAction {
$content: HTMLElement;
@@ -20,6 +20,6 @@ export class ScreenshotAction extends BaseGameBarAction {
onClick(e: Event): void {
super.onClick(e);
- Screenshot.takeScreenshot();
+ ScreenshotManager.getInstance().takeScreenshot();
}
}
diff --git a/src/modules/game-bar/action-true-achievements.ts b/src/modules/game-bar/action-true-achievements.ts
index bd33bbe..690dc10 100644
--- a/src/modules/game-bar/action-true-achievements.ts
+++ b/src/modules/game-bar/action-true-achievements.ts
@@ -18,6 +18,6 @@ export class TrueAchievementsAction extends BaseGameBarAction {
onClick(e: Event) {
super.onClick(e);
- TrueAchievements.open(false);
+ TrueAchievements.getInstance().open(false);
}
}
diff --git a/src/modules/game-bar/game-bar.ts b/src/modules/game-bar/game-bar.ts
index 04f853b..ba0037d 100644
--- a/src/modules/game-bar/game-bar.ts
+++ b/src/modules/game-bar/game-bar.ts
@@ -11,11 +11,13 @@ import { getPref, StreamTouchController, type GameBarPosition } from "@/utils/se
import { TrueAchievementsAction } from "./action-true-achievements";
import { SpeakerAction } from "./action-speaker";
import { RendererAction } from "./action-renderer";
+import { BxLogger } from "@/utils/bx-logger";
export class GameBar {
private static instance: GameBar;
public static getInstance = () => GameBar.instance ?? (GameBar.instance = new GameBar());
+ private readonly LOG_TAG = 'GameBar';
private static readonly VISIBLE_DURATION = 2000;
@@ -27,6 +29,8 @@ export class GameBar {
private actions: BaseGameBarAction[] = [];
private constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
+
let $container;
const position = getPref(PrefKey.GAME_BAR_POSITION) as GameBarPosition;
diff --git a/src/modules/mkb/mkb-handler.ts b/src/modules/mkb/mkb-handler.ts
index 45c2b70..6a36984 100644
--- a/src/modules/mkb/mkb-handler.ts
+++ b/src/modules/mkb/mkb-handler.ts
@@ -6,7 +6,6 @@ import { createButton, ButtonStyle, CE } from "@utils/html";
import { BxEvent } from "@utils/bx-event";
import { Toast } from "@utils/toast";
import { t } from "@utils/translation";
-import { LocalDb } from "@utils/local-db";
import { KeyHelper } from "./key-helper";
import type { MkbStoredPreset } from "@/types/mkb";
import { AppInterface, STATES } from "@utils/global";
@@ -19,8 +18,7 @@ import { SettingsNavigationDialog } from "../ui/dialog/settings-dialog";
import { NavigationDialogManager } from "../ui/dialog/navigation-dialog";
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
-
-const LOG_TAG = 'MkbHandler';
+import { MkbPresetsDb } from "@/utils/local-db/mkb-presets-db";
const PointerToMouseButton = {
1: 0,
@@ -126,6 +124,7 @@ Source: https://github.com/yuzu-emu/yuzu-mainline/blob/master/src/input_common/d
export class EmulatedMkbHandler extends MkbHandler {
private static instance: EmulatedMkbHandler;
public static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler());
+ private static readonly LOG_TAG = 'EmulatedMkbHandler';
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
@@ -167,8 +166,9 @@ export class EmulatedMkbHandler extends MkbHandler {
#RIGHT_STICK_X: GamepadKey[] = [];
#RIGHT_STICK_Y: GamepadKey[] = [];
- constructor() {
+ private constructor() {
super();
+ BxLogger.info(EmulatedMkbHandler.LOG_TAG, 'constructor()');
this.#STICK_MAP = {
[GamepadKey.LS_LEFT]: [this.#LEFT_STICK_X, 0, -1],
@@ -431,7 +431,7 @@ export class EmulatedMkbHandler extends MkbHandler {
#getCurrentPreset = (): Promise => {
return new Promise(resolve => {
const presetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
- LocalDb.INSTANCE.getPreset(presetId).then((preset: MkbStoredPreset) => {
+ MkbPresetsDb.getInstance().getPreset(presetId).then((preset: MkbStoredPreset) => {
resolve(preset);
});
});
@@ -680,7 +680,7 @@ export class EmulatedMkbHandler extends MkbHandler {
AppInterface && NativeMkbHandler.getInstance().init();
}
} else if (getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile())) {
- BxLogger.info(LOG_TAG, 'Emulate MKB');
+ BxLogger.info(EmulatedMkbHandler.LOG_TAG, 'Emulate MKB');
EmulatedMkbHandler.getInstance().init();
}
});
diff --git a/src/modules/mkb/mkb-preset.ts b/src/modules/mkb/mkb-preset.ts
index fde9e84..778dd1f 100644
--- a/src/modules/mkb/mkb-preset.ts
+++ b/src/modules/mkb/mkb-preset.ts
@@ -130,7 +130,6 @@ export class MkbPreset {
mouse[MkbPresetKey.MOUSE_MAP_TO] = MkbPreset.MOUSE_SETTINGS[MkbPresetKey.MOUSE_MAP_TO].default;
}
- console.log(obj);
return obj;
}
}
diff --git a/src/modules/mkb/mkb-remapper.ts b/src/modules/mkb/mkb-remapper.ts
index 868d158..a4b1c75 100644
--- a/src/modules/mkb/mkb-remapper.ts
+++ b/src/modules/mkb/mkb-remapper.ts
@@ -4,7 +4,6 @@ import { Dialog } from "@modules/dialog";
import { KeyHelper } from "./key-helper";
import { MkbPreset } from "./mkb-preset";
import { EmulatedMkbHandler } from "./mkb-handler";
-import { LocalDb } from "@utils/local-db";
import { BxIcon } from "@utils/bx-icon";
import type { MkbPresetData, MkbStoredPresets } from "@/types/mkb";
import { MkbPresetKey, GamepadKey, GamepadKeyName } from "@enums/mkb";
@@ -12,18 +11,10 @@ import { deepClone } from "@utils/global";
import { SettingElement } from "@/utils/setting-element";
import { PrefKey } from "@/enums/pref-keys";
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
+import { MkbPresetsDb } from "@/utils/local-db/mkb-presets-db";
+import { BxLogger } from "@/utils/bx-logger";
-type MkbRemapperElements = {
- wrapper: HTMLElement | null;
- presetsSelect: HTMLSelectElement | null;
- activateButton: HTMLButtonElement | null;
- currentBindingKey: HTMLElement | null;
-
- allKeyElements: HTMLElement[];
- allMouseElements: {[key in MkbPresetKey]?: HTMLElement};
-};
-
type MkbRemapperStates = {
currentPresetId: number;
presets: MkbStoredPresets;
@@ -33,7 +24,7 @@ type MkbRemapperStates = {
};
export class MkbRemapper {
- readonly #BUTTON_ORDERS = [
+ private readonly BUTTON_ORDERS = [
GamepadKey.UP,
GamepadKey.DOWN,
GamepadKey.LEFT,
@@ -66,16 +57,11 @@ export class MkbRemapper {
GamepadKey.RS_RIGHT,
];
- static #instance: MkbRemapper;
- static get INSTANCE() {
- if (!MkbRemapper.#instance) {
- MkbRemapper.#instance = new MkbRemapper();
- }
+ private static instance: MkbRemapper;
+ public static getInstance = () => MkbRemapper.instance ?? (MkbRemapper.instance = new MkbRemapper());
+ private readonly LOG_TAG = 'MkbRemapper';
- return MkbRemapper.#instance;
- };
-
- #STATE: MkbRemapperStates = {
+ private STATE: MkbRemapperStates = {
currentPresetId: 0,
presets: {},
@@ -84,151 +70,150 @@ export class MkbRemapper {
isEditing: false,
};
- #$: MkbRemapperElements = {
- wrapper: null,
- presetsSelect: null,
- activateButton: null,
+ private $wrapper!: HTMLElement;
+ private $presetsSelect!: HTMLSelectElement;
+ private $activateButton!: HTMLButtonElement;
- currentBindingKey: null,
+ private $currentBindingKey!: HTMLElement;
- allKeyElements: [],
- allMouseElements: {},
- };
+ private allKeyElements: HTMLElement[] = [];
+ private allMouseElements: {[key in MkbPresetKey]?: HTMLElement} = {};
bindingDialog: Dialog;
- constructor() {
- this.#STATE.currentPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
+ private constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
+ this.STATE.currentPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
this.bindingDialog = new Dialog({
className: 'bx-binding-dialog',
content: CE('div', {},
- CE('p', {}, t('press-to-bind')),
- CE('i', {}, t('press-esc-to-cancel')),
- ),
+ CE('p', {}, t('press-to-bind')),
+ CE('i', {}, t('press-esc-to-cancel')),
+ ),
hideCloseButton: true,
});
}
- #clearEventListeners = () => {
- window.removeEventListener('keydown', this.#onKeyDown);
- window.removeEventListener('mousedown', this.#onMouseDown);
- window.removeEventListener('wheel', this.#onWheel);
+ private clearEventListeners = () => {
+ window.removeEventListener('keydown', this.onKeyDown);
+ window.removeEventListener('mousedown', this.onMouseDown);
+ window.removeEventListener('wheel', this.onWheel);
};
- #bindKey = ($elm: HTMLElement, key: any) => {
- const buttonIndex = parseInt($elm.getAttribute('data-button-index')!);
- const keySlot = parseInt($elm.getAttribute('data-key-slot')!);
+ private bindKey = ($elm: HTMLElement, key: any) => {
+ const buttonIndex = parseInt($elm.dataset.buttonIndex!);
+ const keySlot = parseInt($elm.dataset.keySlot!);
// Ignore if bind the save key to the same element
- if ($elm.getAttribute('data-key-code') === key.code) {
+ if ($elm.dataset.keyCode! === key.code) {
return;
}
// Unbind duplicated keys
- for (const $otherElm of this.#$.allKeyElements) {
- if ($otherElm.getAttribute('data-key-code') === key.code) {
- this.#unbindKey($otherElm);
+ for (const $otherElm of this.allKeyElements) {
+ if ($otherElm.dataset.keyCode === key.code) {
+ this.unbindKey($otherElm);
}
}
- this.#STATE.editingPresetData!.mapping[buttonIndex][keySlot] = key.code;
+ this.STATE.editingPresetData!.mapping[buttonIndex][keySlot] = key.code;
$elm.textContent = key.name;
- $elm.setAttribute('data-key-code', key.code);
+ $elm.dataset.keyCode = key.code;
}
- #unbindKey = ($elm: HTMLElement) => {
- const buttonIndex = parseInt($elm.getAttribute('data-button-index')!);
- const keySlot = parseInt($elm.getAttribute('data-key-slot')!);
+ private unbindKey = ($elm: HTMLElement) => {
+ const buttonIndex = parseInt($elm.dataset.buttonIndex!);
+ const keySlot = parseInt($elm.dataset.keySlot!);
// Remove key from preset
- this.#STATE.editingPresetData!.mapping[buttonIndex][keySlot] = null;
+ this.STATE.editingPresetData!.mapping[buttonIndex][keySlot] = null;
$elm.textContent = '';
- $elm.removeAttribute('data-key-code');
+ delete $elm.dataset.keyCode;
}
- #onWheel = (e: WheelEvent) => {
+ private onWheel = (e: WheelEvent) => {
e.preventDefault();
- this.#clearEventListeners();
+ this.clearEventListeners();
- this.#bindKey(this.#$.currentBindingKey!, KeyHelper.getKeyFromEvent(e));
+ this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e));
window.setTimeout(() => this.bindingDialog.hide(), 200);
};
- #onMouseDown = (e: MouseEvent) => {
+ private onMouseDown = (e: MouseEvent) => {
e.preventDefault();
- this.#clearEventListeners();
+ this.clearEventListeners();
- this.#bindKey(this.#$.currentBindingKey!, KeyHelper.getKeyFromEvent(e));
+ this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e));
window.setTimeout(() => this.bindingDialog.hide(), 200);
};
- #onKeyDown = (e: KeyboardEvent) => {
+ private onKeyDown = (e: KeyboardEvent) => {
e.preventDefault();
e.stopPropagation();
- this.#clearEventListeners();
+ this.clearEventListeners();
if (e.code !== 'Escape') {
- this.#bindKey(this.#$.currentBindingKey!, KeyHelper.getKeyFromEvent(e));
+ this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e));
}
window.setTimeout(() => this.bindingDialog.hide(), 200);
};
- #onBindingKey = (e: MouseEvent) => {
- if (!this.#STATE.isEditing || e.button !== 0) {
+ private onBindingKey = (e: MouseEvent) => {
+ if (!this.STATE.isEditing || e.button !== 0) {
return;
}
console.log(e);
- this.#$.currentBindingKey = e.target as HTMLElement;
+ this.$currentBindingKey = e.target as HTMLElement;
- window.addEventListener('keydown', this.#onKeyDown);
- window.addEventListener('mousedown', this.#onMouseDown);
- window.addEventListener('wheel', this.#onWheel);
+ window.addEventListener('keydown', this.onKeyDown);
+ window.addEventListener('mousedown', this.onMouseDown);
+ window.addEventListener('wheel', this.onWheel);
- this.bindingDialog.show({title: this.#$.currentBindingKey.getAttribute('data-prompt')!});
+ this.bindingDialog.show({title: this.$currentBindingKey.dataset.prompt!});
};
- #onContextMenu = (e: Event) => {
+ private onContextMenu = (e: Event) => {
e.preventDefault();
- if (!this.#STATE.isEditing) {
+ if (!this.STATE.isEditing) {
return;
}
- this.#unbindKey(e.target as HTMLElement);
+ this.unbindKey(e.target as HTMLElement);
};
- #getPreset = (presetId: number) => {
- return this.#STATE.presets[presetId];
+ private getPreset = (presetId: number) => {
+ return this.STATE.presets[presetId];
}
- #getCurrentPreset = () => {
- return this.#getPreset(this.#STATE.currentPresetId);
+ private getCurrentPreset = () => {
+ return this.getPreset(this.STATE.currentPresetId);
}
- #switchPreset = (presetId: number) => {
- this.#STATE.currentPresetId = presetId;
- const presetData = this.#getCurrentPreset().data;
+ private switchPreset = (presetId: number) => {
+ this.STATE.currentPresetId = presetId;
+ const presetData = this.getCurrentPreset().data;
- for (const $elm of this.#$.allKeyElements) {
- const buttonIndex = parseInt($elm.getAttribute('data-button-index')!);
- const keySlot = parseInt($elm.getAttribute('data-key-slot')!);
+ for (const $elm of this.allKeyElements) {
+ const buttonIndex = parseInt($elm.dataset.buttonIndex!);
+ const keySlot = parseInt($elm.dataset.keySlot!);
const buttonKeys = presetData.mapping[buttonIndex];
if (buttonKeys && buttonKeys[keySlot]) {
$elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]!);
- $elm.setAttribute('data-key-code', buttonKeys[keySlot]!);
+ $elm.dataset.keyCode = buttonKeys[keySlot]!;
} else {
$elm.textContent = '';
- $elm.removeAttribute('data-key-code');
+ delete $elm.dataset.keyCode;
}
}
let key: MkbPresetKey;
- for (key in this.#$.allMouseElements) {
- const $elm = this.#$.allMouseElements[key]!;
+ for (key in this.allMouseElements) {
+ const $elm = this.allMouseElements[key]!;
let value = presetData.mouse[key];
if (typeof value === 'undefined') {
value = MkbPreset.MOUSE_SETTINGS[key].default;
@@ -238,26 +223,26 @@ export class MkbRemapper {
}
// Update state of Activate button
- const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.#STATE.currentPresetId;
- this.#$.activateButton!.disabled = activated;
- this.#$.activateButton!.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
+ const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.STATE.currentPresetId;
+ this.$activateButton.disabled = activated;
+ this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
}
- #refresh() {
+ private refresh() {
// Clear presets select
- while (this.#$.presetsSelect!.firstChild) {
- this.#$.presetsSelect!.removeChild(this.#$.presetsSelect!.firstChild);
+ while (this.$presetsSelect.firstChild) {
+ this.$presetsSelect.removeChild(this.$presetsSelect.firstChild);
}
- LocalDb.INSTANCE.getPresets().then(presets => {
- this.#STATE.presets = presets;
+ MkbPresetsDb.getInstance().getPresets().then(presets => {
+ this.STATE.presets = presets;
const $fragment = document.createDocumentFragment();
let defaultPresetId;
- if (this.#STATE.currentPresetId === 0) {
- this.#STATE.currentPresetId = parseInt(Object.keys(presets)[0]);
+ if (this.STATE.currentPresetId === 0) {
+ this.STATE.currentPresetId = parseInt(Object.keys(presets)[0]);
- defaultPresetId = this.#STATE.currentPresetId;
+ defaultPresetId = this.STATE.currentPresetId;
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId);
EmulatedMkbHandler.getInstance().refreshPresetData();
} else {
@@ -272,40 +257,40 @@ export class MkbRemapper {
}
const $options = CE('option', {value: id}, name);
- $options.selected = parseInt(id) === this.#STATE.currentPresetId;
+ $options.selected = parseInt(id) === this.STATE.currentPresetId;
$fragment.appendChild($options);
};
- this.#$.presetsSelect!.appendChild($fragment);
+ this.$presetsSelect.appendChild($fragment);
// Update state of Activate button
- const activated = defaultPresetId === this.#STATE.currentPresetId;
- this.#$.activateButton!.disabled = activated;
- this.#$.activateButton!.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
+ const activated = defaultPresetId === this.STATE.currentPresetId;
+ this.$activateButton.disabled = activated;
+ this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
- !this.#STATE.isEditing && this.#switchPreset(this.#STATE.currentPresetId);
+ !this.STATE.isEditing && this.switchPreset(this.STATE.currentPresetId);
});
}
- #toggleEditing = (force?: boolean) => {
- this.#STATE.isEditing = typeof force !== 'undefined' ? force : !this.#STATE.isEditing;
- this.#$.wrapper!.classList.toggle('bx-editing', this.#STATE.isEditing);
+ private toggleEditing = (force?: boolean) => {
+ this.STATE.isEditing = typeof force !== 'undefined' ? force : !this.STATE.isEditing;
+ this.$wrapper.classList.toggle('bx-editing', this.STATE.isEditing);
- if (this.#STATE.isEditing) {
- this.#STATE.editingPresetData = deepClone(this.#getCurrentPreset().data);
+ if (this.STATE.isEditing) {
+ this.STATE.editingPresetData = deepClone(this.getCurrentPreset().data);
} else {
- this.#STATE.editingPresetData = null;
+ this.STATE.editingPresetData = null;
}
- const childElements = this.#$.wrapper!.querySelectorAll('select, button, input');
+ const childElements = this.$wrapper.querySelectorAll('select, button, input');
for (const $elm of Array.from(childElements)) {
if ($elm.parentElement!.parentElement!.classList.contains('bx-mkb-action-buttons')) {
continue;
}
- let disable = !this.#STATE.isEditing;
+ let disable = !this.STATE.isEditing;
if ($elm.parentElement!.classList.contains('bx-mkb-preset-tools')) {
disable = !disable;
@@ -316,14 +301,14 @@ export class MkbRemapper {
}
render() {
- this.#$.wrapper = CE('div', {'class': 'bx-mkb-settings'});
+ this.$wrapper = CE('div', {class: 'bx-mkb-settings'});
- this.#$.presetsSelect = CE('select', {tabindex: -1});
- this.#$.presetsSelect!.addEventListener('change', e => {
- this.#switchPreset(parseInt((e.target as HTMLSelectElement).value));
+ this.$presetsSelect = CE('select', {tabindex: -1});
+ this.$presetsSelect.addEventListener('change', e => {
+ this.switchPreset(parseInt((e.target as HTMLSelectElement).value));
});
- const promptNewName = (value?: string) => {
+ const promptNewName = (value: string) => {
let newName: string | null = '';
while (!newName) {
newName = prompt(t('prompt-preset-name'), value);
@@ -336,15 +321,15 @@ export class MkbRemapper {
return newName ? newName : false;
};
- const $header = CE('div', {'class': 'bx-mkb-preset-tools'},
- this.#$.presetsSelect,
+ const $header = CE('div', {class: 'bx-mkb-preset-tools'},
+ this.$presetsSelect,
// Rename button
createButton({
title: t('rename'),
icon: BxIcon.CURSOR_TEXT,
tabIndex: -1,
onClick: e => {
- const preset = this.#getCurrentPreset();
+ const preset = this.getCurrentPreset();
let newName = promptNewName(preset.name);
if (!newName || newName === preset.name) {
@@ -353,28 +338,28 @@ export class MkbRemapper {
// Update preset with new name
preset.name = newName;
- LocalDb.INSTANCE.updatePreset(preset).then(id => this.#refresh());
+ MkbPresetsDb.getInstance().updatePreset(preset).then(id => this.refresh());
},
}),
// New button
createButton({
- icon: BxIcon.NEW,
- title: t('new'),
- tabIndex: -1,
- onClick: e => {
- let newName = promptNewName('');
- if (!newName) {
- return;
- }
+ icon: BxIcon.NEW,
+ title: t('new'),
+ tabIndex: -1,
+ onClick: e => {
+ let newName = promptNewName('');
+ if (!newName) {
+ return;
+ }
- // Create new preset selected name
- LocalDb.INSTANCE.newPreset(newName, MkbPreset.DEFAULT_PRESET).then(id => {
- this.#STATE.currentPresetId = id;
- this.#refresh();
- });
- },
- }),
+ // Create new preset selected name
+ MkbPresetsDb.getInstance().newPreset(newName, MkbPreset.DEFAULT_PRESET).then(id => {
+ this.STATE.currentPresetId = id;
+ this.refresh();
+ });
+ },
+ }),
// Copy button
createButton({
@@ -382,7 +367,7 @@ export class MkbRemapper {
title: t('copy'),
tabIndex: -1,
onClick: e => {
- const preset = this.#getCurrentPreset();
+ const preset = this.getCurrentPreset();
let newName = promptNewName(`${preset.name} (2)`);
if (!newName) {
@@ -390,9 +375,9 @@ export class MkbRemapper {
}
// Create new preset selected name
- LocalDb.INSTANCE.newPreset(newName, preset.data).then(id => {
- this.#STATE.currentPresetId = id;
- this.#refresh();
+ MkbPresetsDb.getInstance().newPreset(newName, preset.data).then(id => {
+ this.STATE.currentPresetId = id;
+ this.refresh();
});
},
}),
@@ -408,23 +393,23 @@ export class MkbRemapper {
return;
}
- LocalDb.INSTANCE.deletePreset(this.#STATE.currentPresetId).then(id => {
- this.#STATE.currentPresetId = 0;
- this.#refresh();
+ MkbPresetsDb.getInstance().deletePreset(this.STATE.currentPresetId).then(id => {
+ this.STATE.currentPresetId = 0;
+ this.refresh();
});
},
}),
);
- this.#$.wrapper!.appendChild($header);
+ this.$wrapper.appendChild($header);
- const $rows = CE('div', {'class': 'bx-mkb-settings-rows'},
- CE('i', {'class': 'bx-mkb-note'}, t('right-click-to-unbind')),
+ const $rows = CE('div', {class: 'bx-mkb-settings-rows'},
+ CE('i', {class: 'bx-mkb-note'}, t('right-click-to-unbind')),
);
// Render keys
const keysPerButton = 2;
- for (const buttonIndex of this.#BUTTON_ORDERS) {
+ for (const buttonIndex of this.BUTTON_ORDERS) {
const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex];
let $elm;
@@ -437,22 +422,22 @@ export class MkbRemapper {
'data-key-slot': i,
}, ' ');
- $elm.addEventListener('mouseup', this.#onBindingKey);
- $elm.addEventListener('contextmenu', this.#onContextMenu);
+ $elm.addEventListener('mouseup', this.onBindingKey);
+ $elm.addEventListener('contextmenu', this.onContextMenu);
$fragment.appendChild($elm);
- this.#$.allKeyElements.push($elm);
+ this.allKeyElements.push($elm);
}
- const $keyRow = CE('div', {'class': 'bx-mkb-key-row'},
- CE('label', {'title': buttonName}, buttonPrompt),
+ const $keyRow = CE('div', {class: 'bx-mkb-key-row'},
+ CE('label', {title: buttonName}, buttonPrompt),
$fragment,
);
$rows.appendChild($keyRow);
}
- $rows.appendChild(CE('i', {'class': 'bx-mkb-note'}, t('mkb-adjust-ingame-settings')),);
+ $rows.appendChild(CE('i', {class: 'bx-mkb-note'}, t('mkb-adjust-ingame-settings')),);
// Render mouse settings
const $mouseSettings = document.createDocumentFragment();
@@ -463,7 +448,7 @@ export class MkbRemapper {
let $elm;
const onChange = (e: Event, value: any) => {
- (this.#STATE.editingPresetData!.mouse as any)[key] = value;
+ (this.STATE.editingPresetData!.mouse as any)[key] = value;
};
const $row = CE('label', {
class: 'bx-settings-row',
@@ -474,32 +459,32 @@ export class MkbRemapper {
);
$mouseSettings.appendChild($row);
- this.#$.allMouseElements[key as MkbPresetKey] = $elm;
+ this.allMouseElements[key as MkbPresetKey] = $elm;
}
$rows.appendChild($mouseSettings);
- this.#$.wrapper!.appendChild($rows);
+ this.$wrapper.appendChild($rows);
// Render action buttons
- const $actionButtons = CE('div', {'class': 'bx-mkb-action-buttons'},
+ const $actionButtons = CE('div', {class: 'bx-mkb-action-buttons'},
CE('div', {},
// Edit button
createButton({
label: t('edit'),
tabIndex: -1,
- onClick: e => this.#toggleEditing(true),
+ onClick: e => this.toggleEditing(true),
}),
// Activate button
- this.#$.activateButton = createButton({
+ this.$activateButton = createButton({
label: t('activate'),
style: ButtonStyle.PRIMARY,
tabIndex: -1,
onClick: e => {
- setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.#STATE.currentPresetId);
+ setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.STATE.currentPresetId);
EmulatedMkbHandler.getInstance().refreshPresetData();
- this.#refresh();
+ this.refresh();
},
}),
),
@@ -512,8 +497,8 @@ export class MkbRemapper {
tabIndex: -1,
onClick: e => {
// Restore preset
- this.#switchPreset(this.#STATE.currentPresetId);
- this.#toggleEditing(false);
+ this.switchPreset(this.STATE.currentPresetId);
+ this.toggleEditing(false);
},
}),
@@ -523,27 +508,27 @@ export class MkbRemapper {
style: ButtonStyle.PRIMARY,
tabIndex: -1,
onClick: e => {
- const updatedPreset = deepClone(this.#getCurrentPreset());
- updatedPreset.data = this.#STATE.editingPresetData as MkbPresetData;
+ const updatedPreset = deepClone(this.getCurrentPreset());
+ updatedPreset.data = this.STATE.editingPresetData as MkbPresetData;
- LocalDb.INSTANCE.updatePreset(updatedPreset).then(id => {
+ MkbPresetsDb.getInstance().updatePreset(updatedPreset).then(id => {
// If this is the default preset => refresh preset data
if (id === getPref(PrefKey.MKB_DEFAULT_PRESET_ID)) {
EmulatedMkbHandler.getInstance().refreshPresetData();
}
- this.#toggleEditing(false);
- this.#refresh();
+ this.toggleEditing(false);
+ this.refresh();
});
},
}),
),
);
- this.#$.wrapper!.appendChild($actionButtons);
+ this.$wrapper.appendChild($actionButtons);
- this.#toggleEditing(false);
- this.#refresh();
- return this.#$.wrapper;
+ this.toggleEditing(false);
+ this.refresh();
+ return this.$wrapper;
}
}
diff --git a/src/modules/mkb/native-mkb-handler.ts b/src/modules/mkb/native-mkb-handler.ts
index f7cefc1..2a2d6c4 100644
--- a/src/modules/mkb/native-mkb-handler.ts
+++ b/src/modules/mkb/native-mkb-handler.ts
@@ -7,6 +7,7 @@ import { BxEvent } from "@/utils/bx-event";
import { ButtonStyle, CE, createButton } from "@/utils/html";
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
+import { BxLogger } from "@/utils/bx-logger";
type NativeMouseData = {
X: number,
@@ -24,6 +25,7 @@ type XcloudInputSink = {
export class NativeMkbHandler extends MkbHandler {
private static instance: NativeMkbHandler;
public static getInstance = () => NativeMkbHandler.instance ?? (NativeMkbHandler.instance = new NativeMkbHandler());
+ private readonly LOG_TAG = 'NativeMkbHandler';
#pointerClient: PointerClient | undefined;
#enabled: boolean = false;
@@ -39,6 +41,11 @@ export class NativeMkbHandler extends MkbHandler {
#$message?: HTMLElement;
+ private constructor() {
+ super();
+ BxLogger.info(this.LOG_TAG, 'constructor()');
+ }
+
#onKeyboardEvent(e: KeyboardEvent) {
if (e.type === 'keyup' && e.code === 'F8') {
e.preventDefault();
diff --git a/src/modules/mkb/pointer-client.ts b/src/modules/mkb/pointer-client.ts
index 495eb19..30b0202 100644
--- a/src/modules/mkb/pointer-client.ts
+++ b/src/modules/mkb/pointer-client.ts
@@ -2,8 +2,6 @@ import { BxLogger } from "@/utils/bx-logger";
import { Toast } from "@/utils/toast";
import type { MkbHandler } from "./base-mkb-handler";
-const LOG_TAG = 'PointerClient';
-
enum PointerAction {
MOVE = 1,
BUTTON_PRESS = 2,
@@ -16,10 +14,15 @@ enum PointerAction {
export class PointerClient {
private static instance: PointerClient;
public static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient());
+ private readonly LOG_TAG = 'PointerClient';
private socket: WebSocket | undefined | null;
private mkbHandler: MkbHandler | undefined;
+ private constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
+ }
+
start(port: number, mkbHandler: MkbHandler) {
if (!port) {
throw new Error('PointerServer port is 0');
@@ -33,12 +36,12 @@ export class PointerClient {
// Connection opened
this.socket.addEventListener('open', (event) => {
- BxLogger.info(LOG_TAG, 'connected')
+ BxLogger.info(this.LOG_TAG, 'connected')
});
// Error
this.socket.addEventListener('error', (event) => {
- BxLogger.error(LOG_TAG, event);
+ BxLogger.error(this.LOG_TAG, event);
Toast.show('Cannot setup mouse: ' + event);
});
diff --git a/src/modules/patcher.ts b/src/modules/patcher.ts
index 0917417..4558c76 100644
--- a/src/modules/patcher.ts
+++ b/src/modules/patcher.ts
@@ -1212,7 +1212,7 @@ export class PatcherCache {
*/
static #getSignature(): number {
const scriptVersion = SCRIPT_VERSION;
- const webVersion = (document.querySelector('meta[name=gamepass-app-version]') as HTMLMetaElement)?.content;
+ const webVersion = (document.querySelector('meta[name=gamepass-app-version]'))?.content;
const patches = JSON.stringify(ALL_PATCHES);
// Calculate signature
diff --git a/src/modules/remote-play-manager.ts b/src/modules/remote-play-manager.ts
index 4fb1e22..163f2b3 100644
--- a/src/modules/remote-play-manager.ts
+++ b/src/modules/remote-play-manager.ts
@@ -9,8 +9,6 @@ import { PrefKey } from "@/enums/pref-keys";
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
import { RemotePlayNavigationDialog } from "./ui/dialog/remote-play-dialog";
-const LOG_TAG = 'RemotePlay';
-
export const enum RemotePlayConsoleState {
ON = 'On',
OFF = 'Off',
@@ -38,6 +36,7 @@ type RemotePlayConsole = {
export class RemotePlayManager {
private static instance: RemotePlayManager;
public static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager());
+ private readonly LOG_TAG = 'RemotePlayManager';
private isInitialized = false;
@@ -47,6 +46,10 @@ export class RemotePlayManager {
private consoles!: Array;
private regions: Array = [];
+ private constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
+ }
+
initialize() {
if (this.isInitialized) {
return;
@@ -56,9 +59,9 @@ export class RemotePlayManager {
this.getXhomeToken(() => {
this.getConsolesList(() => {
- BxLogger.info(LOG_TAG, 'Consoles', this.consoles);
+ BxLogger.info(this.LOG_TAG, 'Consoles', this.consoles);
- STATES.supportedRegion && HeaderSection.showRemotePlayButton();
+ STATES.supportedRegion && HeaderSection.getInstance().showRemotePlayButton();
BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY);
});
});
diff --git a/src/modules/shortcuts/shortcut-sound.ts b/src/modules/shortcuts/shortcut-sound.ts
index 2b94888..1a908c9 100644
--- a/src/modules/shortcuts/shortcut-sound.ts
+++ b/src/modules/shortcuts/shortcut-sound.ts
@@ -77,13 +77,7 @@ export class SoundShortcut {
return;
}
- let $media: HTMLMediaElement;
-
- $media = document.querySelector('div[data-testid=media-container] audio') as HTMLAudioElement;
- if (!$media) {
- $media = document.querySelector('div[data-testid=media-container] video') as HTMLAudioElement;
- }
-
+ const $media = document.querySelector('div[data-testid=media-container] audio') ?? document.querySelector('div[data-testid=media-container] video');
if ($media) {
$media.muted = !$media.muted;
diff --git a/src/modules/stream-player.ts b/src/modules/stream-player.ts
index 0aed8e7..8369c77 100644
--- a/src/modules/stream-player.ts
+++ b/src/modules/stream-player.ts
@@ -2,7 +2,7 @@ import { isFullVersion } from "@macros/build" with {type: "macro"};
import { CE } from "@/utils/html";
import { WebGL2Player } from "./player/webgl2-player";
-import { Screenshot } from "@/utils/screenshot";
+import { ScreenshotManager } from "@/utils/screenshot-manager";
import { StreamPlayerType, StreamVideoProcessing } from "@enums/stream-player";
import { STATES } from "@/utils/global";
import { PrefKey } from "@/enums/pref-keys";
@@ -237,7 +237,7 @@ export class StreamPlayer {
webGL2Player.setFilter(2);
}
- isFullVersion() && Screenshot.updateCanvasFilters('none');
+ isFullVersion() && ScreenshotManager.getInstance().updateCanvasFilters('none');
webGL2Player.setSharpness(options.sharpness || 0);
webGL2Player.setSaturation(options.saturation || 100);
@@ -252,7 +252,7 @@ export class StreamPlayer {
// Apply video filters to screenshots
if (isFullVersion() && getPref(PrefKey.SCREENSHOT_APPLY_FILTERS)) {
- Screenshot.updateCanvasFilters(filters);
+ ScreenshotManager.getInstance().updateCanvasFilters(filters);
}
let css = '';
diff --git a/src/modules/stream/stream-badges.ts b/src/modules/stream/stream-badges.ts
index 5924eca..ac341c5 100644
--- a/src/modules/stream/stream-badges.ts
+++ b/src/modules/stream/stream-badges.ts
@@ -50,6 +50,7 @@ enum StreamBadge {
export class StreamBadges {
private static instance: StreamBadges;
public static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges());
+ private readonly LOG_TAG = 'StreamBadges';
private serverInfo: StreamServerInfo = {};
@@ -96,6 +97,10 @@ export class StreamBadges {
private intervalId?: number | null;
private readonly REFRESH_INTERVAL = 3 * 1000;
+ private constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
+ }
+
setRegion(region: string) {
this.serverInfo.server = {
region: region,
diff --git a/src/modules/stream/stream-settings-utils.ts b/src/modules/stream/stream-settings-utils.ts
index 31e5967..840ba97 100644
--- a/src/modules/stream/stream-settings-utils.ts
+++ b/src/modules/stream/stream-settings-utils.ts
@@ -18,7 +18,7 @@ export function onChangeVideoPlayerType() {
let isDisabled = false;
- const $optCas = $videoProcessing.querySelector(`option[value=${StreamVideoProcessing.CAS}]`) as HTMLOptionElement;
+ const $optCas = $videoProcessing.querySelector(`option[value=${StreamVideoProcessing.CAS}]`);
if (playerType === StreamPlayerType.WEBGL2) {
$optCas && ($optCas.disabled = false);
diff --git a/src/modules/stream/stream-stats.ts b/src/modules/stream/stream-stats.ts
index 2702f45..bf2bc67 100644
--- a/src/modules/stream/stream-stats.ts
+++ b/src/modules/stream/stream-stats.ts
@@ -5,11 +5,13 @@ import { STATES } from "@utils/global"
import { PrefKey } from "@/enums/pref-keys"
import { getPref } from "@/utils/settings-storages/global-settings-storage"
import { StreamStat, StreamStatsCollector, type StreamStatGrade } from "@/utils/stream-stats-collector"
+import { BxLogger } from "@/utils/bx-logger"
export class StreamStats {
private static instance: StreamStats;
public static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats());
+ private readonly LOG_TAG = 'StreamStats';
private intervalId?: number | null;
private readonly REFRESH_INTERVAL = 1 * 1000;
@@ -69,7 +71,8 @@ export class StreamStats {
quickGlanceObserver?: MutationObserver | null;
- constructor() {
+ private constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
this.render();
}
diff --git a/src/modules/stream/stream-ui.ts b/src/modules/stream/stream-ui.ts
index 2993b92..a8ab54a 100644
--- a/src/modules/stream/stream-ui.ts
+++ b/src/modules/stream/stream-ui.ts
@@ -39,7 +39,7 @@ export class StreamUiHandler {
return;
}
- const $streamHud = (e.target as HTMLElement).closest('#StreamHud') as HTMLElement;
+ const $streamHud = (e.target as HTMLElement).closest('#StreamHud');
if (!$streamHud) {
return;
}
@@ -58,13 +58,13 @@ export class StreamUiHandler {
$container.addEventListener('transitionend', onTransitionEnd);
}
- const $button = $container.querySelector('button') as HTMLElement;
+ const $button = $container.querySelector('button');
if (!$button) {
return null;
}
$button.setAttribute('title', label);
- const $orgSvg = $button.querySelector('svg') as SVGElement;
+ const $orgSvg = $button.querySelector('svg');
if (!$orgSvg) {
return null;
}
@@ -102,7 +102,7 @@ export class StreamUiHandler {
}
private static async handleStreamMenu() {
- const $btnCloseHud = document.querySelector('button[class*=StreamMenu-module__backButton]') as HTMLElement;
+ const $btnCloseHud = document.querySelector('button[class*=StreamMenu-module__backButton]');
if (!$btnCloseHud) {
return;
}
@@ -136,14 +136,14 @@ export class StreamUiHandler {
private static handleSystemMenu($streamHud: HTMLElement) {
// Get the last button
- const $orgButton = $streamHud.querySelector('div[class^=HUDButton]') as HTMLElement;
+ const $orgButton = $streamHud.querySelector('div[class^=HUDButton]');
if (!$orgButton) {
return;
}
const hideGripHandle = () => {
// Grip handle
- const $gripHandle = document.querySelector('#StreamHud button[class^=GripHandle]') as HTMLElement;
+ const $gripHandle = document.querySelector('#StreamHud button[class^=GripHandle]');
if ($gripHandle && $gripHandle.ariaExpanded === 'true') {
$gripHandle.dispatchEvent(new PointerEvent('pointerdown'));
$gripHandle.click();
diff --git a/src/modules/ui/dialog/navigation-dialog.ts b/src/modules/ui/dialog/navigation-dialog.ts
index 3e76b04..8de771a 100644
--- a/src/modules/ui/dialog/navigation-dialog.ts
+++ b/src/modules/ui/dialog/navigation-dialog.ts
@@ -2,6 +2,7 @@ import { GamepadKey } from "@/enums/mkb";
import { PrefKey } from "@/enums/pref-keys";
import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler";
import { BxEvent } from "@/utils/bx-event";
+import { BxLogger } from "@/utils/bx-logger";
import { STATES } from "@/utils/global";
import { CE, isElementVisible } from "@/utils/html";
import { setNearby } from "@/utils/navigation-utils";
@@ -89,6 +90,7 @@ export abstract class NavigationDialog {
export class NavigationDialogManager {
private static instance: NavigationDialogManager;
public static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager());
+ private readonly LOG_TAG = 'NavigationDialogManager';
private static readonly GAMEPAD_POLLING_INTERVAL = 50;
private static readonly GAMEPAD_KEYS = [
@@ -136,7 +138,9 @@ export class NavigationDialogManager {
private $container: HTMLElement;
private dialog: NavigationDialog | null = null;
- constructor() {
+ private constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
+
this.$overlay = CE('div', {class: 'bx-navigation-dialog-overlay bx-gone'});
this.$overlay.addEventListener('click', e => {
e.preventDefault();
@@ -185,17 +189,17 @@ export class NavigationDialogManager {
const rect = $select.getBoundingClientRect();
- let $label;
+ let $label: HTMLElement;
let width = Math.ceil(rect.width);
if (!width) {
return;
}
if (($select as HTMLSelectElement).multiple) {
- $label = $parent.querySelector('.bx-select-value') as HTMLElement;
+ $label = $parent.querySelector('.bx-select-value')!;
width += 20; // Add checkbox's width
} else {
- $label = $parent.querySelector('div') as HTMLElement;
+ $label = $parent.querySelector('div')!;
}
// Set min-width
diff --git a/src/modules/ui/dialog/remote-play-dialog.ts b/src/modules/ui/dialog/remote-play-dialog.ts
index cb0d6f7..8b1aeb1 100644
--- a/src/modules/ui/dialog/remote-play-dialog.ts
+++ b/src/modules/ui/dialog/remote-play-dialog.ts
@@ -7,11 +7,13 @@ import { t } from "@/utils/translation";
import { RemotePlayConsoleState, RemotePlayManager } from "@/modules/remote-play-manager";
import { BxSelectElement } from "@/web-components/bx-select";
import { BxEvent } from "@/utils/bx-event";
+import { BxLogger } from "@/utils/bx-logger";
export class RemotePlayNavigationDialog extends NavigationDialog {
private static instance: RemotePlayNavigationDialog;
public static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog());
+ private readonly LOG_TAG = 'RemotePlayNavigationDialog';
private readonly STATE_LABELS: Record = {
[RemotePlayConsoleState.ON]: t('powered-on'),
@@ -22,8 +24,9 @@ export class RemotePlayNavigationDialog extends NavigationDialog {
$container!: HTMLElement;
- constructor() {
+ private constructor() {
super();
+ BxLogger.info(this.LOG_TAG, 'constructor()');
this.setupDialog();
}
@@ -124,7 +127,7 @@ export class RemotePlayNavigationDialog extends NavigationDialog {
}
focusIfNeeded(): void {
- const $btnConnect = this.$container.querySelector('.bx-remote-play-device-wrapper button') as HTMLElement;
+ const $btnConnect = this.$container.querySelector('.bx-remote-play-device-wrapper button');
$btnConnect && $btnConnect.focus();
}
}
diff --git a/src/modules/ui/dialog/settings-dialog.ts b/src/modules/ui/dialog/settings-dialog.ts
index 7bed895..a75632f 100644
--- a/src/modules/ui/dialog/settings-dialog.ts
+++ b/src/modules/ui/dialog/settings-dialog.ts
@@ -27,12 +27,13 @@ import { ControllerDeviceVibration, getPref, getPrefDefinition, setPref, StreamT
import { SettingElement, type BxHtmlSettingElement } from "@/utils/setting-element";
import type { RecommendedSettings, SettingDefinition, SuggestedSettingCategory as SuggestedSettingProfile } from "@/types/setting-definition";
import { FullscreenText } from "../fullscreen-text";
+import { BxLogger } from "@/utils/bx-logger";
type SettingTabContentItem = Partial<{
pref: PrefKey;
label: string;
- note: string;
+ note: string | (() => HTMLElement);
experimental: string;
content: HTMLElement | (() => HTMLElement);
options: {[key: string]: string};
@@ -51,24 +52,29 @@ type SettingTabContent = {
unsupportedNote?: string | Text | null;
helpUrl?: string;
content?: any;
+ lazyContent?: boolean | (() => HTMLElement);
items?: Array void) | false>;
requiredVariants?: BuildVariant | Array;
};
type SettingTab = {
icon: SVGElement;
- group: 'global';
- items: Array;
+ group: SettingTabGroup,
+ items: Array | (() => Array);
requiredVariants?: BuildVariant | Array;
+ lazyContent?: boolean;
};
+type SettingTabGroup = 'global' | 'stream' | 'controller' | 'mkb' | 'native-mkb' | 'shortcuts' | 'stats';
+
export class SettingsNavigationDialog extends NavigationDialog {
private static instance: SettingsNavigationDialog;
public static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog());
+ private readonly LOG_TAG = 'SettingsNavigationDialog';
$container!: HTMLElement;
private $tabs!: HTMLElement;
- private $settings!: HTMLElement;
+ private $tabContents!: HTMLElement;
private $btnReload!: HTMLElement;
private $btnGlobalReload!: HTMLButtonElement;
@@ -326,8 +332,8 @@ export class SettingsNavigationDialog extends NavigationDialog {
// xCloud version
($parent) => {
try {
- const appVersion = (document.querySelector('meta[name=gamepass-app-version]') as HTMLMetaElement).content;
- const appDate = new Date((document.querySelector('meta[name=gamepass-app-date]') as HTMLMetaElement).content).toISOString().substring(0, 10);
+ const appVersion = document.querySelector('meta[name=gamepass-app-version]')!.content;
+ const appDate = new Date(document.querySelector('meta[name=gamepass-app-date]')!.content).toISOString().substring(0, 10);
$parent.appendChild(CE('div', {
class: 'bx-settings-app-version',
}, `xCloud website version ${appVersion} (${appDate})`));
@@ -380,7 +386,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
disabled: !getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL),
},
onCreated: (setting: SettingTabContentItem, $elm: HTMLElement) => {
- const $range = $elm.querySelector('input[type=range') as HTMLInputElement;
+ const $range = $elm.querySelector('input[type=range')!;
window.addEventListener(BxEvent.SETTINGS_CHANGED, e => {
const { storageKey, settingKey, settingValue } = e as any;
if (storageKey !== StorageKey.GLOBAL || settingKey !== PrefKey.AUDIO_VOLUME) {
@@ -511,11 +517,11 @@ export class SettingsNavigationDialog extends NavigationDialog {
}],
}];
- private readonly TAB_VIRTUAL_CONTROLLER_ITEMS: Array = [{
+ private readonly TAB_VIRTUAL_CONTROLLER_ITEMS: (() => Array) = () => [{
group: 'mkb',
label: t('virtual-controller'),
helpUrl: 'https://better-xcloud.github.io/mouse-and-keyboard/',
- content: isFullVersion() && MkbRemapper.INSTANCE.render(),
+ content: MkbRemapper.getInstance().render(),
}];
private readonly TAB_NATIVE_MKB_ITEMS: Array = [{
@@ -535,7 +541,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
}] : [],
}];
- private readonly TAB_SHORTCUTS_ITEMS: Array = [{
+ private readonly TAB_SHORTCUTS_ITEMS: (() => Array) = () => [{
requiredVariants: 'full',
group: 'controller-shortcuts',
label: t('controller-shortcuts'),
@@ -576,56 +582,59 @@ export class SettingsNavigationDialog extends NavigationDialog {
],
}];
- private readonly SETTINGS_UI: Array = [
- {
- icon: BxIcon.HOME,
+ private readonly SETTINGS_UI: Record = {
+ global: {
group: 'global',
+ icon: BxIcon.HOME,
items: this.TAB_GLOBAL_ITEMS,
},
- {
- icon: BxIcon.DISPLAY,
+ stream: {
group: 'stream',
+ icon: BxIcon.DISPLAY,
items: this.TAB_DISPLAY_ITEMS,
},
- {
- icon: BxIcon.CONTROLLER,
+ controller: {
group: 'controller',
+ icon: BxIcon.CONTROLLER,
items: this.TAB_CONTROLLER_ITEMS,
requiredVariants: 'full',
},
- isFullVersion() && getPref(PrefKey.MKB_ENABLED) && {
- icon: BxIcon.VIRTUAL_CONTROLLER,
+ mkb: isFullVersion() && getPref(PrefKey.MKB_ENABLED) && {
group: 'mkb',
+ icon: BxIcon.VIRTUAL_CONTROLLER,
items: this.TAB_VIRTUAL_CONTROLLER_ITEMS,
+ lazyContent: true,
requiredVariants: 'full',
},
- isFullVersion() && AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' && {
- icon: BxIcon.NATIVE_MKB,
+ 'native-mkb': isFullVersion() && AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' && {
group: 'native-mkb',
+ icon: BxIcon.NATIVE_MKB,
items: this.TAB_NATIVE_MKB_ITEMS,
requiredVariants: 'full',
},
- {
- icon: BxIcon.COMMAND,
+ shortcuts: {
group: 'shortcuts',
+ icon: BxIcon.COMMAND,
items: this.TAB_SHORTCUTS_ITEMS,
+ lazyContent: true,
requiredVariants: 'full',
},
- {
- icon: BxIcon.STREAM_STATS,
+ stats: {
group: 'stats',
+ icon: BxIcon.STREAM_STATS,
items: this.TAB_STATS_ITEMS,
},
- ];
+ };
- constructor() {
+ private constructor() {
super();
+ BxLogger.info(this.LOG_TAG, 'constructor()');
this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn;
this.setupDialog();
@@ -653,7 +662,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
// Trigger event
- const $selectUserAgent = document.querySelector(`#bx_setting_${PrefKey.USER_AGENT_PROFILE}`) as HTMLSelectElement;
+ const $selectUserAgent = document.querySelector(`#bx_setting_${PrefKey.USER_AGENT_PROFILE}`);
if ($selectUserAgent) {
$selectUserAgent.disabled = true;
BxEvent.dispatch($selectUserAgent, 'input', {});
@@ -757,8 +766,11 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
// Get labels
- for (const settingTab of this.SETTINGS_UI) {
- if (!settingTab || !settingTab.items) {
+ let settingTabGroup: keyof typeof this.SETTINGS_UI;
+ for (settingTabGroup in this.SETTINGS_UI) {
+ const settingTab = this.SETTINGS_UI[settingTabGroup];
+
+ if (!settingTab || !settingTab.items || typeof settingTab.items === 'function') {
continue;
}
@@ -901,7 +913,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
let prefKey: PrefKey;
for (prefKey in settings) {
const suggestedValue = settings[prefKey];
- const $checkBox = $content.querySelector(`#bx_suggest_${prefKey}`) as HTMLInputElement;
+ const $checkBox = $content.querySelector(`#bx_suggest_${prefKey}`)!;
if (!$checkBox.checked || $checkBox.disabled) {
continue;
}
@@ -961,36 +973,57 @@ export class SettingsNavigationDialog extends NavigationDialog {
}, t('suggest-settings-link')),
);
- $btnSuggest?.insertAdjacentElement('afterend', $content);
+ $btnSuggest.insertAdjacentElement('afterend', $content);
+ }
+
+ private onTabClicked(e: Event) {
+ const $svg = (e.target as SVGElement).closest('svg')!;
+
+ // Render tab content lazily
+ if (!!$svg.dataset.lazy) {
+ // Remove attribute
+ delete $svg.dataset.lazy;
+ // Render data
+ const settingTab = this.SETTINGS_UI[$svg.dataset.group as SettingTabGroup];
+
+ const items = (settingTab.items as Function)();
+ const $tabContent = this.renderTabContent.call(this, settingTab, items);
+ this.$tabContents.appendChild($tabContent);
+ }
+
+ // Switch tab
+ let $child: HTMLElement;
+ const children = Array.from(this.$tabContents.children) as HTMLElement[];
+ for ($child of children) {
+ if ($child.dataset.tabGroup === $svg.dataset.group) {
+ // Show tab content
+ $child.classList.remove('bx-gone');
+
+ // Calculate size of controller-friendly select boxes
+ if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
+ this.dialogManager.calculateSelectBoxes($child as HTMLElement);
+ }
+ } else {
+ // Hide tab content
+ $child.classList.add('bx-gone');
+ }
+ }
+
+ // Highlight current tab button
+ for (const $child of Array.from(this.$tabs.children)) {
+ $child.classList.remove('bx-active');
+ }
+
+ $svg.classList.add('bx-active');
}
private renderTab(settingTab: SettingTab) {
const $svg = createSvgIcon(settingTab.icon as any);
$svg.dataset.group = settingTab.group;
$svg.tabIndex = 0;
+ settingTab.lazyContent && ($svg.dataset.lazy = settingTab.lazyContent.toString());
- $svg.addEventListener('click', e => {
- // Switch tab
- for (const $child of Array.from(this.$settings.children)) {
- if ($child.getAttribute('data-tab-group') === settingTab.group) {
- $child.classList.remove('bx-gone');
-
- // Calculate size of controller-friendly select boxes
- if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
- this.dialogManager.calculateSelectBoxes($child as HTMLElement);
- }
- } else {
- $child.classList.add('bx-gone');
- }
- }
-
- // Highlight current tab button
- for (const $child of Array.from(this.$tabs.children)) {
- $child.classList.remove('bx-active');
- }
-
- $svg.classList.add('bx-active');
- });
+ $svg.addEventListener('click', this.onTabClicked.bind(this));
return $svg;
}
@@ -1137,10 +1170,19 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
let label = prefDefinition?.label || setting.label;
- let note = prefDefinition?.note || setting.note;
- let unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote;
+ let note: string | undefined | (() => HTMLElement) | HTMLElement = prefDefinition?.note || setting.note;
+ let unsupportedNote: string | undefined | (() => HTMLElement) | HTMLElement = prefDefinition?.unsupportedNote || setting.unsupportedNote;
const experimental = prefDefinition?.experimental || setting.experimental;
+ // Render note lazily
+ if (typeof note === 'function') {
+ note = note();
+ }
+
+ if (typeof unsupportedNote === 'function') {
+ unsupportedNote = unsupportedNote();
+ }
+
if (settingTabContent.label && setting.pref) {
if (prefDefinition?.suggest) {
typeof prefDefinition.suggest.lowest !== 'undefined' && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest);
@@ -1195,9 +1237,101 @@ export class SettingsNavigationDialog extends NavigationDialog {
!prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control);
}
+ private renderTabContent(settingTab: SettingTab, items: Array): HTMLElement {
+ const $tabContent = CE('div', {
+ class: 'bx-gone',
+ 'data-tab-group': settingTab.group,
+ });
+
+ for (const settingTabContent of items) {
+ if (!settingTabContent) {
+ continue;
+ }
+
+ if (!this.isSupportedVariant(settingTabContent.requiredVariants)) {
+ continue;
+ }
+
+ // Don't render other settings in unsupported regions
+ if (!this.renderFullSettings && settingTab.group === 'global' && settingTabContent.group !== 'general' && settingTabContent.group !== 'footer') {
+ continue;
+ }
+
+ let label = settingTabContent.label;
+
+ // If label is "Better xCloud" => create a link to Releases page
+ if (label === t('better-xcloud')) {
+ label += ' ' + SCRIPT_VERSION;
+
+ if (SCRIPT_VARIANT === 'lite') {
+ label += ' (Lite)';
+ }
+
+ label = createButton({
+ label: label,
+ url: 'https://github.com/redphx/better-xcloud/releases',
+ style: ButtonStyle.NORMAL_CASE | ButtonStyle.FROSTED | ButtonStyle.FOCUSABLE,
+ });
+ }
+
+ if (label) {
+ const $title = CE('h2', {
+ _nearby: {
+ orientation: 'horizontal',
+ }
+ },
+ CE('span', {}, label),
+ settingTabContent.helpUrl && createButton({
+ icon: BxIcon.QUESTION,
+ style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
+ url: settingTabContent.helpUrl,
+ title: t('help'),
+ }),
+ );
+
+ $tabContent.appendChild($title);
+ }
+
+ // Add note
+ if (settingTabContent.unsupportedNote) {
+ const $note = CE('b', {class: 'bx-note-unsupported'}, settingTabContent.unsupportedNote);
+
+ $tabContent.appendChild($note);
+ }
+
+ // Don't render settings if this is an unsupported feature
+ if (settingTabContent.unsupported) {
+ continue;
+ }
+
+ // Add content DOM
+ if (settingTabContent.content) {
+ $tabContent.appendChild(settingTabContent.content);
+ continue;
+ }
+
+ // Render list of settings
+ settingTabContent.items = settingTabContent.items || [];
+ for (const setting of settingTabContent.items) {
+ if (setting === false) {
+ continue;
+ }
+
+ if (typeof setting === 'function') {
+ setting.apply(this, [$tabContent]);
+ continue;
+ }
+
+ this.renderSettingRow(settingTab, $tabContent, settingTabContent, setting);
+ }
+ }
+
+ return $tabContent;
+ }
+
private setupDialog() {
let $tabs: HTMLElement;
- let $settings: HTMLElement;
+ let $tabContents: HTMLElement;
const $container = CE('div', {
class: 'bx-settings-dialog',
@@ -1245,7 +1379,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
),
),
- $settings = CE('div', {
+ $tabContents = CE('div', {
class: 'bx-settings-tab-contents',
_nearby: {
orientation: 'vertical',
@@ -1264,7 +1398,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
this.$container = $container;
this.$tabs = $tabs;
- this.$settings = $settings;
+ this.$tabContents = $tabContents;
// Close dialog when not clicking on any child elements in the dialog
$container.addEventListener('click', e => {
@@ -1275,7 +1409,10 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
});
- for (const settingTab of this.SETTINGS_UI) {
+ let settingTabGroup: keyof typeof this.SETTINGS_UI
+ for (settingTabGroup in this.SETTINGS_UI) {
+ const settingTab = this.SETTINGS_UI[settingTabGroup];
+
if (!settingTab) {
continue;
}
@@ -1293,95 +1430,13 @@ export class SettingsNavigationDialog extends NavigationDialog {
const $svg = this.renderTab(settingTab);
$tabs.appendChild($svg);
- const $tabContent = CE('div', {
- class: 'bx-gone',
- 'data-tab-group': settingTab.group,
- });
-
- for (const settingTabContent of settingTab.items) {
- if (settingTabContent === false) {
- continue;
- }
-
- if (!this.isSupportedVariant(settingTabContent.requiredVariants)) {
- continue;
- }
-
- // Don't render other settings in unsupported regions
- if (!this.renderFullSettings && settingTab.group === 'global' && settingTabContent.group !== 'general' && settingTabContent.group !== 'footer') {
- continue;
- }
-
- let label = settingTabContent.label;
-
- // If label is "Better xCloud" => create a link to Releases page
- if (label === t('better-xcloud')) {
- label += ' ' + SCRIPT_VERSION;
-
- if (SCRIPT_VARIANT === 'lite') {
- label += ' (Lite)';
- }
-
- label = createButton({
- label: label,
- url: 'https://github.com/redphx/better-xcloud/releases',
- style: ButtonStyle.NORMAL_CASE | ButtonStyle.FROSTED | ButtonStyle.FOCUSABLE,
- });
- }
-
- if (label) {
- const $title = CE('h2', {
- _nearby: {
- orientation: 'horizontal',
- }
- },
- CE('span', {}, label),
- settingTabContent.helpUrl && createButton({
- icon: BxIcon.QUESTION,
- style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
- url: settingTabContent.helpUrl,
- title: t('help'),
- }),
- );
-
- $tabContent.appendChild($title);
- }
-
- // Add note
- if (settingTabContent.unsupportedNote) {
- const $note = CE('b', {class: 'bx-note-unsupported'}, settingTabContent.unsupportedNote);
-
- $tabContent.appendChild($note);
- }
-
- // Don't render settings if this is an unsupported feature
- if (settingTabContent.unsupported) {
- continue;
- }
-
- // Add content DOM
- if (settingTabContent.content) {
- $tabContent.appendChild(settingTabContent.content);
- continue;
- }
-
- // Render list of settings
- settingTabContent.items = settingTabContent.items || [];
- for (const setting of settingTabContent.items) {
- if (setting === false) {
- continue;
- }
-
- if (typeof setting === 'function') {
- setting.apply(this, [$tabContent]);
- continue;
- }
-
- this.renderSettingRow(settingTab, $tabContent, settingTabContent, setting);
- }
+ // Don't render lazy tab content
+ if (typeof settingTab.items === 'function') {
+ continue;
}
- $settings.appendChild($tabContent);
+ const $tabContent = this.renderTabContent.call(this, settingTab, settingTab.items);
+ $tabContents.appendChild($tabContent);
}
// Select first tab
@@ -1398,13 +1453,13 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
private focusActiveTab() {
- const $currentTab = this.$tabs!.querySelector('.bx-active') as HTMLElement;
+ const $currentTab = this.$tabs!.querySelector('.bx-active');
$currentTab && $currentTab.focus();
return true;
}
private focusVisibleSetting(type: 'first' | 'last' = 'first'): boolean {
- const controls = Array.from(this.$settings.querySelectorAll('div[data-tab-group]:not(.bx-gone) > *'));
+ const controls = Array.from(this.$tabContents.querySelectorAll('div[data-tab-group]:not(.bx-gone) > *'));
if (!controls.length) {
return false;
}
@@ -1450,7 +1505,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
private jumpToSettingGroup(direction: 'next' | 'previous'): boolean {
- const $tabContent = this.$settings.querySelector('div[data-tab-group]:not(.bx-gone)');
+ const $tabContent = this.$tabContents.querySelector('div[data-tab-group]:not(.bx-gone)');
if (!$tabContent) {
return false;
}
@@ -1461,7 +1516,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
$header = $tabContent.querySelector('h2');
} else {
// Find the parent element
- const $parent = $focusing.closest('[data-tab-group] > *') as HTMLElement;
+ const $parent = $focusing.closest('[data-tab-group] > *');
const siblingProperty = direction === 'next' ? 'nextSibling' : 'previousSibling';
let $tmp = $parent;
diff --git a/src/modules/ui/fullscreen-text.ts b/src/modules/ui/fullscreen-text.ts
index fa7bcc3..a1f1eb2 100644
--- a/src/modules/ui/fullscreen-text.ts
+++ b/src/modules/ui/fullscreen-text.ts
@@ -1,12 +1,15 @@
+import { BxLogger } from "@/utils/bx-logger";
import { CE } from "@/utils/html";
export class FullscreenText {
private static instance: FullscreenText;
public static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText());
+ private readonly LOG_TAG = 'FullscreenText';
$text: HTMLElement;
- constructor() {
+ private constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
this.$text = CE('div', {
class: 'bx-fullscreen-text bx-gone',
});
diff --git a/src/modules/ui/guide-menu.ts b/src/modules/ui/guide-menu.ts
index 9d337a7..0fbb8d2 100644
--- a/src/modules/ui/guide-menu.ts
+++ b/src/modules/ui/guide-menu.ts
@@ -13,101 +13,104 @@ export enum GuideMenuTab {
}
export class GuideMenu {
- static #BUTTONS = {
- scriptSettings: createButton({
- label: t('better-xcloud'),
- style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY,
- onClick: e => {
- // Wait until the Guide dialog is closed
- window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, e => {
- setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50);
- }, {once: true});
+ private static instance: GuideMenu;
+ public static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu());
- // Close all xCloud's dialogs
- GuideMenu.#closeGuideMenu();
- },
- }),
+ private $renderedButtons?: HTMLElement;
- closeApp: AppInterface && createButton({
- icon: BxIcon.POWER,
- label: t('close-app'),
- title: t('close-app'),
- style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.DANGER,
- onClick: e => {
- AppInterface.closeApp();
- },
-
- attributes: {
- 'data-state': 'normal',
- },
- }),
-
- reloadPage: createButton({
- icon: BxIcon.REFRESH,
- label: t('reload-page'),
- title: t('reload-page'),
- style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
- onClick: e => {
- if (STATES.isPlaying) {
- confirm(t('confirm-reload-stream')) && window.location.reload();
- } else {
- window.location.reload();
- }
-
- // Close all xCloud's dialogs
- GuideMenu.#closeGuideMenu();
- },
- }),
-
- backToHome: createButton({
- icon: BxIcon.HOME,
- label: t('back-to-home'),
- title: t('back-to-home'),
- style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
- onClick: e => {
- confirm(t('back-to-home-confirm')) && (window.location.href = window.location.href.substring(0, 31));
-
- // Close all xCloud's dialogs
- GuideMenu.#closeGuideMenu();
- },
- attributes: {
- 'data-state': 'playing',
- },
- }),
- }
-
- static #$renderedButtons: HTMLElement;
-
- static #closeGuideMenu() {
+ closeGuideMenu() {
if (window.BX_EXPOSED.dialogRoutes) {
window.BX_EXPOSED.dialogRoutes.closeAll();
return;
}
// Use alternative method for Lite version
- const $btnClose = document.querySelector('#gamepass-dialog-root button[class^=Header-module__closeButton]') as HTMLElement;
+ const $btnClose = document.querySelector('#gamepass-dialog-root button[class^=Header-module__closeButton]');
$btnClose && $btnClose.click();
}
- static #renderButtons() {
- if (GuideMenu.#$renderedButtons) {
- return GuideMenu.#$renderedButtons;
+ private renderButtons() {
+ if (this.$renderedButtons) {
+ return this.$renderedButtons;
}
+ const buttons = {
+ scriptSettings: createButton({
+ label: t('better-xcloud'),
+ style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY,
+ onClick: (() => {
+ // Wait until the Guide dialog is closed
+ window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, e => {
+ setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50);
+ }, {once: true});
+
+ // Close all xCloud's dialogs
+ this.closeGuideMenu();
+ }).bind(this),
+ }),
+
+ closeApp: AppInterface && createButton({
+ icon: BxIcon.POWER,
+ label: t('close-app'),
+ title: t('close-app'),
+ style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.DANGER,
+ onClick: e => {
+ AppInterface.closeApp();
+ },
+
+ attributes: {
+ 'data-state': 'normal',
+ },
+ }),
+
+ reloadPage: createButton({
+ icon: BxIcon.REFRESH,
+ label: t('reload-page'),
+ title: t('reload-page'),
+ style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
+ onClick: (() => {
+ // Close all xCloud's dialogs
+ this.closeGuideMenu();
+
+ if (STATES.isPlaying) {
+ confirm(t('confirm-reload-stream')) && window.location.reload();
+ } else {
+ window.location.reload();
+ }
+ }).bind(this),
+ }),
+
+ backToHome: createButton({
+ icon: BxIcon.HOME,
+ label: t('back-to-home'),
+ title: t('back-to-home'),
+ style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
+ onClick: (() => {
+ // Close all xCloud's dialogs
+ this.closeGuideMenu();
+
+ confirm(t('back-to-home-confirm')) && (window.location.href = window.location.href.substring(0, 31));
+ }).bind(this),
+ attributes: {
+ 'data-state': 'playing',
+ },
+ }),
+ };
+
+ const buttonsLayout = [
+ buttons.scriptSettings,
+ [
+ buttons.backToHome,
+ buttons.reloadPage,
+ buttons.closeApp,
+ ],
+ ];
+
const $div = CE('div', {
class: 'bx-guide-home-buttons',
});
- const buttons = [
- GuideMenu.#BUTTONS.scriptSettings,
- [
- GuideMenu.#BUTTONS.backToHome,
- GuideMenu.#BUTTONS.reloadPage,
- GuideMenu.#BUTTONS.closeApp,
- ],
- ];
-
- for (const $button of buttons) {
+ for (const $button of buttonsLayout) {
if (!$button) {
continue;
}
@@ -123,15 +126,15 @@ export class GuideMenu {
}
}
- GuideMenu.#$renderedButtons = $div;
+ this.$renderedButtons = $div;
return $div;
}
- static #injectHome($root: HTMLElement, isPlaying = false) {
+ injectHome($root: HTMLElement, isPlaying = false) {
if (isFullVersion()) {
const $achievementsProgress = $root.querySelector('button[class*=AchievementsButton-module__progressBarContainer]');
if ($achievementsProgress) {
- TrueAchievements.injectAchievementsProgress($achievementsProgress as HTMLElement);
+ TrueAchievements.getInstance().injectAchievementsProgress($achievementsProgress as HTMLElement);
}
}
@@ -142,7 +145,7 @@ export class GuideMenu {
$target = $root.querySelector('a[class*=QuitGameButton]');
// Hide xCloud's Home button
- const $btnXcloudHome = $root.querySelector('div[class^=HomeButtonWithDivider]') as HTMLElement;
+ const $btnXcloudHome = $root.querySelector('div[class^=HomeButtonWithDivider]');
$btnXcloudHome && ($btnXcloudHome.style.display = 'none');
} else {
// Last divider
@@ -156,29 +159,30 @@ export class GuideMenu {
return false;
}
- const $buttons = GuideMenu.#renderButtons();
+ const $buttons = this.renderButtons();
$buttons.dataset.isPlaying = isPlaying.toString();
$target.insertAdjacentElement('afterend', $buttons);
}
- static async #onShown(e: Event) {
+ async onShown(e: Event) {
const where = (e as any).where as GuideMenuTab;
if (where === GuideMenuTab.HOME) {
- const $root = document.querySelector('#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]') as HTMLElement;
- $root && GuideMenu.#injectHome($root, STATES.isPlaying);
+ const $root = document.querySelector('#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]');
+ $root && this.injectHome($root, STATES.isPlaying);
}
}
- static addEventListeners() {
- window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown);
+ addEventListeners() {
+ window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown.bind(this));
}
- static observe($addedElm: HTMLElement) {
+ observe($addedElm: HTMLElement) {
const className = $addedElm.className;
+ // TrueAchievements
if (isFullVersion() && className.includes('AchievementsButton-module__progressBarContainer')) {
- TrueAchievements.injectAchievementsProgress($addedElm);
+ TrueAchievements.getInstance().injectAchievementsProgress($addedElm);
return;
}
@@ -192,7 +196,7 @@ export class GuideMenu {
if (isFullVersion()) {
const $achievDetailPage = $addedElm.querySelector('div[class*=AchievementDetailPage]');
if ($achievDetailPage) {
- TrueAchievements.injectAchievementDetailPage($achievDetailPage as HTMLElement);
+ TrueAchievements.getInstance().injectAchievementDetailPage($achievDetailPage as HTMLElement);
return;
}
}
diff --git a/src/modules/ui/header.ts b/src/modules/ui/header.ts
index 97f3a67..3183742 100644
--- a/src/modules/ui/header.ts
+++ b/src/modules/ui/header.ts
@@ -7,36 +7,45 @@ import { t } from "@utils/translation";
import { SettingsNavigationDialog } from "./dialog/settings-dialog";
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
+import { BxLogger } from "@/utils/bx-logger";
export class HeaderSection {
- static #$remotePlayBtn = createButton({
- classes: ['bx-header-remote-play-button', 'bx-gone'],
- icon: BxIcon.REMOTE_PLAY,
- title: t('remote-play'),
- style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR,
- onClick: e => {
- RemotePlayManager.getInstance().togglePopup();
- },
- });
+ private static instance: HeaderSection;
+ public static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection());
+ private readonly LOG_TAG = 'HeaderSection';
- static #$settingsBtn = createButton({
- classes: ['bx-header-settings-button'],
- label: '???',
- style: ButtonStyle.FROSTED | ButtonStyle.DROP_SHADOW | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_HEIGHT,
- onClick: e => {
- SettingsNavigationDialog.getInstance().show();
- },
- });
+ private $btnRemotePlay: HTMLElement;
+ private $btnSettings: HTMLElement;
+ private $buttonsWrapper: HTMLElement;
- static #$buttonsWrapper = CE('div', {},
- getPref(PrefKey.REMOTE_PLAY_ENABLED) ? HeaderSection.#$remotePlayBtn : null,
- HeaderSection.#$settingsBtn,
- );
+ private observer?: MutationObserver;
+ private timeoutId?: number | null;
- static #observer: MutationObserver;
- static #timeout: number | null;
+ constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
- static #injectSettingsButton($parent?: HTMLElement) {
+ this.$btnRemotePlay = createButton({
+ classes: ['bx-header-remote-play-button', 'bx-gone'],
+ icon: BxIcon.REMOTE_PLAY,
+ title: t('remote-play'),
+ style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR,
+ onClick: e => RemotePlayManager.getInstance().togglePopup(),
+ });
+
+ this.$btnSettings = createButton({
+ classes: ['bx-header-settings-button'],
+ label: '???',
+ style: ButtonStyle.FROSTED | ButtonStyle.DROP_SHADOW | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_HEIGHT,
+ onClick: e => SettingsNavigationDialog.getInstance().show(),
+ });
+
+ this.$buttonsWrapper = CE('div', {},
+ getPref(PrefKey.REMOTE_PLAY_ENABLED) ? this.$btnRemotePlay : null,
+ this.$btnSettings,
+ );
+ }
+
+ private injectSettingsButton($parent?: HTMLElement) {
if (!$parent) {
return;
}
@@ -44,8 +53,8 @@ export class HeaderSection {
const PREF_LATEST_VERSION = getPref(PrefKey.LATEST_VERSION);
// Setup Settings button
- const $btnSettings = HeaderSection.#$settingsBtn;
- if (isElementVisible(HeaderSection.#$buttonsWrapper)) {
+ const $btnSettings = this.$btnSettings;
+ if (isElementVisible(this.$buttonsWrapper)) {
return;
}
@@ -57,38 +66,42 @@ export class HeaderSection {
}
// Add the Settings button to the web page
- $parent.appendChild(HeaderSection.#$buttonsWrapper);
+ $parent.appendChild(this.$buttonsWrapper);
}
- static checkHeader() {
+ private checkHeader() {
let $target = document.querySelector('#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]');
if (!$target) {
$target = document.querySelector('div[class^=UnsupportedMarketPage-module__buttons]');
}
- $target && HeaderSection.#injectSettingsButton($target as HTMLElement);
+ $target && this.injectSettingsButton($target as HTMLElement);
}
- static showRemotePlayButton() {
- HeaderSection.#$remotePlayBtn.classList.remove('bx-gone');
- }
-
- static watchHeader() {
+ private watchHeader() {
const $root = document.querySelector('#PageContent header') || document.querySelector('#root');
if (!$root) {
return;
}
- HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout);
- HeaderSection.#timeout = null;
+ this.timeoutId && clearTimeout(this.timeoutId);
+ this.timeoutId = null;
- HeaderSection.#observer && HeaderSection.#observer.disconnect();
- HeaderSection.#observer = new MutationObserver(mutationList => {
- HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout);
- HeaderSection.#timeout = window.setTimeout(HeaderSection.checkHeader, 2000);
+ this.observer && this.observer.disconnect();
+ this.observer = new MutationObserver(mutationList => {
+ this.timeoutId && clearTimeout(this.timeoutId);
+ this.timeoutId = window.setTimeout(this.checkHeader.bind(this), 2000);
});
- HeaderSection.#observer.observe($root, {subtree: true, childList: true});
+ this.observer.observe($root, {subtree: true, childList: true});
- HeaderSection.checkHeader();
+ this.checkHeader();
+ }
+
+ showRemotePlayButton() {
+ this.$btnRemotePlay.classList.remove('bx-gone');
+ }
+
+ static watchHeader() {
+ HeaderSection.getInstance().watchHeader();
}
}
diff --git a/src/types/preferences.d.ts b/src/types/preferences.d.ts
index 104c30b..c2d896d 100644
--- a/src/types/preferences.d.ts
+++ b/src/types/preferences.d.ts
@@ -4,8 +4,8 @@ export type PreferenceSetting = {
options?: {[index: string]: string};
multipleOptions?: {[index: string]: string};
unsupported?: boolean;
- unsupported_note?: string | HTMLElement;
- note?: string | HTMLElement;
+ unsupportedNote?: string | (() => HTMLElement);
+ note?: string | (() => HTMLElement);
type?: SettingElementType;
ready?: (setting: PreferenceSetting) => void;
migrate?: (this: Preferences, savedPrefs: any, value: any) => void;
diff --git a/src/types/setting-definition.d.ts b/src/types/setting-definition.d.ts
index eb4bb58..dae1e0d 100644
--- a/src/types/setting-definition.d.ts
+++ b/src/types/setting-definition.d.ts
@@ -18,10 +18,10 @@ export type SettingDefinition = {
default: any;
} & Partial<{
label: string;
- note: string | HTMLElement;
+ note: string | (() => HTMLElement);
experimental: boolean;
unsupported: boolean;
- unsupportedNote: string | HTMLElement;
+ unsupportedNote: string | (() => HTMLElement);
suggest: PartialRecord,
ready: (setting: SettingDefinition) => void;
type: SettingElementType,
diff --git a/src/utils/bx-logger.ts b/src/utils/bx-logger.ts
index c24f739..59f9cdd 100644
--- a/src/utils/bx-logger.ts
+++ b/src/utils/bx-logger.ts
@@ -1,3 +1,5 @@
+import { BX_FLAGS } from "./bx-flags";
+
const enum TextColor {
INFO = '#008746',
WARNING = '#c1a404',
@@ -10,7 +12,7 @@ export class BxLogger {
static error = (tag: string, ...args: any[]) => BxLogger.log(TextColor.ERROR, tag, ...args);
private static log(color: string, tag: string, ...args: any) {
- console.log(`%c[BxC]`, `color:${color};font-weight:bold;`, tag, '//', ...args);
+ BX_FLAGS.Debug && console.log(`%c[BxC]`, `color:${color};font-weight:bold;`, tag, '//', ...args);
}
}
diff --git a/src/utils/html.ts b/src/utils/html.ts
index 9840446..2d2800b 100644
--- a/src/utils/html.ts
+++ b/src/utils/html.ts
@@ -56,6 +56,8 @@ function createElement(elmName: string, props: CreateElementOptio
let $elm;
const hasNs = 'xmlns' in props;
+ // console.trace('createElement', elmName, props);
+
if (hasNs) {
$elm = document.createElementNS(props.xmlns, elmName);
delete props.xmlns;
@@ -111,11 +113,11 @@ const ButtonStyleIndices = Object.keys(ButtonStyleClass).map(i => parseInt(i));
export function createButton(options: BxButton): T {
let $btn;
if (options.url) {
- $btn = CE('a', {'class': 'bx-button'}) as HTMLAnchorElement;
+ $btn = CE('a', {'class': 'bx-button'});
$btn.href = options.url;
$btn.target = '_blank';
} else {
- $btn = CE('button', {'class': 'bx-button', type: 'button'}) as HTMLButtonElement;
+ $btn = CE('button', {'class': 'bx-button', type: 'button'});
}
const style = (options.style || 0) as number;
diff --git a/src/utils/local-db.ts b/src/utils/local-db.ts
deleted file mode 100644
index 65f48e0..0000000
--- a/src/utils/local-db.ts
+++ /dev/null
@@ -1,165 +0,0 @@
-import { MkbPreset } from "@modules/mkb/mkb-preset";
-import { t } from "@utils/translation";
-import type { MkbStoredPreset, MkbStoredPresets } from "@/types/mkb";
-import { PrefKey } from "@/enums/pref-keys";
-import { setPref } from "./settings-storages/global-settings-storage";
-
-export class LocalDb {
- static #instance: LocalDb;
- static get INSTANCE() {
- if (!LocalDb.#instance) {
- LocalDb.#instance = new LocalDb();
- }
-
- return LocalDb.#instance;
- }
-
- static readonly DB_NAME = 'BetterXcloud';
- static readonly DB_VERSION = 1;
- static readonly TABLE_PRESETS = 'mkb_presets';
-
- #DB: any;
-
- #open() {
- return new Promise((resolve, reject) => {
- if (this.#DB) {
- resolve();
- return;
- }
-
- const request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
- request.onupgradeneeded = (e: IDBVersionChangeEvent) => {
- const db = (e.target! as any).result;
-
- switch (e.oldVersion) {
- case 0: {
- const presets = db.createObjectStore(LocalDb.TABLE_PRESETS, {keyPath: 'id', autoIncrement: true});
- presets.createIndex('name_idx', 'name');
- break;
- }
- }
- };
-
- request.onerror = e => {
- console.log(e);
- alert((e.target as any).error.message);
- reject && reject();
- };
-
- request.onsuccess = e => {
- this.#DB = (e.target as any).result;
- resolve();
- };
- });
- }
-
- #table(name: string, type: string): Promise {
- const transaction = this.#DB.transaction(name, type || 'readonly');
- const table = transaction.objectStore(name);
-
- return new Promise(resolve => resolve(table));
- }
-
- // Convert IndexDB method to Promise
- #call(method: any) {
- const table = arguments[1];
- return new Promise(resolve => {
- const request = method.call(table, ...Array.from(arguments).slice(2));
- request.onsuccess = (e: Event) => {
- resolve([table, (e.target as any).result]);
- };
- });
- }
-
- #count(table: IDBObjectStore): Promise<[IDBObjectStore, number]> {
- // @ts-ignore
- return this.#call(table.count, ...arguments);
- }
-
- #add(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
- // @ts-ignore
- return this.#call(table.add, ...arguments);
- }
-
- #put(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
- // @ts-ignore
- return this.#call(table.put, ...arguments);
- }
-
- #delete(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
- // @ts-ignore
- return this.#call(table.delete, ...arguments);
- }
-
- #get(table: IDBObjectStore, id: number): Promise {
- // @ts-ignore
- return this.#call(table.get, ...arguments);
- }
-
- #getAll(table: IDBObjectStore): Promise<[IDBObjectStore, any]> {
- // @ts-ignore
- return this.#call(table.getAll, ...arguments);
- }
-
- newPreset(name: string, data: any) {
- return this.#open()
- .then(() => this.#table(LocalDb.TABLE_PRESETS, 'readwrite'))
- .then(table => this.#add(table, {name, data}))
- .then(([table, id]) => new Promise(resolve => resolve(id)));
- }
-
- updatePreset(preset: MkbStoredPreset) {
- return this.#open()
- .then(() => this.#table(LocalDb.TABLE_PRESETS, 'readwrite'))
- .then(table => this.#put(table, preset))
- .then(([table, id]) => new Promise(resolve => resolve(id)));
- }
-
- deletePreset(id: number) {
- return this.#open()
- .then(() => this.#table(LocalDb.TABLE_PRESETS, 'readwrite'))
- .then(table => this.#delete(table, id))
- .then(([table, id]) => new Promise(resolve => resolve(id)));
- }
-
- getPreset(id: number): Promise {
- return this.#open()
- .then(() => this.#table(LocalDb.TABLE_PRESETS, 'readwrite'))
- .then(table => this.#get(table, id))
- .then(([table, preset]) => new Promise(resolve => resolve(preset)));
- }
-
- getPresets(): Promise {
- return this.#open()
- .then(() => this.#table(LocalDb.TABLE_PRESETS, 'readwrite'))
- .then(table => this.#count(table))
- .then(([table, count]) => {
- if (count > 0) {
- return new Promise(resolve => {
- this.#getAll(table)
- .then(([table, items]) => {
- const presets: MkbStoredPresets = {};
- items.forEach((item: MkbStoredPreset) => (presets[item.id!] = item));
- resolve(presets);
- });
- });
- }
-
- // Create "Default" preset when the table is empty
- const preset: MkbStoredPreset = {
- name: t('default'),
- data: MkbPreset.DEFAULT_PRESET,
- }
-
- return new Promise(resolve => {
- this.#add(table, preset)
- .then(([table, id]) => {
- preset.id = id;
- setPref(PrefKey.MKB_DEFAULT_PRESET_ID, id);
-
- resolve({[id]: preset});
- });
- });
- });
- }
-}
diff --git a/src/utils/local-db/local-db.ts b/src/utils/local-db/local-db.ts
new file mode 100644
index 0000000..36a9567
--- /dev/null
+++ b/src/utils/local-db/local-db.ts
@@ -0,0 +1,79 @@
+export abstract class LocalDb {
+ static readonly DB_NAME = 'BetterXcloud';
+ static readonly DB_VERSION = 1;
+
+ private db: any;
+
+ protected open() {
+ return new Promise((resolve, reject) => {
+ if (this.db) {
+ resolve();
+ return;
+ }
+
+ const request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
+ request.onupgradeneeded = this.onUpgradeNeeded;
+
+ request.onerror = e => {
+ console.log(e);
+ alert((e.target as any).error.message);
+ reject && reject();
+ };
+
+ request.onsuccess = e => {
+ this.db = (e.target as any).result;
+ resolve();
+ };
+ });
+ }
+
+ protected abstract onUpgradeNeeded(e: IDBVersionChangeEvent): void;
+
+ protected table(name: string, type: string): Promise {
+ const transaction = this.db.transaction(name, type || 'readonly');
+ const table = transaction.objectStore(name);
+
+ return new Promise(resolve => resolve(table));
+ }
+
+ // Convert IndexDB method to Promise
+ protected call(method: any) {
+ const table = arguments[1];
+ return new Promise(resolve => {
+ const request = method.call(table, ...Array.from(arguments).slice(2));
+ request.onsuccess = (e: Event) => {
+ resolve([table, (e.target as any).result]);
+ };
+ });
+ }
+
+ protected count(table: IDBObjectStore): Promise<[IDBObjectStore, number]> {
+ // @ts-ignore
+ return this.call(table.count, ...arguments);
+ }
+
+ protected add(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
+ // @ts-ignore
+ return this.call(table.add, ...arguments);
+ }
+
+ protected put(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
+ // @ts-ignore
+ return this.call(table.put, ...arguments);
+ }
+
+ protected delete(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
+ // @ts-ignore
+ return this.call(table.delete, ...arguments);
+ }
+
+ protected get(table: IDBObjectStore, id: number): Promise {
+ // @ts-ignore
+ return this.call(table.get, ...arguments);
+ }
+
+ protected getAll(table: IDBObjectStore): Promise<[IDBObjectStore, any]> {
+ // @ts-ignore
+ return this.call(table.getAll, ...arguments);
+ }
+}
diff --git a/src/utils/local-db/mkb-presets-db.ts b/src/utils/local-db/mkb-presets-db.ts
new file mode 100644
index 0000000..2a303ec
--- /dev/null
+++ b/src/utils/local-db/mkb-presets-db.ts
@@ -0,0 +1,96 @@
+import { PrefKey } from "@/enums/pref-keys";
+import { MkbPreset } from "@/modules/mkb/mkb-preset";
+import type { MkbStoredPreset, MkbStoredPresets } from "@/types/mkb";
+import { setPref } from "../settings-storages/global-settings-storage";
+import { t } from "../translation";
+import { LocalDb } from "./local-db";
+import { BxLogger } from "../bx-logger";
+
+export class MkbPresetsDb extends LocalDb {
+ private static instance: MkbPresetsDb;
+ public static getInstance = () => MkbPresetsDb.instance ?? (MkbPresetsDb.instance = new MkbPresetsDb());
+ private readonly LOG_TAG = 'MkbPresetsDb';
+
+ private readonly TABLE_PRESETS = 'mkb_presets';
+
+ private constructor() {
+ super();
+ BxLogger.info(this.LOG_TAG, 'constructor()');
+ }
+
+ protected onUpgradeNeeded(e: IDBVersionChangeEvent): void {
+ const db = (e.target! as any).result;
+ switch (e.oldVersion) {
+ case 0: {
+ const presets = db.createObjectStore(this.TABLE_PRESETS, {
+ keyPath: 'id',
+ autoIncrement: true,
+ });
+ presets.createIndex('name_idx', 'name');
+ break;
+ }
+ }
+ }
+
+ private presetsTable() {
+ return this.open()
+ .then(() => this.table(this.TABLE_PRESETS, 'readwrite'))
+ }
+
+ newPreset(name: string, data: any) {
+ return this.presetsTable()
+ .then(table => this.add(table, {name, data}))
+ .then(([table, id]) => new Promise(resolve => resolve(id)));
+ }
+
+ updatePreset(preset: MkbStoredPreset) {
+ return this.presetsTable()
+ .then(table => this.put(table, preset))
+ .then(([table, id]) => new Promise(resolve => resolve(id)));
+ }
+
+ deletePreset(id: number) {
+ return this.presetsTable()
+ .then(table => this.delete(table, id))
+ .then(([table, id]) => new Promise(resolve => resolve(id)));
+ }
+
+ getPreset(id: number): Promise {
+ return this.presetsTable()
+ .then(table => this.get(table, id))
+ .then(([table, preset]) => new Promise(resolve => resolve(preset)));
+ }
+
+ getPresets(): Promise {
+ return this.presetsTable()
+ .then(table => this.count(table))
+ .then(([table, count]) => {
+ if (count > 0) {
+ return new Promise(resolve => {
+ this.getAll(table)
+ .then(([table, items]) => {
+ const presets: MkbStoredPresets = {};
+ items.forEach((item: MkbStoredPreset) => (presets[item.id!] = item));
+ resolve(presets);
+ });
+ });
+ }
+
+ // Create "Default" preset when the table is empty
+ const preset: MkbStoredPreset = {
+ name: t('default'),
+ data: MkbPreset.DEFAULT_PRESET,
+ }
+
+ return new Promise(resolve => {
+ this.add(table, preset)
+ .then(([table, id]) => {
+ preset.id = id;
+ setPref(PrefKey.MKB_DEFAULT_PRESET_ID, id);
+
+ resolve({[id]: preset});
+ });
+ });
+ });
+ }
+}
diff --git a/src/utils/network.ts b/src/utils/network.ts
index 9c5a420..c9f01cf 100644
--- a/src/utils/network.ts
+++ b/src/utils/network.ts
@@ -1,6 +1,5 @@
import { isFullVersion } from "@macros/build" with {type: "macro"};
-import { BxEvent } from "@utils/bx-event";
import { BX_FLAGS, NATIVE_FETCH } from "@utils/bx-flags";
import { TouchController } from "@modules/touch-controller";
import { STATES } from "@utils/global";
@@ -29,9 +28,7 @@ function clearDbLogs(dbName: string, table: string) {
const objectStore = db.transaction(table, 'readwrite').objectStore(table);
const objectStoreRequest = objectStore.clear();
- objectStoreRequest.onsuccess = function() {
- console.log(`[Better xCloud] Cleared ${dbName}.${table}`);
- };
+ objectStoreRequest.onsuccess = () => BxLogger.info('clearDbLogs', `Cleared ${dbName}.${table}`);
} catch (ex) {}
}
}
@@ -134,6 +131,7 @@ export function interceptHttpRequests() {
'https://browser.events.data.microsoft.com',
'https://dc.services.visualstudio.com',
'https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io',
+ 'https://mscom.demdex.net',
]);
}
@@ -172,29 +170,42 @@ export function interceptHttpRequests() {
};
let gamepassAllGames: string[] = [];
+ const IGNORED_DOMAINS = [
+ 'accounts.xboxlive.com',
+ 'chat.xboxlive.com',
+ 'notificationinbox.xboxlive.com',
+ 'peoplehub.xboxlive.com',
+ 'rta.xboxlive.com',
+ 'userpresence.xboxlive.com',
+ 'xblmessaging.xboxlive.com',
+ 'consent.config.office.com',
+
+ 'arc.msn.com',
+ 'browser.events.data.microsoft.com',
+ 'dc.services.visualstudio.com',
+ '2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io',
+ ];
(window as any).BX_FETCH = window.fetch = async (request: RequestInfo | URL, init?: RequestInit): Promise => {
let url = (typeof request === 'string') ? request : (request as Request).url;
// Check blocked URLs
for (let blocked of BLOCKED_URLS) {
- if (!url.startsWith(blocked)) {
- continue;
+ if (url.startsWith(blocked)) {
+ return new Response('{"acc":1,"webResult":{}}', {
+ status: 200,
+ statusText: '200 OK',
+ });
}
-
- return new Response('{"acc":1,"webResult":{}}', {
- status: 200,
- statusText: '200 OK',
- });
}
- if (url.endsWith('/play')) {
- BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
+ // Ignore URLs
+ const domain = (new URL(url)).hostname;
+ if (IGNORED_DOMAINS.includes(domain)) {
+ return NATIVE_FETCH(request, init);
}
- if (url.endsWith('/configuration')) {
- BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
- }
+ // BxLogger.info('fetch', url);
// Override experimentals
if (url.startsWith('https://emerald.xboxservices.com/xboxcomfd/experimentation')) {
@@ -212,6 +223,7 @@ export function interceptHttpRequests() {
return response;
} catch (e) {
console.log(e);
+ return NATIVE_FETCH(request, init);
}
}
diff --git a/src/utils/root-dialog-observer.ts b/src/utils/root-dialog-observer.ts
index 2772649..f66c0c8 100644
--- a/src/utils/root-dialog-observer.ts
+++ b/src/utils/root-dialog-observer.ts
@@ -60,7 +60,7 @@ export class RootDialogObserver {
}
} else if ($root.querySelector('div[class*=GuideDialog]')) {
// Guide menu
- GuideMenu.observe($addedElm);
+ GuideMenu.getInstance().observe($addedElm);
return true;
}
diff --git a/src/utils/screenshot-manager.ts b/src/utils/screenshot-manager.ts
new file mode 100644
index 0000000..699112e
--- /dev/null
+++ b/src/utils/screenshot-manager.ts
@@ -0,0 +1,105 @@
+import { StreamPlayerType } from "@enums/stream-player";
+import { AppInterface, STATES } from "./global";
+import { CE } from "./html";
+import { PrefKey } from "@/enums/pref-keys";
+import { getPref } from "./settings-storages/global-settings-storage";
+import { BxLogger } from "./bx-logger";
+
+
+export class ScreenshotManager {
+ private static instance: ScreenshotManager;
+ public static getInstance = () => ScreenshotManager.instance ?? (ScreenshotManager.instance = new ScreenshotManager());
+ private readonly LOG_TAG = 'ScreenshotManager';
+
+ private $download: HTMLAnchorElement;
+ private $canvas: HTMLCanvasElement;
+ private canvasContext: CanvasRenderingContext2D;
+
+ private constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
+
+ this.$download = CE('a');
+
+ this.$canvas = CE('canvas', {'class': 'bx-gone'});
+ this.canvasContext = this.$canvas.getContext('2d', {
+ alpha: false,
+ willReadFrequently: false,
+ })!;
+ }
+
+ updateCanvasSize(width: number, height: number) {
+ this.$canvas.width = width;
+ this.$canvas.height = height;
+ }
+
+ updateCanvasFilters(filters: string) {
+ this.canvasContext.filter = filters;
+ }
+
+ private onAnimationEnd(e: Event) {
+ (e.target as HTMLElement).classList.remove('bx-taking-screenshot');
+ }
+
+ takeScreenshot(callback?: any) {
+ const currentStream = STATES.currentStream;
+ const streamPlayer = currentStream.streamPlayer;
+ const $canvas = this.$canvas;
+ if (!streamPlayer || !$canvas) {
+ return;
+ }
+
+ let $player;
+ if (getPref(PrefKey.SCREENSHOT_APPLY_FILTERS)) {
+ $player = streamPlayer.getPlayerElement();
+ } else {
+ $player = streamPlayer.getPlayerElement(StreamPlayerType.VIDEO);
+ }
+
+ if (!$player || !$player.isConnected) {
+ return;
+ }
+
+ $player.parentElement!.addEventListener('animationend', this.onAnimationEnd, { once: true });
+ $player.parentElement!.classList.add('bx-taking-screenshot');
+
+ const canvasContext = this.canvasContext;
+
+ if ($player instanceof HTMLCanvasElement) {
+ streamPlayer.getWebGL2Player().drawFrame(true);
+ }
+ canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height);
+
+ // Get data URL and pass to parent app
+ if (AppInterface) {
+ const data = $canvas.toDataURL('image/png').split(';base64,')[1];
+ AppInterface.saveScreenshot(currentStream.titleSlug, data);
+
+ // Free screenshot from memory
+ canvasContext.clearRect(0, 0, $canvas.width, $canvas.height);
+
+ callback && callback();
+ return;
+ }
+
+ $canvas.toBlob(blob => {
+ if (!blob) {
+ return;
+ }
+
+ // Download screenshot
+ const now = +new Date;
+ const $download = this.$download;
+ $download.download = `${currentStream.titleSlug}-${now}.png`;
+ $download.href = URL.createObjectURL(blob);
+ $download.click();
+
+ // Free screenshot from memory
+ URL.revokeObjectURL($download.href);
+ $download.href = '';
+ $download.download = '';
+ canvasContext.clearRect(0, 0, $canvas.width, $canvas.height);
+
+ callback && callback();
+ }, 'image/png');
+ }
+}
diff --git a/src/utils/screenshot.ts b/src/utils/screenshot.ts
deleted file mode 100644
index d9f59cd..0000000
--- a/src/utils/screenshot.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import { StreamPlayerType } from "@enums/stream-player";
-import { AppInterface, STATES } from "./global";
-import { CE } from "./html";
-import { PrefKey } from "@/enums/pref-keys";
-import { getPref } from "./settings-storages/global-settings-storage";
-
-
-export class Screenshot {
- static #$canvas: HTMLCanvasElement;
- static #canvasContext: CanvasRenderingContext2D;
-
- static setup() {
- if (Screenshot.#$canvas) {
- return;
- }
-
- Screenshot.#$canvas = CE('canvas', {'class': 'bx-gone'});
-
- Screenshot.#canvasContext = Screenshot.#$canvas.getContext('2d', {
- alpha: false,
- willReadFrequently: false,
- })!;
- }
-
- static updateCanvasSize(width: number, height: number) {
- const $canvas = Screenshot.#$canvas;
- if ($canvas) {
- $canvas.width = width;
- $canvas.height = height;
- }
- }
-
- static updateCanvasFilters(filters: string) {
- Screenshot.#canvasContext && (Screenshot.#canvasContext.filter = filters);
- }
-
- static #onAnimationEnd(e: Event) {
- const $target = e.target as HTMLElement;
- $target.classList.remove('bx-taking-screenshot');
- }
-
- static takeScreenshot(callback?: any) {
- const currentStream = STATES.currentStream;
- const streamPlayer = currentStream.streamPlayer;
- const $canvas = Screenshot.#$canvas;
- if (!streamPlayer || !$canvas) {
- return;
- }
-
- let $player;
- if (getPref(PrefKey.SCREENSHOT_APPLY_FILTERS)) {
- $player = streamPlayer.getPlayerElement();
- } else {
- $player = streamPlayer.getPlayerElement(StreamPlayerType.VIDEO);
- }
-
- if (!$player || !$player.isConnected) {
- return;
- }
-
- $player.parentElement!.addEventListener('animationend', this.#onAnimationEnd, { once: true });
- $player.parentElement!.classList.add('bx-taking-screenshot');
-
- const canvasContext = Screenshot.#canvasContext;
-
- if ($player instanceof HTMLCanvasElement) {
- streamPlayer.getWebGL2Player().drawFrame(true);
- }
- canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height);
-
- // Get data URL and pass to parent app
- if (AppInterface) {
- const data = $canvas.toDataURL('image/png').split(';base64,')[1];
- AppInterface.saveScreenshot(currentStream.titleSlug, data);
-
- // Free screenshot from memory
- canvasContext.clearRect(0, 0, $canvas.width, $canvas.height);
-
- callback && callback();
- return;
- }
-
- $canvas && $canvas.toBlob(blob => {
- // Download screenshot
- const now = +new Date;
- const $anchor = CE('a', {
- 'download': `${currentStream.titleSlug}-${now}.png`,
- 'href': URL.createObjectURL(blob!),
- });
- $anchor.click();
-
- // Free screenshot from memory
- URL.revokeObjectURL($anchor.href);
- canvasContext.clearRect(0, 0, $canvas.width, $canvas.height);
-
- callback && callback();
- }, 'image/png');
- }
-}
diff --git a/src/utils/settings-storages/global-settings-storage.ts b/src/utils/settings-storages/global-settings-storage.ts
index 86528b3..48149fb 100644
--- a/src/utils/settings-storages/global-settings-storage.ts
+++ b/src/utils/settings-storages/global-settings-storage.ts
@@ -339,10 +339,10 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
requiredVariants: 'full',
label: t('enable-local-co-op-support'),
default: false,
- note: CE('a', {
- href: 'https://github.com/redphx/better-xcloud/discussions/275',
- target: '_blank',
- }, t('enable-local-co-op-support-note')),
+ note: () => CE('a', {
+ href: 'https://github.com/redphx/better-xcloud/discussions/275',
+ target: '_blank',
+ }, t('enable-local-co-op-support-note')),
},
/*
@@ -409,10 +409,10 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
url = 'https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer';
}
- setting.unsupportedNote = CE('a', {
- href: url,
- target: '_blank',
- }, '⚠️ ' + note);
+ setting.unsupportedNote = () => CE('a', {
+ href: url,
+ target: '_blank',
+ }, '⚠️ ' + note);
},
},
diff --git a/src/utils/stream-stats-collector.ts b/src/utils/stream-stats-collector.ts
index feb9ab8..4e73eee 100644
--- a/src/utils/stream-stats-collector.ts
+++ b/src/utils/stream-stats-collector.ts
@@ -3,6 +3,7 @@ import { BxEvent } from "./bx-event";
import { STATES } from "./global";
import { humanFileSize, secondsToHm } from "./html";
import { getPref } from "./settings-storages/global-settings-storage";
+import { BxLogger } from "./bx-logger";
export enum StreamStat {
PING = 'ping',
@@ -95,6 +96,7 @@ type CurrentStats = {
export class StreamStatsCollector {
private static instance: StreamStatsCollector;
public static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector());
+ private readonly LOG_TAG = 'StreamStatsCollector';
// Collect in background - 60 seconds
static readonly INTERVAL_BACKGROUND = 60 * 1000;
@@ -214,6 +216,10 @@ export class StreamStatsCollector {
private lastVideoStat?: RTCInboundRtpStreamStats | null;
+ private constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
+ }
+
async collect() {
const stats = await STATES.currentStream.peerConnection?.getStats();
if (!stats) {
diff --git a/src/utils/toast.ts b/src/utils/toast.ts
index da8b742..20dcfe6 100644
--- a/src/utils/toast.ts
+++ b/src/utils/toast.ts
@@ -1,4 +1,5 @@
import { CE } from "@utils/html";
+import { BxLogger } from "./bx-logger";
type ToastOptions = {
instant?: boolean;
@@ -6,85 +7,100 @@ type ToastOptions = {
}
export class Toast {
- private static $wrapper: HTMLElement;
- private static $msg: HTMLElement;
- private static $status: HTMLElement;
- private static stack: Array<[string, string, ToastOptions]> = [];
- private static isShowing = false;
+ private static instance: Toast;
+ public static getInstance = () => Toast.instance ?? (Toast.instance = new Toast());
+ private readonly LOG_TAG = 'Toast';
- private static timeout?: number | null;
- private static DURATION = 3000;
+ private $wrapper: HTMLElement;
+ private $msg: HTMLElement;
+ private $status: HTMLElement;
- static show(msg: string, status?: string, options: Partial = {}) {
+ private stack: Array<[string, string, ToastOptions]> = [];
+ private isShowing = false;
+
+ private timeoutId?: number | null;
+ private DURATION = 3000;
+
+ private constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
+
+ this.$wrapper = CE('div', {class: 'bx-toast bx-offscreen'},
+ this.$msg = CE('span', {class: 'bx-toast-msg'}),
+ this.$status = CE('span', {class: 'bx-toast-status'}),
+ );
+
+ this.$wrapper.addEventListener('transitionend', e => {
+ const classList = this.$wrapper.classList;
+ if (classList.contains('bx-hide')) {
+ classList.remove('bx-offscreen', 'bx-hide');
+ classList.add('bx-offscreen');
+
+ this.showNext();
+ }
+ });
+
+ document.documentElement.appendChild(this.$wrapper);
+ }
+
+ private show(msg: string, status?: string, options: Partial = {}) {
options = options || {};
const args = Array.from(arguments) as [string, string, ToastOptions];
if (options.instant) {
// Clear stack
- Toast.stack = [args];
- Toast.showNext();
+ this.stack = [args];
+ this.showNext();
} else {
- Toast.stack.push(args);
- !Toast.isShowing && Toast.showNext();
+ this.stack.push(args);
+ !this.isShowing && this.showNext();
}
}
- private static showNext() {
- if (!Toast.stack.length) {
- Toast.isShowing = false;
+ private showNext() {
+ if (!this.stack.length) {
+ this.isShowing = false;
return;
}
- Toast.isShowing = true;
+ this.isShowing = true;
- Toast.timeout && clearTimeout(Toast.timeout);
- Toast.timeout = window.setTimeout(Toast.hide, Toast.DURATION);
+ this.timeoutId && clearTimeout(this.timeoutId);
+ this.timeoutId = window.setTimeout(this.hide.bind(this), this.DURATION);
// Get values from item
- const [msg, status, options] = Toast.stack.shift()!;
+ const [msg, status, options] = this.stack.shift()!;
if (options && options.html) {
- Toast.$msg.innerHTML = msg;
+ this.$msg.innerHTML = msg;
} else {
- Toast.$msg.textContent = msg;
+ this.$msg.textContent = msg;
}
if (status) {
- Toast.$status.classList.remove('bx-gone');
- Toast.$status.textContent = status;
+ this.$status.classList.remove('bx-gone');
+ this.$status.textContent = status;
} else {
- Toast.$status.classList.add('bx-gone');
+ this.$status.classList.add('bx-gone');
}
- const classList = Toast.$wrapper.classList;
+ const classList = this.$wrapper.classList;
classList.remove('bx-offscreen', 'bx-hide');
classList.add('bx-show');
}
- private static hide() {
- Toast.timeout = null;
+ private hide() {
+ this.timeoutId = null;
- const classList = Toast.$wrapper.classList;
+ const classList = this.$wrapper.classList;
classList.remove('bx-show');
classList.add('bx-hide');
}
- static setup() {
- Toast.$wrapper = CE('div', {'class': 'bx-toast bx-offscreen'},
- Toast.$msg = CE('span', {'class': 'bx-toast-msg'}),
- Toast.$status = CE('span', {'class': 'bx-toast-status'}),
- );
+ static show(msg: string, status?: string, options: Partial = {}) {
+ Toast.getInstance().show(msg, status, options);
+ }
- Toast.$wrapper.addEventListener('transitionend', e => {
- const classList = Toast.$wrapper.classList;
- if (classList.contains('bx-hide')) {
- classList.remove('bx-offscreen', 'bx-hide');
- classList.add('bx-offscreen');
-
- Toast.showNext();
- }
- });
-
- document.documentElement.appendChild(Toast.$wrapper);
+ static showNext() {
+ Toast.getInstance().showNext();
}
}
diff --git a/src/utils/true-achievements.ts b/src/utils/true-achievements.ts
index 6c10d94..6b43362 100644
--- a/src/utils/true-achievements.ts
+++ b/src/utils/true-achievements.ts
@@ -1,42 +1,55 @@
import { BxIcon } from "./bx-icon";
+import { BxLogger } from "./bx-logger";
import { AppInterface, SCRIPT_VARIANT, STATES } from "./global";
import { ButtonStyle, CE, clearDataSet, createButton, getReactProps } from "./html";
import { t } from "./translation";
export class TrueAchievements {
- private static $link = createButton({
- label: t('true-achievements'),
- url: '#',
- icon: BxIcon.TRUE_ACHIEVEMENTS,
- style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_LINK,
- onClick: TrueAchievements.onClick,
- }) as HTMLAnchorElement;
+ private static instance: TrueAchievements;
+ public static getInstance = () => TrueAchievements.instance ?? (TrueAchievements.instance = new TrueAchievements());
+ private readonly LOG_TAG = 'TrueAchievements';
- static $button = createButton({
- label: t('true-achievements'),
- title: t('true-achievements'),
- icon: BxIcon.TRUE_ACHIEVEMENTS,
- style: ButtonStyle.FOCUSABLE,
- onClick: TrueAchievements.onClick,
- }) as HTMLAnchorElement;
+ private $link: HTMLElement;
+ private $button: HTMLElement;
+ private $hiddenLink: HTMLAnchorElement;
- private static onClick(e: Event) {
+ constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
+
+ this.$link = createButton({
+ label: t('true-achievements'),
+ url: '#',
+ icon: BxIcon.TRUE_ACHIEVEMENTS,
+ style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_LINK,
+ onClick: this.onClick.bind(this),
+ });
+
+ this.$button = createButton({
+ label: t('true-achievements'),
+ title: t('true-achievements'),
+ icon: BxIcon.TRUE_ACHIEVEMENTS,
+ style: ButtonStyle.FOCUSABLE,
+ onClick: this.onClick.bind(this),
+ });
+
+ this.$hiddenLink = CE('a', {
+ target: '_blank',
+ });
+ }
+
+ private onClick(e: Event) {
e.preventDefault();
- const dataset = TrueAchievements.$link.dataset;
- TrueAchievements.open(true, dataset.xboxTitleId, dataset.id);
-
// Close all xCloud's dialogs
window.BX_EXPOSED.dialogRoutes?.closeAll();
+
+ const dataset = this.$link.dataset;
+ this.open(true, dataset.xboxTitleId, dataset.id);
}
- private static $hiddenLink = CE('a', {
- target: '_blank',
- });
-
- private static updateIds(xboxTitleId?: string, id?: string) {
- const $link = TrueAchievements.$link;
- const $button = TrueAchievements.$button;
+ private updateIds(xboxTitleId?: string, id?: string) {
+ const $link = this.$link;
+ const $button = this.$button;
clearDataSet($link);
clearDataSet($button);
@@ -52,7 +65,7 @@ export class TrueAchievements {
}
}
- static injectAchievementsProgress($elm: HTMLElement) {
+ injectAchievementsProgress($elm: HTMLElement) {
// Only do this in Full version
if (SCRIPT_VARIANT !== 'full') {
return;
@@ -68,7 +81,7 @@ export class TrueAchievements {
// Get xboxTitleId of the game
let xboxTitleId: string | number | undefined;
try {
- const $container = $parent.closest('div[class*=AchievementsPreview-module__container]') as HTMLElement;
+ const $container = $parent.closest('div[class*=AchievementsPreview-module__container]');
if ($container) {
const props = getReactProps($container);
xboxTitleId = props.children.props.data.data.xboxTitleId;
@@ -76,24 +89,24 @@ export class TrueAchievements {
} catch (e) {}
if (!xboxTitleId) {
- xboxTitleId = TrueAchievements.getStreamXboxTitleId();
+ xboxTitleId = this.getStreamXboxTitleId();
}
if (typeof xboxTitleId !== 'undefined') {
xboxTitleId = xboxTitleId.toString();
}
- TrueAchievements.updateIds(xboxTitleId);
+ this.updateIds(xboxTitleId);
if (document.documentElement.dataset.xdsPlatform === 'tv') {
- $div.appendChild(TrueAchievements.$link);
+ $div.appendChild(this.$link);
} else {
- $div.appendChild(TrueAchievements.$button);
+ $div.appendChild(this.$button);
}
$parent.appendChild($div);
}
- static injectAchievementDetailPage($parent: HTMLElement) {
+ injectAchievementDetailPage($parent: HTMLElement) {
// Only do this in Full version
if (SCRIPT_VARIANT !== 'full') {
return;
@@ -109,7 +122,7 @@ export class TrueAchievements {
const achievementList: XboxAchievement[] = props.children.props.data.data;
// Get current achievement name
- const $header = $parent.querySelector('div[class*=AchievementDetailHeader]') as HTMLElement;
+ const $header = $parent.querySelector('div[class*=AchievementDetailHeader]')!;
const achievementName = getReactProps($header).children[0].props.achievementName;
// Find achievement based on name
@@ -125,19 +138,19 @@ export class TrueAchievements {
// Found achievement -> add TrueAchievements button
if (id) {
- TrueAchievements.updateIds(xboxTitleId, id);
- $parent.appendChild(TrueAchievements.$link);
+ this.updateIds(xboxTitleId, id);
+ $parent.appendChild(this.$link);
}
} catch (e) {};
}
- private static getStreamXboxTitleId() : number | undefined {
+ private getStreamXboxTitleId() : number | undefined {
return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId;
}
- static open(override: boolean, xboxTitleId?: number | string, id?: number | string) {
+ open(override: boolean, xboxTitleId?: number | string, id?: number | string) {
if (!xboxTitleId || xboxTitleId === 'undefined') {
- xboxTitleId = TrueAchievements.getStreamXboxTitleId();
+ xboxTitleId = this.getStreamXboxTitleId();
}
if (AppInterface && AppInterface.openTrueAchievementsLink) {
@@ -154,7 +167,7 @@ export class TrueAchievements {
}
}
- TrueAchievements.$hiddenLink.href = url;
- TrueAchievements.$hiddenLink.click();
+ this.$hiddenLink.href = url;
+ this.$hiddenLink.click();
}
}
diff --git a/src/utils/xcloud-api.ts b/src/utils/xcloud-api.ts
index dc93e47..7a15dc9 100644
--- a/src/utils/xcloud-api.ts
+++ b/src/utils/xcloud-api.ts
@@ -1,13 +1,19 @@
import { NATIVE_FETCH } from "./bx-flags";
+import { BxLogger } from "./bx-logger";
import { STATES } from "./global";
export class XcloudApi {
private static instance: XcloudApi;
public static getInstance = () => XcloudApi.instance ?? (XcloudApi.instance = new XcloudApi());
+ private readonly LOG_TAG = 'XcloudApi';
private CACHE_TITLES: {[key: string]: XcloudTitleInfo} = {};
private CACHE_WAIT_TIME: {[key: string]: XcloudWaitTimeInfo} = {};
+ private constructor() {
+ BxLogger.info(this.LOG_TAG, 'constructor()');
+ }
+
async getTitleInfo(id: string): Promise {
if (id in this.CACHE_TITLES) {
return this.CACHE_TITLES[id];
diff --git a/src/utils/xcloud-interceptor.ts b/src/utils/xcloud-interceptor.ts
index 552de7c..7305537 100644
--- a/src/utils/xcloud-interceptor.ts
+++ b/src/utils/xcloud-interceptor.ts
@@ -92,6 +92,8 @@ export class XcloudInterceptor {
}
private static async handlePlay(request: RequestInfo | URL, init?: RequestInit) {
+ BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
+
const PREF_STREAM_TARGET_RESOLUTION = getPref(PrefKey.STREAM_TARGET_RESOLUTION);
const PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE);
@@ -165,6 +167,8 @@ export class XcloudInterceptor {
return response;
}
+ BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
+
const obj = JSON.parse(text);
let overrides = JSON.parse(obj.clientStreamingConfigOverrides || '{}') || {};
diff --git a/src/utils/xhome-interceptor.ts b/src/utils/xhome-interceptor.ts
index 8a9ed3b..35f57ed 100644
--- a/src/utils/xhome-interceptor.ts
+++ b/src/utils/xhome-interceptor.ts
@@ -54,8 +54,7 @@ export class XhomeInterceptor {
private static async handleLogin(request: Request) {
try {
- const clone = (request as Request).clone();
-
+ const clone = request.clone();
const obj = await clone.json();
obj.offeringId = 'xhome';
@@ -75,30 +74,30 @@ export class XhomeInterceptor {
}
private static async handleConfiguration(request: Request | URL) {
+ BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
+
const response = await NATIVE_FETCH(request);
-
- const obj = await response.clone().json()
- console.log(obj);
-
- const processPorts = (port: number): number[] => {
- const ports = new Set();
- port && ports.add(port);
- ports.add(9002);
-
- return Array.from(ports);
- };
+ const obj = await response.clone().json();
const serverDetails = obj.serverDetails;
- if (serverDetails.ipAddress) {
- XhomeInterceptor.consoleAddrs[serverDetails.ipAddress] = processPorts(serverDetails.port);
- }
+ const pairs = [
+ ['ipAddress', 'port'],
+ ['ipV4Address', 'ipV4Port'],
+ ['ipV6Address', 'ipV6Port'],
+ ];
- if (serverDetails.ipV4Address) {
- XhomeInterceptor.consoleAddrs[serverDetails.ipV4Address] = processPorts(serverDetails.ipV4Port);
- }
-
- if (serverDetails.ipV6Address) {
- XhomeInterceptor.consoleAddrs[serverDetails.ipV6Address] = processPorts(serverDetails.ipV6Port);
+ XhomeInterceptor.consoleAddrs = {};
+ for (const pair in pairs) {
+ const [keyAddr, keyPort] = pair;
+ if (serverDetails[keyAddr]) {
+ const port = serverDetails[keyPort];
+ // Add port 9002 to the list of ports
+ const ports = new Set();
+ port && ports.add(port);
+ ports.add(9002);
+ // Save it
+ XhomeInterceptor.consoleAddrs[serverDetails[keyAddr]] = Array.from(ports);
+ }
}
response.json = () => Promise.resolve(obj);
@@ -164,6 +163,8 @@ export class XhomeInterceptor {
}
private static async handlePlay(request: RequestInfo | URL) {
+ BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
+
const clone = (request as Request).clone();
const body = await clone.json();
@@ -196,23 +197,25 @@ export class XhomeInterceptor {
headers['x-ms-device-info'] = JSON.stringify(deviceInfo);
- const opts: {[index: string]: any} = {
+ const opts: Record = {
method: clone.method,
headers: headers,
};
+ // Copy body
if (clone.method === 'POST') {
opts.body = await clone.text();
}
- let newUrl = request.url;
- if (!newUrl.includes('/servers/home')) {
- const index = request.url.indexOf('.xboxlive.com');
- newUrl = STATES.remotePlay.server + request.url.substring(index + 13);
+ // Replace xCloud domain with xHome domain
+ let url = request.url;
+ if (!url.includes('/servers/home')) {
+ const parsed = new URL(url);
+ url = STATES.remotePlay.server + parsed.pathname;
}
- request = new Request(newUrl, opts);
- let url = (typeof request === 'string') ? request : request.url;
+ // Create new Request instance
+ request = new Request(url, opts);
// Get console IP
if (url.includes('/configuration')) {
@@ -225,7 +228,7 @@ export class XhomeInterceptor {
return XhomeInterceptor.handleLogin(request);
} else if (url.endsWith('/titles')) {
return XhomeInterceptor.handleTitles(request);
- } else if (url && url.endsWith('/ice') && url.includes('/sessions/') && (request as Request).method === 'GET') {
+ } else if (url && url.endsWith('/ice') && url.includes('/sessions/') && request.method === 'GET') {
return patchIceCandidates(request, XhomeInterceptor.consoleAddrs);
}