mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-04 22:57:19 +02:00
Optimize + refactor code
This commit is contained in:
parent
075b15aa48
commit
de76364a46
1060
dist/better-xcloud.lite.user.js
vendored
1060
dist/better-xcloud.lite.user.js
vendored
File diff suppressed because it is too large
Load Diff
19
src/index.ts
19
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<HTMLElement>('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<HTMLElement>('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();
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -30,7 +30,7 @@ export class Dialog {
|
||||
} = options;
|
||||
|
||||
// Create dialog overlay
|
||||
const $overlay = document.querySelector('.bx-dialog-overlay') as HTMLElement;
|
||||
const $overlay = document.querySelector<HTMLElement>('.bx-dialog-overlay');
|
||||
|
||||
if (!$overlay) {
|
||||
this.$overlay = CE('div', {'class': 'bx-dialog-overlay bx-gone'});
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,6 @@ export class TrueAchievementsAction extends BaseGameBarAction {
|
||||
|
||||
onClick(e: Event) {
|
||||
super.onClick(e);
|
||||
TrueAchievements.open(false);
|
||||
TrueAchievements.getInstance().open(false);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<MkbStoredPreset> => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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<HTMLOptionElement>('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<HTMLSelectElement>('select', {tabindex: -1});
|
||||
this.#$.presetsSelect!.addEventListener('change', e => {
|
||||
this.#switchPreset(parseInt((e.target as HTMLSelectElement).value));
|
||||
this.$presetsSelect = CE<HTMLSelectElement>('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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -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<HTMLMetaElement>('meta[name=gamepass-app-version]'))?.content;
|
||||
const patches = JSON.stringify(ALL_PATCHES);
|
||||
|
||||
// Calculate signature
|
||||
|
@ -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<RemotePlayConsole>;
|
||||
private regions: Array<RemotePlayRegion> = [];
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -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<HTMLAudioElement>('div[data-testid=media-container] audio') ?? document.querySelector<HTMLAudioElement>('div[data-testid=media-container] video');
|
||||
if ($media) {
|
||||
$media.muted = !$media.muted;
|
||||
|
||||
|
@ -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 = '';
|
||||
|
@ -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,
|
||||
|
@ -18,7 +18,7 @@ export function onChangeVideoPlayerType() {
|
||||
|
||||
let isDisabled = false;
|
||||
|
||||
const $optCas = $videoProcessing.querySelector(`option[value=${StreamVideoProcessing.CAS}]`) as HTMLOptionElement;
|
||||
const $optCas = $videoProcessing.querySelector<HTMLOptionElement>(`option[value=${StreamVideoProcessing.CAS}]`);
|
||||
|
||||
if (playerType === StreamPlayerType.WEBGL2) {
|
||||
$optCas && ($optCas.disabled = false);
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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<HTMLElement>('#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<HTMLButtonElement>('button');
|
||||
if (!$button) {
|
||||
return null;
|
||||
}
|
||||
$button.setAttribute('title', label);
|
||||
|
||||
const $orgSvg = $button.querySelector('svg') as SVGElement;
|
||||
const $orgSvg = $button.querySelector<SVGElement>('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<HTMLElement>('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<HTMLElement>('div[class^=HUDButton]');
|
||||
if (!$orgButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hideGripHandle = () => {
|
||||
// Grip handle
|
||||
const $gripHandle = document.querySelector('#StreamHud button[class^=GripHandle]') as HTMLElement;
|
||||
const $gripHandle = document.querySelector<HTMLElement>('#StreamHud button[class^=GripHandle]');
|
||||
if ($gripHandle && $gripHandle.ariaExpanded === 'true') {
|
||||
$gripHandle.dispatchEvent(new PointerEvent('pointerdown'));
|
||||
$gripHandle.click();
|
||||
|
@ -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<HTMLElement>('.bx-select-value')!;
|
||||
width += 20; // Add checkbox's width
|
||||
} else {
|
||||
$label = $parent.querySelector('div') as HTMLElement;
|
||||
$label = $parent.querySelector<HTMLElement>('div')!;
|
||||
}
|
||||
|
||||
// Set min-width
|
||||
|
@ -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, string> = {
|
||||
[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<HTMLElement>('.bx-remote-play-device-wrapper button');
|
||||
$btnConnect && $btnConnect.focus();
|
||||
}
|
||||
}
|
||||
|
@ -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<SettingTabContentItem | PrefKey | (($parent: HTMLElement) => void) | false>;
|
||||
requiredVariants?: BuildVariant | Array<BuildVariant>;
|
||||
};
|
||||
|
||||
type SettingTab = {
|
||||
icon: SVGElement;
|
||||
group: 'global';
|
||||
items: Array<SettingTabContent | false>;
|
||||
group: SettingTabGroup,
|
||||
items: Array<SettingTabContent | false> | (() => Array<SettingTabContent | false>);
|
||||
requiredVariants?: BuildVariant | Array<BuildVariant>;
|
||||
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<HTMLMetaElement>('meta[name=gamepass-app-version]')!.content;
|
||||
const appDate = new Date(document.querySelector<HTMLMetaElement>('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<HTMLInputElement>('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<SettingTabContent | false> = [{
|
||||
private readonly TAB_VIRTUAL_CONTROLLER_ITEMS: (() => Array<SettingTabContent | false>) = () => [{
|
||||
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<SettingTabContent | false> = [{
|
||||
@ -535,7 +541,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
||||
}] : [],
|
||||
}];
|
||||
|
||||
private readonly TAB_SHORTCUTS_ITEMS: Array<SettingTabContent | false> = [{
|
||||
private readonly TAB_SHORTCUTS_ITEMS: (() => Array<SettingTabContent | false>) = () => [{
|
||||
requiredVariants: 'full',
|
||||
group: 'controller-shortcuts',
|
||||
label: t('controller-shortcuts'),
|
||||
@ -576,56 +582,59 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
||||
],
|
||||
}];
|
||||
|
||||
private readonly SETTINGS_UI: Array<SettingTab> = [
|
||||
{
|
||||
icon: BxIcon.HOME,
|
||||
private readonly SETTINGS_UI: Record<SettingTabGroup, SettingTab> = {
|
||||
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<HTMLSelectElement>(`#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<HTMLInputElement>(`#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<SettingTabContent | false>): 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<HTMLElement>('.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<HTMLElement>('[data-tab-group] > *');
|
||||
const siblingProperty = direction === 'next' ? 'nextSibling' : 'previousSibling';
|
||||
|
||||
let $tmp = $parent;
|
||||
|
@ -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',
|
||||
});
|
||||
|
@ -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<HTMLElement>('#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<HTMLElement>('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<HTMLElement>('#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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
4
src/types/preferences.d.ts
vendored
4
src/types/preferences.d.ts
vendored
@ -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;
|
||||
|
4
src/types/setting-definition.d.ts
vendored
4
src/types/setting-definition.d.ts
vendored
@ -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<SuggestedSettingCategory, any>,
|
||||
ready: (setting: SettingDefinition) => void;
|
||||
type: SettingElementType,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,6 +56,8 @@ function createElement<T=HTMLElement>(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<T=HTMLButtonElement>(options: BxButton): T {
|
||||
let $btn;
|
||||
if (options.url) {
|
||||
$btn = CE('a', {'class': 'bx-button'}) as HTMLAnchorElement;
|
||||
$btn = CE<HTMLAnchorElement>('a', {'class': 'bx-button'});
|
||||
$btn.href = options.url;
|
||||
$btn.target = '_blank';
|
||||
} else {
|
||||
$btn = CE('button', {'class': 'bx-button', type: 'button'}) as HTMLButtonElement;
|
||||
$btn = CE<HTMLButtonElement>('button', {'class': 'bx-button', type: 'button'});
|
||||
}
|
||||
|
||||
const style = (options.style || 0) as number;
|
||||
|
@ -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<void>((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<IDBObjectStore> {
|
||||
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<any> {
|
||||
// @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<number>(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<MkbStoredPreset> {
|
||||
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<MkbStoredPresets> {
|
||||
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<MkbStoredPresets>(resolve => {
|
||||
this.#add(table, preset)
|
||||
.then(([table, id]) => {
|
||||
preset.id = id;
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, id);
|
||||
|
||||
resolve({[id]: preset});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
79
src/utils/local-db/local-db.ts
Normal file
79
src/utils/local-db/local-db.ts
Normal file
@ -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<void>((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<IDBObjectStore> {
|
||||
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<any> {
|
||||
// @ts-ignore
|
||||
return this.call(table.get, ...arguments);
|
||||
}
|
||||
|
||||
protected getAll(table: IDBObjectStore): Promise<[IDBObjectStore, any]> {
|
||||
// @ts-ignore
|
||||
return this.call(table.getAll, ...arguments);
|
||||
}
|
||||
}
|
96
src/utils/local-db/mkb-presets-db.ts
Normal file
96
src/utils/local-db/mkb-presets-db.ts
Normal file
@ -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<number>(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<MkbStoredPreset> {
|
||||
return this.presetsTable()
|
||||
.then(table => this.get(table, id))
|
||||
.then(([table, preset]) => new Promise(resolve => resolve(preset)));
|
||||
}
|
||||
|
||||
getPresets(): Promise<MkbStoredPresets> {
|
||||
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<MkbStoredPresets>(resolve => {
|
||||
this.add(table, preset)
|
||||
.then(([table, id]) => {
|
||||
preset.id = id;
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, id);
|
||||
|
||||
resolve({[id]: preset});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -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<Response> => {
|
||||
let url = (typeof request === 'string') ? request : (request as Request).url;
|
||||
|
||||
// Check blocked URLs
|
||||
for (let blocked of BLOCKED_URLS) {
|
||||
if (!url.startsWith(blocked)) {
|
||||
continue;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
105
src/utils/screenshot-manager.ts
Normal file
105
src/utils/screenshot-manager.ts
Normal file
@ -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<HTMLAnchorElement>('a');
|
||||
|
||||
this.$canvas = CE<HTMLCanvasElement>('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');
|
||||
}
|
||||
}
|
@ -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<HTMLCanvasElement>('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<HTMLAnchorElement>('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');
|
||||
}
|
||||
}
|
@ -339,10 +339,10 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
requiredVariants: 'full',
|
||||
label: t('enable-local-co-op-support'),
|
||||
default: false,
|
||||
note: CE<HTMLAnchorElement>('a', {
|
||||
href: 'https://github.com/redphx/better-xcloud/discussions/275',
|
||||
target: '_blank',
|
||||
}, t('enable-local-co-op-support-note')),
|
||||
note: () => CE<HTMLAnchorElement>('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<HTMLAnchorElement>('a', {
|
||||
href: url,
|
||||
target: '_blank',
|
||||
}, '⚠️ ' + note);
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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<ToastOptions> = {}) {
|
||||
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<ToastOptions> = {}) {
|
||||
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<ToastOptions> = {}) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -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<HTMLAnchorElement>({
|
||||
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<HTMLAnchorElement>({
|
||||
label: t('true-achievements'),
|
||||
title: t('true-achievements'),
|
||||
icon: BxIcon.TRUE_ACHIEVEMENTS,
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: this.onClick.bind(this),
|
||||
});
|
||||
|
||||
this.$hiddenLink = CE<HTMLAnchorElement>('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<HTMLAnchorElement>('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<HTMLElement>('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<HTMLElement>('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();
|
||||
}
|
||||
}
|
||||
|
@ -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<XcloudTitleInfo | null> {
|
||||
if (id in this.CACHE_TITLES) {
|
||||
return this.CACHE_TITLES[id];
|
||||
|
@ -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 || '{}') || {};
|
||||
|
||||
|
@ -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<number>();
|
||||
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<number>();
|
||||
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<string, any> = {
|
||||
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);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user