mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-06 07:37: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 { StreamBadges } from "@modules/stream/stream-badges";
|
||||||
import { StreamStats } from "@modules/stream/stream-stats";
|
import { StreamStats } from "@modules/stream/stream-stats";
|
||||||
import { addCss, preloadFonts } from "@utils/css";
|
import { addCss, preloadFonts } from "@utils/css";
|
||||||
import { Toast } from "@utils/toast";
|
|
||||||
import { LoadingScreen } from "@modules/loading-screen";
|
import { LoadingScreen } from "@modules/loading-screen";
|
||||||
import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider";
|
import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider";
|
||||||
import { TouchController } from "@modules/touch-controller";
|
import { TouchController } from "@modules/touch-controller";
|
||||||
@ -26,7 +25,7 @@ import { disableAdobeAudienceManager, patchAudioContext, patchCanvasContext, pat
|
|||||||
import { AppInterface, STATES } from "@utils/global";
|
import { AppInterface, STATES } from "@utils/global";
|
||||||
import { BxLogger } from "@utils/bx-logger";
|
import { BxLogger } from "@utils/bx-logger";
|
||||||
import { GameBar } from "./modules/game-bar/game-bar";
|
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 { NativeMkbHandler } from "./modules/mkb/native-mkb-handler";
|
||||||
import { GuideMenu } from "./modules/ui/guide-menu";
|
import { GuideMenu } from "./modules/ui/guide-menu";
|
||||||
import { updateVideoPlayer } from "./modules/stream/stream-settings-utils";
|
import { updateVideoPlayer } from "./modules/stream/stream-settings-utils";
|
||||||
@ -170,7 +169,7 @@ document.addEventListener('readystatechange', e => {
|
|||||||
|
|
||||||
// Hide "Play with Friends" skeleton section
|
// Hide "Play with Friends" skeleton section
|
||||||
if (getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS)) {
|
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');
|
$parent && ($parent.style.display = 'none');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +193,7 @@ window.addEventListener(BxEvent.XCLOUD_SERVERS_UNAVAILABLE, e => {
|
|||||||
window.setTimeout(HeaderSection.watchHeader, 2000);
|
window.setTimeout(HeaderSection.watchHeader, 2000);
|
||||||
|
|
||||||
// Open Settings dialog on Unsupported page
|
// 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) {
|
if ($unsupportedPage) {
|
||||||
SettingsNavigationDialog.getInstance().show();
|
SettingsNavigationDialog.getInstance().show();
|
||||||
}
|
}
|
||||||
@ -241,7 +240,7 @@ window.addEventListener(BxEvent.STREAM_PLAYING, e => {
|
|||||||
|
|
||||||
if (isFullVersion()) {
|
if (isFullVersion()) {
|
||||||
const $video = (e as any).$video as HTMLVideoElement;
|
const $video = (e as any).$video as HTMLVideoElement;
|
||||||
Screenshot.updateCanvasSize($video.videoWidth, $video.videoHeight);
|
ScreenshotManager.getInstance().updateCanvasSize($video.videoWidth, $video.videoHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateVideoPlayer();
|
updateVideoPlayer();
|
||||||
@ -316,7 +315,7 @@ function unload() {
|
|||||||
if (isFullVersion()) {
|
if (isFullVersion()) {
|
||||||
MouseCursorHider.stop();
|
MouseCursorHider.stop();
|
||||||
TouchController.reset();
|
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 => {
|
isFullVersion() && window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, e => {
|
||||||
Screenshot.takeScreenshot();
|
ScreenshotManager.getInstance().takeScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -354,17 +353,13 @@ function main() {
|
|||||||
|
|
||||||
// Setup UI
|
// Setup UI
|
||||||
addCss();
|
addCss();
|
||||||
Toast.setup();
|
|
||||||
|
|
||||||
GuideMenu.addEventListeners();
|
GuideMenu.getInstance().addEventListeners();
|
||||||
StreamStatsCollector.setupEvents();
|
StreamStatsCollector.setupEvents();
|
||||||
StreamBadges.setupEvents();
|
StreamBadges.setupEvents();
|
||||||
StreamStats.setupEvents();
|
StreamStats.setupEvents();
|
||||||
|
|
||||||
if (isFullVersion()) {
|
if (isFullVersion()) {
|
||||||
(getPref(PrefKey.GAME_BAR_POSITION) !== 'off') && GameBar.getInstance();
|
|
||||||
Screenshot.setup();
|
|
||||||
|
|
||||||
STATES.userAgent.capabilities.touch && TouchController.updateCustomList();
|
STATES.userAgent.capabilities.touch && TouchController.updateCustomList();
|
||||||
overridePreloadState();
|
overridePreloadState();
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Screenshot } from "@utils/screenshot";
|
import { ScreenshotManager } from "@/utils/screenshot-manager";
|
||||||
import { GamepadKey } from "@enums/mkb";
|
import { GamepadKey } from "@enums/mkb";
|
||||||
import { PrompFont } from "@enums/prompt-font";
|
import { PrompFont } from "@enums/prompt-font";
|
||||||
import { CE, removeChildElements } from "@utils/html";
|
import { CE, removeChildElements } from "@utils/html";
|
||||||
@ -97,7 +97,7 @@ export class ControllerShortcut {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ShortcutAction.STREAM_SCREENSHOT_CAPTURE:
|
case ShortcutAction.STREAM_SCREENSHOT_CAPTURE:
|
||||||
Screenshot.takeScreenshot();
|
ScreenshotManager.getInstance().takeScreenshot();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ShortcutAction.STREAM_STATS_TOGGLE:
|
case ShortcutAction.STREAM_STATS_TOGGLE:
|
||||||
@ -163,8 +163,6 @@ export class ControllerShortcut {
|
|||||||
|
|
||||||
// Save to storage
|
// Save to storage
|
||||||
window.localStorage.setItem(ControllerShortcut.STORAGE_KEY, JSON.stringify(ControllerShortcut.ACTIONS));
|
window.localStorage.setItem(ControllerShortcut.STORAGE_KEY, JSON.stringify(ControllerShortcut.ACTIONS));
|
||||||
|
|
||||||
console.log(ControllerShortcut.ACTIONS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static updateProfileList(e?: GamepadEvent) {
|
private static updateProfileList(e?: GamepadEvent) {
|
||||||
|
@ -30,7 +30,7 @@ export class Dialog {
|
|||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Create dialog overlay
|
// Create dialog overlay
|
||||||
const $overlay = document.querySelector('.bx-dialog-overlay') as HTMLElement;
|
const $overlay = document.querySelector<HTMLElement>('.bx-dialog-overlay');
|
||||||
|
|
||||||
if (!$overlay) {
|
if (!$overlay) {
|
||||||
this.$overlay = CE('div', {'class': 'bx-dialog-overlay bx-gone'});
|
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 { createButton, ButtonStyle } from "@utils/html";
|
||||||
import { BaseGameBarAction } from "./action-base";
|
import { BaseGameBarAction } from "./action-base";
|
||||||
import { t } from "@utils/translation";
|
import { t } from "@utils/translation";
|
||||||
import { Screenshot } from "@/utils/screenshot";
|
import { ScreenshotManager } from "@/utils/screenshot-manager";
|
||||||
|
|
||||||
export class ScreenshotAction extends BaseGameBarAction {
|
export class ScreenshotAction extends BaseGameBarAction {
|
||||||
$content: HTMLElement;
|
$content: HTMLElement;
|
||||||
@ -20,6 +20,6 @@ export class ScreenshotAction extends BaseGameBarAction {
|
|||||||
|
|
||||||
onClick(e: Event): void {
|
onClick(e: Event): void {
|
||||||
super.onClick(e);
|
super.onClick(e);
|
||||||
Screenshot.takeScreenshot();
|
ScreenshotManager.getInstance().takeScreenshot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,6 @@ export class TrueAchievementsAction extends BaseGameBarAction {
|
|||||||
|
|
||||||
onClick(e: Event) {
|
onClick(e: Event) {
|
||||||
super.onClick(e);
|
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 { TrueAchievementsAction } from "./action-true-achievements";
|
||||||
import { SpeakerAction } from "./action-speaker";
|
import { SpeakerAction } from "./action-speaker";
|
||||||
import { RendererAction } from "./action-renderer";
|
import { RendererAction } from "./action-renderer";
|
||||||
|
import { BxLogger } from "@/utils/bx-logger";
|
||||||
|
|
||||||
|
|
||||||
export class GameBar {
|
export class GameBar {
|
||||||
private static instance: GameBar;
|
private static instance: GameBar;
|
||||||
public static getInstance = () => GameBar.instance ?? (GameBar.instance = new GameBar());
|
public static getInstance = () => GameBar.instance ?? (GameBar.instance = new GameBar());
|
||||||
|
private readonly LOG_TAG = 'GameBar';
|
||||||
|
|
||||||
private static readonly VISIBLE_DURATION = 2000;
|
private static readonly VISIBLE_DURATION = 2000;
|
||||||
|
|
||||||
@ -27,6 +29,8 @@ export class GameBar {
|
|||||||
private actions: BaseGameBarAction[] = [];
|
private actions: BaseGameBarAction[] = [];
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
|
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||||
|
|
||||||
let $container;
|
let $container;
|
||||||
|
|
||||||
const position = getPref(PrefKey.GAME_BAR_POSITION) as GameBarPosition;
|
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 { BxEvent } from "@utils/bx-event";
|
||||||
import { Toast } from "@utils/toast";
|
import { Toast } from "@utils/toast";
|
||||||
import { t } from "@utils/translation";
|
import { t } from "@utils/translation";
|
||||||
import { LocalDb } from "@utils/local-db";
|
|
||||||
import { KeyHelper } from "./key-helper";
|
import { KeyHelper } from "./key-helper";
|
||||||
import type { MkbStoredPreset } from "@/types/mkb";
|
import type { MkbStoredPreset } from "@/types/mkb";
|
||||||
import { AppInterface, STATES } from "@utils/global";
|
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 { NavigationDialogManager } from "../ui/dialog/navigation-dialog";
|
||||||
import { PrefKey } from "@/enums/pref-keys";
|
import { PrefKey } from "@/enums/pref-keys";
|
||||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||||
|
import { MkbPresetsDb } from "@/utils/local-db/mkb-presets-db";
|
||||||
const LOG_TAG = 'MkbHandler';
|
|
||||||
|
|
||||||
const PointerToMouseButton = {
|
const PointerToMouseButton = {
|
||||||
1: 0,
|
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 {
|
export class EmulatedMkbHandler extends MkbHandler {
|
||||||
private static instance: EmulatedMkbHandler;
|
private static instance: EmulatedMkbHandler;
|
||||||
public static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler());
|
public static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler());
|
||||||
|
private static readonly LOG_TAG = 'EmulatedMkbHandler';
|
||||||
|
|
||||||
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
|
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
|
||||||
|
|
||||||
@ -167,8 +166,9 @@ export class EmulatedMkbHandler extends MkbHandler {
|
|||||||
#RIGHT_STICK_X: GamepadKey[] = [];
|
#RIGHT_STICK_X: GamepadKey[] = [];
|
||||||
#RIGHT_STICK_Y: GamepadKey[] = [];
|
#RIGHT_STICK_Y: GamepadKey[] = [];
|
||||||
|
|
||||||
constructor() {
|
private constructor() {
|
||||||
super();
|
super();
|
||||||
|
BxLogger.info(EmulatedMkbHandler.LOG_TAG, 'constructor()');
|
||||||
|
|
||||||
this.#STICK_MAP = {
|
this.#STICK_MAP = {
|
||||||
[GamepadKey.LS_LEFT]: [this.#LEFT_STICK_X, 0, -1],
|
[GamepadKey.LS_LEFT]: [this.#LEFT_STICK_X, 0, -1],
|
||||||
@ -431,7 +431,7 @@ export class EmulatedMkbHandler extends MkbHandler {
|
|||||||
#getCurrentPreset = (): Promise<MkbStoredPreset> => {
|
#getCurrentPreset = (): Promise<MkbStoredPreset> => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const presetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
|
const presetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
|
||||||
LocalDb.INSTANCE.getPreset(presetId).then((preset: MkbStoredPreset) => {
|
MkbPresetsDb.getInstance().getPreset(presetId).then((preset: MkbStoredPreset) => {
|
||||||
resolve(preset);
|
resolve(preset);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -680,7 +680,7 @@ export class EmulatedMkbHandler extends MkbHandler {
|
|||||||
AppInterface && NativeMkbHandler.getInstance().init();
|
AppInterface && NativeMkbHandler.getInstance().init();
|
||||||
}
|
}
|
||||||
} else if (getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile())) {
|
} 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();
|
EmulatedMkbHandler.getInstance().init();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -130,7 +130,6 @@ export class MkbPreset {
|
|||||||
mouse[MkbPresetKey.MOUSE_MAP_TO] = MkbPreset.MOUSE_SETTINGS[MkbPresetKey.MOUSE_MAP_TO].default;
|
mouse[MkbPresetKey.MOUSE_MAP_TO] = MkbPreset.MOUSE_SETTINGS[MkbPresetKey.MOUSE_MAP_TO].default;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(obj);
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import { Dialog } from "@modules/dialog";
|
|||||||
import { KeyHelper } from "./key-helper";
|
import { KeyHelper } from "./key-helper";
|
||||||
import { MkbPreset } from "./mkb-preset";
|
import { MkbPreset } from "./mkb-preset";
|
||||||
import { EmulatedMkbHandler } from "./mkb-handler";
|
import { EmulatedMkbHandler } from "./mkb-handler";
|
||||||
import { LocalDb } from "@utils/local-db";
|
|
||||||
import { BxIcon } from "@utils/bx-icon";
|
import { BxIcon } from "@utils/bx-icon";
|
||||||
import type { MkbPresetData, MkbStoredPresets } from "@/types/mkb";
|
import type { MkbPresetData, MkbStoredPresets } from "@/types/mkb";
|
||||||
import { MkbPresetKey, GamepadKey, GamepadKeyName } from "@enums/mkb";
|
import { MkbPresetKey, GamepadKey, GamepadKeyName } from "@enums/mkb";
|
||||||
@ -12,18 +11,10 @@ import { deepClone } from "@utils/global";
|
|||||||
import { SettingElement } from "@/utils/setting-element";
|
import { SettingElement } from "@/utils/setting-element";
|
||||||
import { PrefKey } from "@/enums/pref-keys";
|
import { PrefKey } from "@/enums/pref-keys";
|
||||||
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
|
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 = {
|
type MkbRemapperStates = {
|
||||||
currentPresetId: number;
|
currentPresetId: number;
|
||||||
presets: MkbStoredPresets;
|
presets: MkbStoredPresets;
|
||||||
@ -33,7 +24,7 @@ type MkbRemapperStates = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class MkbRemapper {
|
export class MkbRemapper {
|
||||||
readonly #BUTTON_ORDERS = [
|
private readonly BUTTON_ORDERS = [
|
||||||
GamepadKey.UP,
|
GamepadKey.UP,
|
||||||
GamepadKey.DOWN,
|
GamepadKey.DOWN,
|
||||||
GamepadKey.LEFT,
|
GamepadKey.LEFT,
|
||||||
@ -66,16 +57,11 @@ export class MkbRemapper {
|
|||||||
GamepadKey.RS_RIGHT,
|
GamepadKey.RS_RIGHT,
|
||||||
];
|
];
|
||||||
|
|
||||||
static #instance: MkbRemapper;
|
private static instance: MkbRemapper;
|
||||||
static get INSTANCE() {
|
public static getInstance = () => MkbRemapper.instance ?? (MkbRemapper.instance = new MkbRemapper());
|
||||||
if (!MkbRemapper.#instance) {
|
private readonly LOG_TAG = 'MkbRemapper';
|
||||||
MkbRemapper.#instance = new MkbRemapper();
|
|
||||||
}
|
|
||||||
|
|
||||||
return MkbRemapper.#instance;
|
private STATE: MkbRemapperStates = {
|
||||||
};
|
|
||||||
|
|
||||||
#STATE: MkbRemapperStates = {
|
|
||||||
currentPresetId: 0,
|
currentPresetId: 0,
|
||||||
presets: {},
|
presets: {},
|
||||||
|
|
||||||
@ -84,151 +70,150 @@ export class MkbRemapper {
|
|||||||
isEditing: false,
|
isEditing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
#$: MkbRemapperElements = {
|
private $wrapper!: HTMLElement;
|
||||||
wrapper: null,
|
private $presetsSelect!: HTMLSelectElement;
|
||||||
presetsSelect: null,
|
private $activateButton!: HTMLButtonElement;
|
||||||
activateButton: null,
|
|
||||||
|
|
||||||
currentBindingKey: null,
|
private $currentBindingKey!: HTMLElement;
|
||||||
|
|
||||||
allKeyElements: [],
|
private allKeyElements: HTMLElement[] = [];
|
||||||
allMouseElements: {},
|
private allMouseElements: {[key in MkbPresetKey]?: HTMLElement} = {};
|
||||||
};
|
|
||||||
|
|
||||||
bindingDialog: Dialog;
|
bindingDialog: Dialog;
|
||||||
|
|
||||||
constructor() {
|
private constructor() {
|
||||||
this.#STATE.currentPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
|
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||||
|
this.STATE.currentPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
|
||||||
|
|
||||||
this.bindingDialog = new Dialog({
|
this.bindingDialog = new Dialog({
|
||||||
className: 'bx-binding-dialog',
|
className: 'bx-binding-dialog',
|
||||||
content: CE('div', {},
|
content: CE('div', {},
|
||||||
CE('p', {}, t('press-to-bind')),
|
CE('p', {}, t('press-to-bind')),
|
||||||
CE('i', {}, t('press-esc-to-cancel')),
|
CE('i', {}, t('press-esc-to-cancel')),
|
||||||
),
|
),
|
||||||
hideCloseButton: true,
|
hideCloseButton: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#clearEventListeners = () => {
|
private clearEventListeners = () => {
|
||||||
window.removeEventListener('keydown', this.#onKeyDown);
|
window.removeEventListener('keydown', this.onKeyDown);
|
||||||
window.removeEventListener('mousedown', this.#onMouseDown);
|
window.removeEventListener('mousedown', this.onMouseDown);
|
||||||
window.removeEventListener('wheel', this.#onWheel);
|
window.removeEventListener('wheel', this.onWheel);
|
||||||
};
|
};
|
||||||
|
|
||||||
#bindKey = ($elm: HTMLElement, key: any) => {
|
private bindKey = ($elm: HTMLElement, key: any) => {
|
||||||
const buttonIndex = parseInt($elm.getAttribute('data-button-index')!);
|
const buttonIndex = parseInt($elm.dataset.buttonIndex!);
|
||||||
const keySlot = parseInt($elm.getAttribute('data-key-slot')!);
|
const keySlot = parseInt($elm.dataset.keySlot!);
|
||||||
|
|
||||||
// Ignore if bind the save key to the same element
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unbind duplicated keys
|
// Unbind duplicated keys
|
||||||
for (const $otherElm of this.#$.allKeyElements) {
|
for (const $otherElm of this.allKeyElements) {
|
||||||
if ($otherElm.getAttribute('data-key-code') === key.code) {
|
if ($otherElm.dataset.keyCode === key.code) {
|
||||||
this.#unbindKey($otherElm);
|
this.unbindKey($otherElm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#STATE.editingPresetData!.mapping[buttonIndex][keySlot] = key.code;
|
this.STATE.editingPresetData!.mapping[buttonIndex][keySlot] = key.code;
|
||||||
$elm.textContent = key.name;
|
$elm.textContent = key.name;
|
||||||
$elm.setAttribute('data-key-code', key.code);
|
$elm.dataset.keyCode = key.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
#unbindKey = ($elm: HTMLElement) => {
|
private unbindKey = ($elm: HTMLElement) => {
|
||||||
const buttonIndex = parseInt($elm.getAttribute('data-button-index')!);
|
const buttonIndex = parseInt($elm.dataset.buttonIndex!);
|
||||||
const keySlot = parseInt($elm.getAttribute('data-key-slot')!);
|
const keySlot = parseInt($elm.dataset.keySlot!);
|
||||||
|
|
||||||
// Remove key from preset
|
// Remove key from preset
|
||||||
this.#STATE.editingPresetData!.mapping[buttonIndex][keySlot] = null;
|
this.STATE.editingPresetData!.mapping[buttonIndex][keySlot] = null;
|
||||||
$elm.textContent = '';
|
$elm.textContent = '';
|
||||||
$elm.removeAttribute('data-key-code');
|
delete $elm.dataset.keyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
#onWheel = (e: WheelEvent) => {
|
private onWheel = (e: WheelEvent) => {
|
||||||
e.preventDefault();
|
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);
|
window.setTimeout(() => this.bindingDialog.hide(), 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
#onMouseDown = (e: MouseEvent) => {
|
private onMouseDown = (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
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);
|
window.setTimeout(() => this.bindingDialog.hide(), 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
#onKeyDown = (e: KeyboardEvent) => {
|
private onKeyDown = (e: KeyboardEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.#clearEventListeners();
|
this.clearEventListeners();
|
||||||
|
|
||||||
if (e.code !== 'Escape') {
|
if (e.code !== 'Escape') {
|
||||||
this.#bindKey(this.#$.currentBindingKey!, KeyHelper.getKeyFromEvent(e));
|
this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
window.setTimeout(() => this.bindingDialog.hide(), 200);
|
window.setTimeout(() => this.bindingDialog.hide(), 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
#onBindingKey = (e: MouseEvent) => {
|
private onBindingKey = (e: MouseEvent) => {
|
||||||
if (!this.#STATE.isEditing || e.button !== 0) {
|
if (!this.STATE.isEditing || e.button !== 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
|
||||||
this.#$.currentBindingKey = e.target as HTMLElement;
|
this.$currentBindingKey = e.target as HTMLElement;
|
||||||
|
|
||||||
window.addEventListener('keydown', this.#onKeyDown);
|
window.addEventListener('keydown', this.onKeyDown);
|
||||||
window.addEventListener('mousedown', this.#onMouseDown);
|
window.addEventListener('mousedown', this.onMouseDown);
|
||||||
window.addEventListener('wheel', this.#onWheel);
|
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();
|
e.preventDefault();
|
||||||
if (!this.#STATE.isEditing) {
|
if (!this.STATE.isEditing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#unbindKey(e.target as HTMLElement);
|
this.unbindKey(e.target as HTMLElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
#getPreset = (presetId: number) => {
|
private getPreset = (presetId: number) => {
|
||||||
return this.#STATE.presets[presetId];
|
return this.STATE.presets[presetId];
|
||||||
}
|
}
|
||||||
|
|
||||||
#getCurrentPreset = () => {
|
private getCurrentPreset = () => {
|
||||||
return this.#getPreset(this.#STATE.currentPresetId);
|
return this.getPreset(this.STATE.currentPresetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
#switchPreset = (presetId: number) => {
|
private switchPreset = (presetId: number) => {
|
||||||
this.#STATE.currentPresetId = presetId;
|
this.STATE.currentPresetId = presetId;
|
||||||
const presetData = this.#getCurrentPreset().data;
|
const presetData = this.getCurrentPreset().data;
|
||||||
|
|
||||||
for (const $elm of this.#$.allKeyElements) {
|
for (const $elm of this.allKeyElements) {
|
||||||
const buttonIndex = parseInt($elm.getAttribute('data-button-index')!);
|
const buttonIndex = parseInt($elm.dataset.buttonIndex!);
|
||||||
const keySlot = parseInt($elm.getAttribute('data-key-slot')!);
|
const keySlot = parseInt($elm.dataset.keySlot!);
|
||||||
|
|
||||||
const buttonKeys = presetData.mapping[buttonIndex];
|
const buttonKeys = presetData.mapping[buttonIndex];
|
||||||
if (buttonKeys && buttonKeys[keySlot]) {
|
if (buttonKeys && buttonKeys[keySlot]) {
|
||||||
$elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]!);
|
$elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]!);
|
||||||
$elm.setAttribute('data-key-code', buttonKeys[keySlot]!);
|
$elm.dataset.keyCode = buttonKeys[keySlot]!;
|
||||||
} else {
|
} else {
|
||||||
$elm.textContent = '';
|
$elm.textContent = '';
|
||||||
$elm.removeAttribute('data-key-code');
|
delete $elm.dataset.keyCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let key: MkbPresetKey;
|
let key: MkbPresetKey;
|
||||||
for (key in this.#$.allMouseElements) {
|
for (key in this.allMouseElements) {
|
||||||
const $elm = this.#$.allMouseElements[key]!;
|
const $elm = this.allMouseElements[key]!;
|
||||||
let value = presetData.mouse[key];
|
let value = presetData.mouse[key];
|
||||||
if (typeof value === 'undefined') {
|
if (typeof value === 'undefined') {
|
||||||
value = MkbPreset.MOUSE_SETTINGS[key].default;
|
value = MkbPreset.MOUSE_SETTINGS[key].default;
|
||||||
@ -238,26 +223,26 @@ export class MkbRemapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update state of Activate button
|
// Update state of Activate button
|
||||||
const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.#STATE.currentPresetId;
|
const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.STATE.currentPresetId;
|
||||||
this.#$.activateButton!.disabled = activated;
|
this.$activateButton.disabled = activated;
|
||||||
this.#$.activateButton!.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
|
this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
|
||||||
}
|
}
|
||||||
|
|
||||||
#refresh() {
|
private refresh() {
|
||||||
// Clear presets select
|
// Clear presets select
|
||||||
while (this.#$.presetsSelect!.firstChild) {
|
while (this.$presetsSelect.firstChild) {
|
||||||
this.#$.presetsSelect!.removeChild(this.#$.presetsSelect!.firstChild);
|
this.$presetsSelect.removeChild(this.$presetsSelect.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalDb.INSTANCE.getPresets().then(presets => {
|
MkbPresetsDb.getInstance().getPresets().then(presets => {
|
||||||
this.#STATE.presets = presets;
|
this.STATE.presets = presets;
|
||||||
const $fragment = document.createDocumentFragment();
|
const $fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
let defaultPresetId;
|
let defaultPresetId;
|
||||||
if (this.#STATE.currentPresetId === 0) {
|
if (this.STATE.currentPresetId === 0) {
|
||||||
this.#STATE.currentPresetId = parseInt(Object.keys(presets)[0]);
|
this.STATE.currentPresetId = parseInt(Object.keys(presets)[0]);
|
||||||
|
|
||||||
defaultPresetId = this.#STATE.currentPresetId;
|
defaultPresetId = this.STATE.currentPresetId;
|
||||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId);
|
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId);
|
||||||
EmulatedMkbHandler.getInstance().refreshPresetData();
|
EmulatedMkbHandler.getInstance().refreshPresetData();
|
||||||
} else {
|
} else {
|
||||||
@ -272,40 +257,40 @@ export class MkbRemapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const $options = CE<HTMLOptionElement>('option', {value: id}, name);
|
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);
|
$fragment.appendChild($options);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.#$.presetsSelect!.appendChild($fragment);
|
this.$presetsSelect.appendChild($fragment);
|
||||||
|
|
||||||
// Update state of Activate button
|
// Update state of Activate button
|
||||||
const activated = defaultPresetId === this.#STATE.currentPresetId;
|
const activated = defaultPresetId === this.STATE.currentPresetId;
|
||||||
this.#$.activateButton!.disabled = activated;
|
this.$activateButton.disabled = activated;
|
||||||
this.#$.activateButton!.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
|
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) => {
|
private toggleEditing = (force?: boolean) => {
|
||||||
this.#STATE.isEditing = typeof force !== 'undefined' ? force : !this.#STATE.isEditing;
|
this.STATE.isEditing = typeof force !== 'undefined' ? force : !this.STATE.isEditing;
|
||||||
this.#$.wrapper!.classList.toggle('bx-editing', this.#STATE.isEditing);
|
this.$wrapper.classList.toggle('bx-editing', this.STATE.isEditing);
|
||||||
|
|
||||||
if (this.#STATE.isEditing) {
|
if (this.STATE.isEditing) {
|
||||||
this.#STATE.editingPresetData = deepClone(this.#getCurrentPreset().data);
|
this.STATE.editingPresetData = deepClone(this.getCurrentPreset().data);
|
||||||
} else {
|
} 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)) {
|
for (const $elm of Array.from(childElements)) {
|
||||||
if ($elm.parentElement!.parentElement!.classList.contains('bx-mkb-action-buttons')) {
|
if ($elm.parentElement!.parentElement!.classList.contains('bx-mkb-action-buttons')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let disable = !this.#STATE.isEditing;
|
let disable = !this.STATE.isEditing;
|
||||||
|
|
||||||
if ($elm.parentElement!.classList.contains('bx-mkb-preset-tools')) {
|
if ($elm.parentElement!.classList.contains('bx-mkb-preset-tools')) {
|
||||||
disable = !disable;
|
disable = !disable;
|
||||||
@ -316,14 +301,14 @@ export class MkbRemapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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 = CE<HTMLSelectElement>('select', {tabindex: -1});
|
||||||
this.#$.presetsSelect!.addEventListener('change', e => {
|
this.$presetsSelect.addEventListener('change', e => {
|
||||||
this.#switchPreset(parseInt((e.target as HTMLSelectElement).value));
|
this.switchPreset(parseInt((e.target as HTMLSelectElement).value));
|
||||||
});
|
});
|
||||||
|
|
||||||
const promptNewName = (value?: string) => {
|
const promptNewName = (value: string) => {
|
||||||
let newName: string | null = '';
|
let newName: string | null = '';
|
||||||
while (!newName) {
|
while (!newName) {
|
||||||
newName = prompt(t('prompt-preset-name'), value);
|
newName = prompt(t('prompt-preset-name'), value);
|
||||||
@ -336,15 +321,15 @@ export class MkbRemapper {
|
|||||||
return newName ? newName : false;
|
return newName ? newName : false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const $header = CE('div', {'class': 'bx-mkb-preset-tools'},
|
const $header = CE('div', {class: 'bx-mkb-preset-tools'},
|
||||||
this.#$.presetsSelect,
|
this.$presetsSelect,
|
||||||
// Rename button
|
// Rename button
|
||||||
createButton({
|
createButton({
|
||||||
title: t('rename'),
|
title: t('rename'),
|
||||||
icon: BxIcon.CURSOR_TEXT,
|
icon: BxIcon.CURSOR_TEXT,
|
||||||
tabIndex: -1,
|
tabIndex: -1,
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
const preset = this.#getCurrentPreset();
|
const preset = this.getCurrentPreset();
|
||||||
|
|
||||||
let newName = promptNewName(preset.name);
|
let newName = promptNewName(preset.name);
|
||||||
if (!newName || newName === preset.name) {
|
if (!newName || newName === preset.name) {
|
||||||
@ -353,28 +338,28 @@ export class MkbRemapper {
|
|||||||
|
|
||||||
// Update preset with new name
|
// Update preset with new name
|
||||||
preset.name = newName;
|
preset.name = newName;
|
||||||
LocalDb.INSTANCE.updatePreset(preset).then(id => this.#refresh());
|
MkbPresetsDb.getInstance().updatePreset(preset).then(id => this.refresh());
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// New button
|
// New button
|
||||||
createButton({
|
createButton({
|
||||||
icon: BxIcon.NEW,
|
icon: BxIcon.NEW,
|
||||||
title: t('new'),
|
title: t('new'),
|
||||||
tabIndex: -1,
|
tabIndex: -1,
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
let newName = promptNewName('');
|
let newName = promptNewName('');
|
||||||
if (!newName) {
|
if (!newName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new preset selected name
|
// Create new preset selected name
|
||||||
LocalDb.INSTANCE.newPreset(newName, MkbPreset.DEFAULT_PRESET).then(id => {
|
MkbPresetsDb.getInstance().newPreset(newName, MkbPreset.DEFAULT_PRESET).then(id => {
|
||||||
this.#STATE.currentPresetId = id;
|
this.STATE.currentPresetId = id;
|
||||||
this.#refresh();
|
this.refresh();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Copy button
|
// Copy button
|
||||||
createButton({
|
createButton({
|
||||||
@ -382,7 +367,7 @@ export class MkbRemapper {
|
|||||||
title: t('copy'),
|
title: t('copy'),
|
||||||
tabIndex: -1,
|
tabIndex: -1,
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
const preset = this.#getCurrentPreset();
|
const preset = this.getCurrentPreset();
|
||||||
|
|
||||||
let newName = promptNewName(`${preset.name} (2)`);
|
let newName = promptNewName(`${preset.name} (2)`);
|
||||||
if (!newName) {
|
if (!newName) {
|
||||||
@ -390,9 +375,9 @@ export class MkbRemapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new preset selected name
|
// Create new preset selected name
|
||||||
LocalDb.INSTANCE.newPreset(newName, preset.data).then(id => {
|
MkbPresetsDb.getInstance().newPreset(newName, preset.data).then(id => {
|
||||||
this.#STATE.currentPresetId = id;
|
this.STATE.currentPresetId = id;
|
||||||
this.#refresh();
|
this.refresh();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -408,23 +393,23 @@ export class MkbRemapper {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalDb.INSTANCE.deletePreset(this.#STATE.currentPresetId).then(id => {
|
MkbPresetsDb.getInstance().deletePreset(this.STATE.currentPresetId).then(id => {
|
||||||
this.#STATE.currentPresetId = 0;
|
this.STATE.currentPresetId = 0;
|
||||||
this.#refresh();
|
this.refresh();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.#$.wrapper!.appendChild($header);
|
this.$wrapper.appendChild($header);
|
||||||
|
|
||||||
const $rows = CE('div', {'class': 'bx-mkb-settings-rows'},
|
const $rows = CE('div', {class: 'bx-mkb-settings-rows'},
|
||||||
CE('i', {'class': 'bx-mkb-note'}, t('right-click-to-unbind')),
|
CE('i', {class: 'bx-mkb-note'}, t('right-click-to-unbind')),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render keys
|
// Render keys
|
||||||
const keysPerButton = 2;
|
const keysPerButton = 2;
|
||||||
for (const buttonIndex of this.#BUTTON_ORDERS) {
|
for (const buttonIndex of this.BUTTON_ORDERS) {
|
||||||
const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex];
|
const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex];
|
||||||
|
|
||||||
let $elm;
|
let $elm;
|
||||||
@ -437,22 +422,22 @@ export class MkbRemapper {
|
|||||||
'data-key-slot': i,
|
'data-key-slot': i,
|
||||||
}, ' ');
|
}, ' ');
|
||||||
|
|
||||||
$elm.addEventListener('mouseup', this.#onBindingKey);
|
$elm.addEventListener('mouseup', this.onBindingKey);
|
||||||
$elm.addEventListener('contextmenu', this.#onContextMenu);
|
$elm.addEventListener('contextmenu', this.onContextMenu);
|
||||||
|
|
||||||
$fragment.appendChild($elm);
|
$fragment.appendChild($elm);
|
||||||
this.#$.allKeyElements.push($elm);
|
this.allKeyElements.push($elm);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $keyRow = CE('div', {'class': 'bx-mkb-key-row'},
|
const $keyRow = CE('div', {class: 'bx-mkb-key-row'},
|
||||||
CE('label', {'title': buttonName}, buttonPrompt),
|
CE('label', {title: buttonName}, buttonPrompt),
|
||||||
$fragment,
|
$fragment,
|
||||||
);
|
);
|
||||||
|
|
||||||
$rows.appendChild($keyRow);
|
$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
|
// Render mouse settings
|
||||||
const $mouseSettings = document.createDocumentFragment();
|
const $mouseSettings = document.createDocumentFragment();
|
||||||
@ -463,7 +448,7 @@ export class MkbRemapper {
|
|||||||
|
|
||||||
let $elm;
|
let $elm;
|
||||||
const onChange = (e: Event, value: any) => {
|
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', {
|
const $row = CE('label', {
|
||||||
class: 'bx-settings-row',
|
class: 'bx-settings-row',
|
||||||
@ -474,32 +459,32 @@ export class MkbRemapper {
|
|||||||
);
|
);
|
||||||
|
|
||||||
$mouseSettings.appendChild($row);
|
$mouseSettings.appendChild($row);
|
||||||
this.#$.allMouseElements[key as MkbPresetKey] = $elm;
|
this.allMouseElements[key as MkbPresetKey] = $elm;
|
||||||
}
|
}
|
||||||
|
|
||||||
$rows.appendChild($mouseSettings);
|
$rows.appendChild($mouseSettings);
|
||||||
this.#$.wrapper!.appendChild($rows);
|
this.$wrapper.appendChild($rows);
|
||||||
|
|
||||||
// Render action buttons
|
// Render action buttons
|
||||||
const $actionButtons = CE('div', {'class': 'bx-mkb-action-buttons'},
|
const $actionButtons = CE('div', {class: 'bx-mkb-action-buttons'},
|
||||||
CE('div', {},
|
CE('div', {},
|
||||||
// Edit button
|
// Edit button
|
||||||
createButton({
|
createButton({
|
||||||
label: t('edit'),
|
label: t('edit'),
|
||||||
tabIndex: -1,
|
tabIndex: -1,
|
||||||
onClick: e => this.#toggleEditing(true),
|
onClick: e => this.toggleEditing(true),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Activate button
|
// Activate button
|
||||||
this.#$.activateButton = createButton({
|
this.$activateButton = createButton({
|
||||||
label: t('activate'),
|
label: t('activate'),
|
||||||
style: ButtonStyle.PRIMARY,
|
style: ButtonStyle.PRIMARY,
|
||||||
tabIndex: -1,
|
tabIndex: -1,
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.#STATE.currentPresetId);
|
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.STATE.currentPresetId);
|
||||||
EmulatedMkbHandler.getInstance().refreshPresetData();
|
EmulatedMkbHandler.getInstance().refreshPresetData();
|
||||||
|
|
||||||
this.#refresh();
|
this.refresh();
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@ -512,8 +497,8 @@ export class MkbRemapper {
|
|||||||
tabIndex: -1,
|
tabIndex: -1,
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
// Restore preset
|
// Restore preset
|
||||||
this.#switchPreset(this.#STATE.currentPresetId);
|
this.switchPreset(this.STATE.currentPresetId);
|
||||||
this.#toggleEditing(false);
|
this.toggleEditing(false);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@ -523,27 +508,27 @@ export class MkbRemapper {
|
|||||||
style: ButtonStyle.PRIMARY,
|
style: ButtonStyle.PRIMARY,
|
||||||
tabIndex: -1,
|
tabIndex: -1,
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
const updatedPreset = deepClone(this.#getCurrentPreset());
|
const updatedPreset = deepClone(this.getCurrentPreset());
|
||||||
updatedPreset.data = this.#STATE.editingPresetData as MkbPresetData;
|
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 this is the default preset => refresh preset data
|
||||||
if (id === getPref(PrefKey.MKB_DEFAULT_PRESET_ID)) {
|
if (id === getPref(PrefKey.MKB_DEFAULT_PRESET_ID)) {
|
||||||
EmulatedMkbHandler.getInstance().refreshPresetData();
|
EmulatedMkbHandler.getInstance().refreshPresetData();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#toggleEditing(false);
|
this.toggleEditing(false);
|
||||||
this.#refresh();
|
this.refresh();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.#$.wrapper!.appendChild($actionButtons);
|
this.$wrapper.appendChild($actionButtons);
|
||||||
|
|
||||||
this.#toggleEditing(false);
|
this.toggleEditing(false);
|
||||||
this.#refresh();
|
this.refresh();
|
||||||
return this.#$.wrapper;
|
return this.$wrapper;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { BxEvent } from "@/utils/bx-event";
|
|||||||
import { ButtonStyle, CE, createButton } from "@/utils/html";
|
import { ButtonStyle, CE, createButton } from "@/utils/html";
|
||||||
import { PrefKey } from "@/enums/pref-keys";
|
import { PrefKey } from "@/enums/pref-keys";
|
||||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||||
|
import { BxLogger } from "@/utils/bx-logger";
|
||||||
|
|
||||||
type NativeMouseData = {
|
type NativeMouseData = {
|
||||||
X: number,
|
X: number,
|
||||||
@ -24,6 +25,7 @@ type XcloudInputSink = {
|
|||||||
export class NativeMkbHandler extends MkbHandler {
|
export class NativeMkbHandler extends MkbHandler {
|
||||||
private static instance: NativeMkbHandler;
|
private static instance: NativeMkbHandler;
|
||||||
public static getInstance = () => NativeMkbHandler.instance ?? (NativeMkbHandler.instance = new NativeMkbHandler());
|
public static getInstance = () => NativeMkbHandler.instance ?? (NativeMkbHandler.instance = new NativeMkbHandler());
|
||||||
|
private readonly LOG_TAG = 'NativeMkbHandler';
|
||||||
|
|
||||||
#pointerClient: PointerClient | undefined;
|
#pointerClient: PointerClient | undefined;
|
||||||
#enabled: boolean = false;
|
#enabled: boolean = false;
|
||||||
@ -39,6 +41,11 @@ export class NativeMkbHandler extends MkbHandler {
|
|||||||
|
|
||||||
#$message?: HTMLElement;
|
#$message?: HTMLElement;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
super();
|
||||||
|
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||||
|
}
|
||||||
|
|
||||||
#onKeyboardEvent(e: KeyboardEvent) {
|
#onKeyboardEvent(e: KeyboardEvent) {
|
||||||
if (e.type === 'keyup' && e.code === 'F8') {
|
if (e.type === 'keyup' && e.code === 'F8') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -2,8 +2,6 @@ import { BxLogger } from "@/utils/bx-logger";
|
|||||||
import { Toast } from "@/utils/toast";
|
import { Toast } from "@/utils/toast";
|
||||||
import type { MkbHandler } from "./base-mkb-handler";
|
import type { MkbHandler } from "./base-mkb-handler";
|
||||||
|
|
||||||
const LOG_TAG = 'PointerClient';
|
|
||||||
|
|
||||||
enum PointerAction {
|
enum PointerAction {
|
||||||
MOVE = 1,
|
MOVE = 1,
|
||||||
BUTTON_PRESS = 2,
|
BUTTON_PRESS = 2,
|
||||||
@ -16,10 +14,15 @@ enum PointerAction {
|
|||||||
export class PointerClient {
|
export class PointerClient {
|
||||||
private static instance: PointerClient;
|
private static instance: PointerClient;
|
||||||
public static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient());
|
public static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient());
|
||||||
|
private readonly LOG_TAG = 'PointerClient';
|
||||||
|
|
||||||
private socket: WebSocket | undefined | null;
|
private socket: WebSocket | undefined | null;
|
||||||
private mkbHandler: MkbHandler | undefined;
|
private mkbHandler: MkbHandler | undefined;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||||
|
}
|
||||||
|
|
||||||
start(port: number, mkbHandler: MkbHandler) {
|
start(port: number, mkbHandler: MkbHandler) {
|
||||||
if (!port) {
|
if (!port) {
|
||||||
throw new Error('PointerServer port is 0');
|
throw new Error('PointerServer port is 0');
|
||||||
@ -33,12 +36,12 @@ export class PointerClient {
|
|||||||
|
|
||||||
// Connection opened
|
// Connection opened
|
||||||
this.socket.addEventListener('open', (event) => {
|
this.socket.addEventListener('open', (event) => {
|
||||||
BxLogger.info(LOG_TAG, 'connected')
|
BxLogger.info(this.LOG_TAG, 'connected')
|
||||||
});
|
});
|
||||||
|
|
||||||
// Error
|
// Error
|
||||||
this.socket.addEventListener('error', (event) => {
|
this.socket.addEventListener('error', (event) => {
|
||||||
BxLogger.error(LOG_TAG, event);
|
BxLogger.error(this.LOG_TAG, event);
|
||||||
Toast.show('Cannot setup mouse: ' + event);
|
Toast.show('Cannot setup mouse: ' + event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1212,7 +1212,7 @@ export class PatcherCache {
|
|||||||
*/
|
*/
|
||||||
static #getSignature(): number {
|
static #getSignature(): number {
|
||||||
const scriptVersion = SCRIPT_VERSION;
|
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);
|
const patches = JSON.stringify(ALL_PATCHES);
|
||||||
|
|
||||||
// Calculate signature
|
// Calculate signature
|
||||||
|
@ -9,8 +9,6 @@ import { PrefKey } from "@/enums/pref-keys";
|
|||||||
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
|
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
|
||||||
import { RemotePlayNavigationDialog } from "./ui/dialog/remote-play-dialog";
|
import { RemotePlayNavigationDialog } from "./ui/dialog/remote-play-dialog";
|
||||||
|
|
||||||
const LOG_TAG = 'RemotePlay';
|
|
||||||
|
|
||||||
export const enum RemotePlayConsoleState {
|
export const enum RemotePlayConsoleState {
|
||||||
ON = 'On',
|
ON = 'On',
|
||||||
OFF = 'Off',
|
OFF = 'Off',
|
||||||
@ -38,6 +36,7 @@ type RemotePlayConsole = {
|
|||||||
export class RemotePlayManager {
|
export class RemotePlayManager {
|
||||||
private static instance: RemotePlayManager;
|
private static instance: RemotePlayManager;
|
||||||
public static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager());
|
public static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager());
|
||||||
|
private readonly LOG_TAG = 'RemotePlayManager';
|
||||||
|
|
||||||
private isInitialized = false;
|
private isInitialized = false;
|
||||||
|
|
||||||
@ -47,6 +46,10 @@ export class RemotePlayManager {
|
|||||||
private consoles!: Array<RemotePlayConsole>;
|
private consoles!: Array<RemotePlayConsole>;
|
||||||
private regions: Array<RemotePlayRegion> = [];
|
private regions: Array<RemotePlayRegion> = [];
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||||
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
if (this.isInitialized) {
|
if (this.isInitialized) {
|
||||||
return;
|
return;
|
||||||
@ -56,9 +59,9 @@ export class RemotePlayManager {
|
|||||||
|
|
||||||
this.getXhomeToken(() => {
|
this.getXhomeToken(() => {
|
||||||
this.getConsolesList(() => {
|
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);
|
BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -77,13 +77,7 @@ export class SoundShortcut {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let $media: HTMLMediaElement;
|
const $media = document.querySelector<HTMLAudioElement>('div[data-testid=media-container] audio') ?? document.querySelector<HTMLAudioElement>('div[data-testid=media-container] video');
|
||||||
|
|
||||||
$media = document.querySelector('div[data-testid=media-container] audio') as HTMLAudioElement;
|
|
||||||
if (!$media) {
|
|
||||||
$media = document.querySelector('div[data-testid=media-container] video') as HTMLAudioElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($media) {
|
if ($media) {
|
||||||
$media.muted = !$media.muted;
|
$media.muted = !$media.muted;
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { isFullVersion } from "@macros/build" with {type: "macro"};
|
|||||||
|
|
||||||
import { CE } from "@/utils/html";
|
import { CE } from "@/utils/html";
|
||||||
import { WebGL2Player } from "./player/webgl2-player";
|
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 { StreamPlayerType, StreamVideoProcessing } from "@enums/stream-player";
|
||||||
import { STATES } from "@/utils/global";
|
import { STATES } from "@/utils/global";
|
||||||
import { PrefKey } from "@/enums/pref-keys";
|
import { PrefKey } from "@/enums/pref-keys";
|
||||||
@ -237,7 +237,7 @@ export class StreamPlayer {
|
|||||||
webGL2Player.setFilter(2);
|
webGL2Player.setFilter(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
isFullVersion() && Screenshot.updateCanvasFilters('none');
|
isFullVersion() && ScreenshotManager.getInstance().updateCanvasFilters('none');
|
||||||
|
|
||||||
webGL2Player.setSharpness(options.sharpness || 0);
|
webGL2Player.setSharpness(options.sharpness || 0);
|
||||||
webGL2Player.setSaturation(options.saturation || 100);
|
webGL2Player.setSaturation(options.saturation || 100);
|
||||||
@ -252,7 +252,7 @@ export class StreamPlayer {
|
|||||||
|
|
||||||
// Apply video filters to screenshots
|
// Apply video filters to screenshots
|
||||||
if (isFullVersion() && getPref(PrefKey.SCREENSHOT_APPLY_FILTERS)) {
|
if (isFullVersion() && getPref(PrefKey.SCREENSHOT_APPLY_FILTERS)) {
|
||||||
Screenshot.updateCanvasFilters(filters);
|
ScreenshotManager.getInstance().updateCanvasFilters(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
let css = '';
|
let css = '';
|
||||||
|
@ -50,6 +50,7 @@ enum StreamBadge {
|
|||||||
export class StreamBadges {
|
export class StreamBadges {
|
||||||
private static instance: StreamBadges;
|
private static instance: StreamBadges;
|
||||||
public static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges());
|
public static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges());
|
||||||
|
private readonly LOG_TAG = 'StreamBadges';
|
||||||
|
|
||||||
private serverInfo: StreamServerInfo = {};
|
private serverInfo: StreamServerInfo = {};
|
||||||
|
|
||||||
@ -96,6 +97,10 @@ export class StreamBadges {
|
|||||||
private intervalId?: number | null;
|
private intervalId?: number | null;
|
||||||
private readonly REFRESH_INTERVAL = 3 * 1000;
|
private readonly REFRESH_INTERVAL = 3 * 1000;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||||
|
}
|
||||||
|
|
||||||
setRegion(region: string) {
|
setRegion(region: string) {
|
||||||
this.serverInfo.server = {
|
this.serverInfo.server = {
|
||||||
region: region,
|
region: region,
|
||||||
|
@ -18,7 +18,7 @@ export function onChangeVideoPlayerType() {
|
|||||||
|
|
||||||
let isDisabled = false;
|
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) {
|
if (playerType === StreamPlayerType.WEBGL2) {
|
||||||
$optCas && ($optCas.disabled = false);
|
$optCas && ($optCas.disabled = false);
|
||||||
|
@ -5,11 +5,13 @@ import { STATES } from "@utils/global"
|
|||||||
import { PrefKey } from "@/enums/pref-keys"
|
import { PrefKey } from "@/enums/pref-keys"
|
||||||
import { getPref } from "@/utils/settings-storages/global-settings-storage"
|
import { getPref } from "@/utils/settings-storages/global-settings-storage"
|
||||||
import { StreamStat, StreamStatsCollector, type StreamStatGrade } from "@/utils/stream-stats-collector"
|
import { StreamStat, StreamStatsCollector, type StreamStatGrade } from "@/utils/stream-stats-collector"
|
||||||
|
import { BxLogger } from "@/utils/bx-logger"
|
||||||
|
|
||||||
|
|
||||||
export class StreamStats {
|
export class StreamStats {
|
||||||
private static instance: StreamStats;
|
private static instance: StreamStats;
|
||||||
public static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats());
|
public static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats());
|
||||||
|
private readonly LOG_TAG = 'StreamStats';
|
||||||
|
|
||||||
private intervalId?: number | null;
|
private intervalId?: number | null;
|
||||||
private readonly REFRESH_INTERVAL = 1 * 1000;
|
private readonly REFRESH_INTERVAL = 1 * 1000;
|
||||||
@ -69,7 +71,8 @@ export class StreamStats {
|
|||||||
|
|
||||||
quickGlanceObserver?: MutationObserver | null;
|
quickGlanceObserver?: MutationObserver | null;
|
||||||
|
|
||||||
constructor() {
|
private constructor() {
|
||||||
|
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ export class StreamUiHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const $streamHud = (e.target as HTMLElement).closest('#StreamHud') as HTMLElement;
|
const $streamHud = (e.target as HTMLElement).closest<HTMLElement>('#StreamHud');
|
||||||
if (!$streamHud) {
|
if (!$streamHud) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -58,13 +58,13 @@ export class StreamUiHandler {
|
|||||||
$container.addEventListener('transitionend', onTransitionEnd);
|
$container.addEventListener('transitionend', onTransitionEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $button = $container.querySelector('button') as HTMLElement;
|
const $button = $container.querySelector<HTMLButtonElement>('button');
|
||||||
if (!$button) {
|
if (!$button) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$button.setAttribute('title', label);
|
$button.setAttribute('title', label);
|
||||||
|
|
||||||
const $orgSvg = $button.querySelector('svg') as SVGElement;
|
const $orgSvg = $button.querySelector<SVGElement>('svg');
|
||||||
if (!$orgSvg) {
|
if (!$orgSvg) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ export class StreamUiHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static async handleStreamMenu() {
|
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) {
|
if (!$btnCloseHud) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -136,14 +136,14 @@ export class StreamUiHandler {
|
|||||||
|
|
||||||
private static handleSystemMenu($streamHud: HTMLElement) {
|
private static handleSystemMenu($streamHud: HTMLElement) {
|
||||||
// Get the last button
|
// Get the last button
|
||||||
const $orgButton = $streamHud.querySelector('div[class^=HUDButton]') as HTMLElement;
|
const $orgButton = $streamHud.querySelector<HTMLElement>('div[class^=HUDButton]');
|
||||||
if (!$orgButton) {
|
if (!$orgButton) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideGripHandle = () => {
|
const hideGripHandle = () => {
|
||||||
// Grip handle
|
// 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') {
|
if ($gripHandle && $gripHandle.ariaExpanded === 'true') {
|
||||||
$gripHandle.dispatchEvent(new PointerEvent('pointerdown'));
|
$gripHandle.dispatchEvent(new PointerEvent('pointerdown'));
|
||||||
$gripHandle.click();
|
$gripHandle.click();
|
||||||
|
@ -2,6 +2,7 @@ import { GamepadKey } from "@/enums/mkb";
|
|||||||
import { PrefKey } from "@/enums/pref-keys";
|
import { PrefKey } from "@/enums/pref-keys";
|
||||||
import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler";
|
import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler";
|
||||||
import { BxEvent } from "@/utils/bx-event";
|
import { BxEvent } from "@/utils/bx-event";
|
||||||
|
import { BxLogger } from "@/utils/bx-logger";
|
||||||
import { STATES } from "@/utils/global";
|
import { STATES } from "@/utils/global";
|
||||||
import { CE, isElementVisible } from "@/utils/html";
|
import { CE, isElementVisible } from "@/utils/html";
|
||||||
import { setNearby } from "@/utils/navigation-utils";
|
import { setNearby } from "@/utils/navigation-utils";
|
||||||
@ -89,6 +90,7 @@ export abstract class NavigationDialog {
|
|||||||
export class NavigationDialogManager {
|
export class NavigationDialogManager {
|
||||||
private static instance: NavigationDialogManager;
|
private static instance: NavigationDialogManager;
|
||||||
public static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new 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_POLLING_INTERVAL = 50;
|
||||||
private static readonly GAMEPAD_KEYS = [
|
private static readonly GAMEPAD_KEYS = [
|
||||||
@ -136,7 +138,9 @@ export class NavigationDialogManager {
|
|||||||
private $container: HTMLElement;
|
private $container: HTMLElement;
|
||||||
private dialog: NavigationDialog | null = null;
|
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 = CE('div', {class: 'bx-navigation-dialog-overlay bx-gone'});
|
||||||
this.$overlay.addEventListener('click', e => {
|
this.$overlay.addEventListener('click', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -185,17 +189,17 @@ export class NavigationDialogManager {
|
|||||||
|
|
||||||
const rect = $select.getBoundingClientRect();
|
const rect = $select.getBoundingClientRect();
|
||||||
|
|
||||||
let $label;
|
let $label: HTMLElement;
|
||||||
let width = Math.ceil(rect.width);
|
let width = Math.ceil(rect.width);
|
||||||
if (!width) {
|
if (!width) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($select as HTMLSelectElement).multiple) {
|
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
|
width += 20; // Add checkbox's width
|
||||||
} else {
|
} else {
|
||||||
$label = $parent.querySelector('div') as HTMLElement;
|
$label = $parent.querySelector<HTMLElement>('div')!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set min-width
|
// Set min-width
|
||||||
|
@ -7,11 +7,13 @@ import { t } from "@/utils/translation";
|
|||||||
import { RemotePlayConsoleState, RemotePlayManager } from "@/modules/remote-play-manager";
|
import { RemotePlayConsoleState, RemotePlayManager } from "@/modules/remote-play-manager";
|
||||||
import { BxSelectElement } from "@/web-components/bx-select";
|
import { BxSelectElement } from "@/web-components/bx-select";
|
||||||
import { BxEvent } from "@/utils/bx-event";
|
import { BxEvent } from "@/utils/bx-event";
|
||||||
|
import { BxLogger } from "@/utils/bx-logger";
|
||||||
|
|
||||||
|
|
||||||
export class RemotePlayNavigationDialog extends NavigationDialog {
|
export class RemotePlayNavigationDialog extends NavigationDialog {
|
||||||
private static instance: RemotePlayNavigationDialog;
|
private static instance: RemotePlayNavigationDialog;
|
||||||
public static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog());
|
public static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog());
|
||||||
|
private readonly LOG_TAG = 'RemotePlayNavigationDialog';
|
||||||
|
|
||||||
private readonly STATE_LABELS: Record<RemotePlayConsoleState, string> = {
|
private readonly STATE_LABELS: Record<RemotePlayConsoleState, string> = {
|
||||||
[RemotePlayConsoleState.ON]: t('powered-on'),
|
[RemotePlayConsoleState.ON]: t('powered-on'),
|
||||||
@ -22,8 +24,9 @@ export class RemotePlayNavigationDialog extends NavigationDialog {
|
|||||||
|
|
||||||
$container!: HTMLElement;
|
$container!: HTMLElement;
|
||||||
|
|
||||||
constructor() {
|
private constructor() {
|
||||||
super();
|
super();
|
||||||
|
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||||
this.setupDialog();
|
this.setupDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +127,7 @@ export class RemotePlayNavigationDialog extends NavigationDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focusIfNeeded(): void {
|
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();
|
$btnConnect && $btnConnect.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,12 +27,13 @@ import { ControllerDeviceVibration, getPref, getPrefDefinition, setPref, StreamT
|
|||||||
import { SettingElement, type BxHtmlSettingElement } from "@/utils/setting-element";
|
import { SettingElement, type BxHtmlSettingElement } from "@/utils/setting-element";
|
||||||
import type { RecommendedSettings, SettingDefinition, SuggestedSettingCategory as SuggestedSettingProfile } from "@/types/setting-definition";
|
import type { RecommendedSettings, SettingDefinition, SuggestedSettingCategory as SuggestedSettingProfile } from "@/types/setting-definition";
|
||||||
import { FullscreenText } from "../fullscreen-text";
|
import { FullscreenText } from "../fullscreen-text";
|
||||||
|
import { BxLogger } from "@/utils/bx-logger";
|
||||||
|
|
||||||
|
|
||||||
type SettingTabContentItem = Partial<{
|
type SettingTabContentItem = Partial<{
|
||||||
pref: PrefKey;
|
pref: PrefKey;
|
||||||
label: string;
|
label: string;
|
||||||
note: string;
|
note: string | (() => HTMLElement);
|
||||||
experimental: string;
|
experimental: string;
|
||||||
content: HTMLElement | (() => HTMLElement);
|
content: HTMLElement | (() => HTMLElement);
|
||||||
options: {[key: string]: string};
|
options: {[key: string]: string};
|
||||||
@ -51,24 +52,29 @@ type SettingTabContent = {
|
|||||||
unsupportedNote?: string | Text | null;
|
unsupportedNote?: string | Text | null;
|
||||||
helpUrl?: string;
|
helpUrl?: string;
|
||||||
content?: any;
|
content?: any;
|
||||||
|
lazyContent?: boolean | (() => HTMLElement);
|
||||||
items?: Array<SettingTabContentItem | PrefKey | (($parent: HTMLElement) => void) | false>;
|
items?: Array<SettingTabContentItem | PrefKey | (($parent: HTMLElement) => void) | false>;
|
||||||
requiredVariants?: BuildVariant | Array<BuildVariant>;
|
requiredVariants?: BuildVariant | Array<BuildVariant>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SettingTab = {
|
type SettingTab = {
|
||||||
icon: SVGElement;
|
icon: SVGElement;
|
||||||
group: 'global';
|
group: SettingTabGroup,
|
||||||
items: Array<SettingTabContent | false>;
|
items: Array<SettingTabContent | false> | (() => Array<SettingTabContent | false>);
|
||||||
requiredVariants?: BuildVariant | Array<BuildVariant>;
|
requiredVariants?: BuildVariant | Array<BuildVariant>;
|
||||||
|
lazyContent?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SettingTabGroup = 'global' | 'stream' | 'controller' | 'mkb' | 'native-mkb' | 'shortcuts' | 'stats';
|
||||||
|
|
||||||
export class SettingsNavigationDialog extends NavigationDialog {
|
export class SettingsNavigationDialog extends NavigationDialog {
|
||||||
private static instance: SettingsNavigationDialog;
|
private static instance: SettingsNavigationDialog;
|
||||||
public static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog());
|
public static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog());
|
||||||
|
private readonly LOG_TAG = 'SettingsNavigationDialog';
|
||||||
|
|
||||||
$container!: HTMLElement;
|
$container!: HTMLElement;
|
||||||
private $tabs!: HTMLElement;
|
private $tabs!: HTMLElement;
|
||||||
private $settings!: HTMLElement;
|
private $tabContents!: HTMLElement;
|
||||||
|
|
||||||
private $btnReload!: HTMLElement;
|
private $btnReload!: HTMLElement;
|
||||||
private $btnGlobalReload!: HTMLButtonElement;
|
private $btnGlobalReload!: HTMLButtonElement;
|
||||||
@ -326,8 +332,8 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
// xCloud version
|
// xCloud version
|
||||||
($parent) => {
|
($parent) => {
|
||||||
try {
|
try {
|
||||||
const appVersion = (document.querySelector('meta[name=gamepass-app-version]') as HTMLMetaElement).content;
|
const appVersion = document.querySelector<HTMLMetaElement>('meta[name=gamepass-app-version]')!.content;
|
||||||
const appDate = new Date((document.querySelector('meta[name=gamepass-app-date]') as HTMLMetaElement).content).toISOString().substring(0, 10);
|
const appDate = new Date(document.querySelector<HTMLMetaElement>('meta[name=gamepass-app-date]')!.content).toISOString().substring(0, 10);
|
||||||
$parent.appendChild(CE('div', {
|
$parent.appendChild(CE('div', {
|
||||||
class: 'bx-settings-app-version',
|
class: 'bx-settings-app-version',
|
||||||
}, `xCloud website version ${appVersion} (${appDate})`));
|
}, `xCloud website version ${appVersion} (${appDate})`));
|
||||||
@ -380,7 +386,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
disabled: !getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL),
|
disabled: !getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL),
|
||||||
},
|
},
|
||||||
onCreated: (setting: SettingTabContentItem, $elm: HTMLElement) => {
|
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 => {
|
window.addEventListener(BxEvent.SETTINGS_CHANGED, e => {
|
||||||
const { storageKey, settingKey, settingValue } = e as any;
|
const { storageKey, settingKey, settingValue } = e as any;
|
||||||
if (storageKey !== StorageKey.GLOBAL || settingKey !== PrefKey.AUDIO_VOLUME) {
|
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',
|
group: 'mkb',
|
||||||
label: t('virtual-controller'),
|
label: t('virtual-controller'),
|
||||||
helpUrl: 'https://better-xcloud.github.io/mouse-and-keyboard/',
|
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> = [{
|
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',
|
requiredVariants: 'full',
|
||||||
group: 'controller-shortcuts',
|
group: 'controller-shortcuts',
|
||||||
label: t('controller-shortcuts'),
|
label: t('controller-shortcuts'),
|
||||||
@ -576,56 +582,59 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
],
|
],
|
||||||
}];
|
}];
|
||||||
|
|
||||||
private readonly SETTINGS_UI: Array<SettingTab> = [
|
private readonly SETTINGS_UI: Record<SettingTabGroup, SettingTab> = {
|
||||||
{
|
global: {
|
||||||
icon: BxIcon.HOME,
|
|
||||||
group: 'global',
|
group: 'global',
|
||||||
|
icon: BxIcon.HOME,
|
||||||
items: this.TAB_GLOBAL_ITEMS,
|
items: this.TAB_GLOBAL_ITEMS,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
stream: {
|
||||||
icon: BxIcon.DISPLAY,
|
|
||||||
group: 'stream',
|
group: 'stream',
|
||||||
|
icon: BxIcon.DISPLAY,
|
||||||
items: this.TAB_DISPLAY_ITEMS,
|
items: this.TAB_DISPLAY_ITEMS,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
controller: {
|
||||||
icon: BxIcon.CONTROLLER,
|
|
||||||
group: 'controller',
|
group: 'controller',
|
||||||
|
icon: BxIcon.CONTROLLER,
|
||||||
items: this.TAB_CONTROLLER_ITEMS,
|
items: this.TAB_CONTROLLER_ITEMS,
|
||||||
requiredVariants: 'full',
|
requiredVariants: 'full',
|
||||||
},
|
},
|
||||||
|
|
||||||
isFullVersion() && getPref(PrefKey.MKB_ENABLED) && {
|
mkb: isFullVersion() && getPref(PrefKey.MKB_ENABLED) && {
|
||||||
icon: BxIcon.VIRTUAL_CONTROLLER,
|
|
||||||
group: 'mkb',
|
group: 'mkb',
|
||||||
|
icon: BxIcon.VIRTUAL_CONTROLLER,
|
||||||
items: this.TAB_VIRTUAL_CONTROLLER_ITEMS,
|
items: this.TAB_VIRTUAL_CONTROLLER_ITEMS,
|
||||||
|
lazyContent: true,
|
||||||
requiredVariants: 'full',
|
requiredVariants: 'full',
|
||||||
},
|
},
|
||||||
|
|
||||||
isFullVersion() && AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' && {
|
'native-mkb': isFullVersion() && AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' && {
|
||||||
icon: BxIcon.NATIVE_MKB,
|
|
||||||
group: 'native-mkb',
|
group: 'native-mkb',
|
||||||
|
icon: BxIcon.NATIVE_MKB,
|
||||||
items: this.TAB_NATIVE_MKB_ITEMS,
|
items: this.TAB_NATIVE_MKB_ITEMS,
|
||||||
requiredVariants: 'full',
|
requiredVariants: 'full',
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
shortcuts: {
|
||||||
icon: BxIcon.COMMAND,
|
|
||||||
group: 'shortcuts',
|
group: 'shortcuts',
|
||||||
|
icon: BxIcon.COMMAND,
|
||||||
items: this.TAB_SHORTCUTS_ITEMS,
|
items: this.TAB_SHORTCUTS_ITEMS,
|
||||||
|
lazyContent: true,
|
||||||
requiredVariants: 'full',
|
requiredVariants: 'full',
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
stats: {
|
||||||
icon: BxIcon.STREAM_STATS,
|
|
||||||
group: 'stats',
|
group: 'stats',
|
||||||
|
icon: BxIcon.STREAM_STATS,
|
||||||
items: this.TAB_STATS_ITEMS,
|
items: this.TAB_STATS_ITEMS,
|
||||||
},
|
},
|
||||||
];
|
};
|
||||||
|
|
||||||
constructor() {
|
private constructor() {
|
||||||
super();
|
super();
|
||||||
|
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||||
|
|
||||||
this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn;
|
this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn;
|
||||||
this.setupDialog();
|
this.setupDialog();
|
||||||
@ -653,7 +662,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trigger event
|
// 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) {
|
if ($selectUserAgent) {
|
||||||
$selectUserAgent.disabled = true;
|
$selectUserAgent.disabled = true;
|
||||||
BxEvent.dispatch($selectUserAgent, 'input', {});
|
BxEvent.dispatch($selectUserAgent, 'input', {});
|
||||||
@ -757,8 +766,11 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get labels
|
// Get labels
|
||||||
for (const settingTab of this.SETTINGS_UI) {
|
let settingTabGroup: keyof typeof this.SETTINGS_UI;
|
||||||
if (!settingTab || !settingTab.items) {
|
for (settingTabGroup in this.SETTINGS_UI) {
|
||||||
|
const settingTab = this.SETTINGS_UI[settingTabGroup];
|
||||||
|
|
||||||
|
if (!settingTab || !settingTab.items || typeof settingTab.items === 'function') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -901,7 +913,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
let prefKey: PrefKey;
|
let prefKey: PrefKey;
|
||||||
for (prefKey in settings) {
|
for (prefKey in settings) {
|
||||||
const suggestedValue = settings[prefKey];
|
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) {
|
if (!$checkBox.checked || $checkBox.disabled) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -961,36 +973,57 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
}, t('suggest-settings-link')),
|
}, 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) {
|
private renderTab(settingTab: SettingTab) {
|
||||||
const $svg = createSvgIcon(settingTab.icon as any);
|
const $svg = createSvgIcon(settingTab.icon as any);
|
||||||
$svg.dataset.group = settingTab.group;
|
$svg.dataset.group = settingTab.group;
|
||||||
$svg.tabIndex = 0;
|
$svg.tabIndex = 0;
|
||||||
|
settingTab.lazyContent && ($svg.dataset.lazy = settingTab.lazyContent.toString());
|
||||||
|
|
||||||
$svg.addEventListener('click', e => {
|
$svg.addEventListener('click', this.onTabClicked.bind(this));
|
||||||
// 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');
|
|
||||||
});
|
|
||||||
|
|
||||||
return $svg;
|
return $svg;
|
||||||
}
|
}
|
||||||
@ -1137,10 +1170,19 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let label = prefDefinition?.label || setting.label;
|
let label = prefDefinition?.label || setting.label;
|
||||||
let note = prefDefinition?.note || setting.note;
|
let note: string | undefined | (() => HTMLElement) | HTMLElement = prefDefinition?.note || setting.note;
|
||||||
let unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote;
|
let unsupportedNote: string | undefined | (() => HTMLElement) | HTMLElement = prefDefinition?.unsupportedNote || setting.unsupportedNote;
|
||||||
const experimental = prefDefinition?.experimental || setting.experimental;
|
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 (settingTabContent.label && setting.pref) {
|
||||||
if (prefDefinition?.suggest) {
|
if (prefDefinition?.suggest) {
|
||||||
typeof prefDefinition.suggest.lowest !== 'undefined' && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest);
|
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);
|
!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() {
|
private setupDialog() {
|
||||||
let $tabs: HTMLElement;
|
let $tabs: HTMLElement;
|
||||||
let $settings: HTMLElement;
|
let $tabContents: HTMLElement;
|
||||||
|
|
||||||
const $container = CE('div', {
|
const $container = CE('div', {
|
||||||
class: 'bx-settings-dialog',
|
class: 'bx-settings-dialog',
|
||||||
@ -1245,7 +1379,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
$settings = CE('div', {
|
$tabContents = CE('div', {
|
||||||
class: 'bx-settings-tab-contents',
|
class: 'bx-settings-tab-contents',
|
||||||
_nearby: {
|
_nearby: {
|
||||||
orientation: 'vertical',
|
orientation: 'vertical',
|
||||||
@ -1264,7 +1398,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
|
|
||||||
this.$container = $container;
|
this.$container = $container;
|
||||||
this.$tabs = $tabs;
|
this.$tabs = $tabs;
|
||||||
this.$settings = $settings;
|
this.$tabContents = $tabContents;
|
||||||
|
|
||||||
// Close dialog when not clicking on any child elements in the dialog
|
// Close dialog when not clicking on any child elements in the dialog
|
||||||
$container.addEventListener('click', e => {
|
$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) {
|
if (!settingTab) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1293,95 +1430,13 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
const $svg = this.renderTab(settingTab);
|
const $svg = this.renderTab(settingTab);
|
||||||
$tabs.appendChild($svg);
|
$tabs.appendChild($svg);
|
||||||
|
|
||||||
const $tabContent = CE('div', {
|
// Don't render lazy tab content
|
||||||
class: 'bx-gone',
|
if (typeof settingTab.items === 'function') {
|
||||||
'data-tab-group': settingTab.group,
|
continue;
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$settings.appendChild($tabContent);
|
const $tabContent = this.renderTabContent.call(this, settingTab, settingTab.items);
|
||||||
|
$tabContents.appendChild($tabContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select first tab
|
// Select first tab
|
||||||
@ -1398,13 +1453,13 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private focusActiveTab() {
|
private focusActiveTab() {
|
||||||
const $currentTab = this.$tabs!.querySelector('.bx-active') as HTMLElement;
|
const $currentTab = this.$tabs!.querySelector<HTMLElement>('.bx-active');
|
||||||
$currentTab && $currentTab.focus();
|
$currentTab && $currentTab.focus();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private focusVisibleSetting(type: 'first' | 'last' = 'first'): boolean {
|
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) {
|
if (!controls.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1450,7 +1505,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private jumpToSettingGroup(direction: 'next' | 'previous'): boolean {
|
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) {
|
if (!$tabContent) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1461,7 +1516,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
$header = $tabContent.querySelector('h2');
|
$header = $tabContent.querySelector('h2');
|
||||||
} else {
|
} else {
|
||||||
// Find the parent element
|
// 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';
|
const siblingProperty = direction === 'next' ? 'nextSibling' : 'previousSibling';
|
||||||
|
|
||||||
let $tmp = $parent;
|
let $tmp = $parent;
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
|
import { BxLogger } from "@/utils/bx-logger";
|
||||||
import { CE } from "@/utils/html";
|
import { CE } from "@/utils/html";
|
||||||
|
|
||||||
export class FullscreenText {
|
export class FullscreenText {
|
||||||
private static instance: FullscreenText;
|
private static instance: FullscreenText;
|
||||||
public static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText());
|
public static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText());
|
||||||
|
private readonly LOG_TAG = 'FullscreenText';
|
||||||
|
|
||||||
$text: HTMLElement;
|
$text: HTMLElement;
|
||||||
|
|
||||||
constructor() {
|
private constructor() {
|
||||||
|
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||||
this.$text = CE('div', {
|
this.$text = CE('div', {
|
||||||
class: 'bx-fullscreen-text bx-gone',
|
class: 'bx-fullscreen-text bx-gone',
|
||||||
});
|
});
|
||||||
|
@ -13,101 +13,104 @@ export enum GuideMenuTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class GuideMenu {
|
export class GuideMenu {
|
||||||
static #BUTTONS = {
|
private static instance: GuideMenu;
|
||||||
scriptSettings: createButton({
|
public static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu());
|
||||||
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});
|
|
||||||
|
|
||||||
// Close all xCloud's dialogs
|
private $renderedButtons?: HTMLElement;
|
||||||
GuideMenu.#closeGuideMenu();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
closeApp: AppInterface && createButton({
|
closeGuideMenu() {
|
||||||
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() {
|
|
||||||
if (window.BX_EXPOSED.dialogRoutes) {
|
if (window.BX_EXPOSED.dialogRoutes) {
|
||||||
window.BX_EXPOSED.dialogRoutes.closeAll();
|
window.BX_EXPOSED.dialogRoutes.closeAll();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use alternative method for Lite version
|
// 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();
|
$btnClose && $btnClose.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
static #renderButtons() {
|
private renderButtons() {
|
||||||
if (GuideMenu.#$renderedButtons) {
|
if (this.$renderedButtons) {
|
||||||
return GuideMenu.#$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', {
|
const $div = CE('div', {
|
||||||
class: 'bx-guide-home-buttons',
|
class: 'bx-guide-home-buttons',
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = [
|
for (const $button of buttonsLayout) {
|
||||||
GuideMenu.#BUTTONS.scriptSettings,
|
|
||||||
[
|
|
||||||
GuideMenu.#BUTTONS.backToHome,
|
|
||||||
GuideMenu.#BUTTONS.reloadPage,
|
|
||||||
GuideMenu.#BUTTONS.closeApp,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const $button of buttons) {
|
|
||||||
if (!$button) {
|
if (!$button) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -123,15 +126,15 @@ export class GuideMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GuideMenu.#$renderedButtons = $div;
|
this.$renderedButtons = $div;
|
||||||
return $div;
|
return $div;
|
||||||
}
|
}
|
||||||
|
|
||||||
static #injectHome($root: HTMLElement, isPlaying = false) {
|
injectHome($root: HTMLElement, isPlaying = false) {
|
||||||
if (isFullVersion()) {
|
if (isFullVersion()) {
|
||||||
const $achievementsProgress = $root.querySelector('button[class*=AchievementsButton-module__progressBarContainer]');
|
const $achievementsProgress = $root.querySelector('button[class*=AchievementsButton-module__progressBarContainer]');
|
||||||
if ($achievementsProgress) {
|
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]');
|
$target = $root.querySelector('a[class*=QuitGameButton]');
|
||||||
|
|
||||||
// Hide xCloud's Home button
|
// 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');
|
$btnXcloudHome && ($btnXcloudHome.style.display = 'none');
|
||||||
} else {
|
} else {
|
||||||
// Last divider
|
// Last divider
|
||||||
@ -156,29 +159,30 @@ export class GuideMenu {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const $buttons = GuideMenu.#renderButtons();
|
const $buttons = this.renderButtons();
|
||||||
$buttons.dataset.isPlaying = isPlaying.toString();
|
$buttons.dataset.isPlaying = isPlaying.toString();
|
||||||
$target.insertAdjacentElement('afterend', $buttons);
|
$target.insertAdjacentElement('afterend', $buttons);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #onShown(e: Event) {
|
async onShown(e: Event) {
|
||||||
const where = (e as any).where as GuideMenuTab;
|
const where = (e as any).where as GuideMenuTab;
|
||||||
|
|
||||||
if (where === GuideMenuTab.HOME) {
|
if (where === GuideMenuTab.HOME) {
|
||||||
const $root = document.querySelector('#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]') as HTMLElement;
|
const $root = document.querySelector<HTMLElement>('#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]');
|
||||||
$root && GuideMenu.#injectHome($root, STATES.isPlaying);
|
$root && this.injectHome($root, STATES.isPlaying);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static addEventListeners() {
|
addEventListeners() {
|
||||||
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown);
|
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
static observe($addedElm: HTMLElement) {
|
observe($addedElm: HTMLElement) {
|
||||||
const className = $addedElm.className;
|
const className = $addedElm.className;
|
||||||
|
|
||||||
|
// TrueAchievements
|
||||||
if (isFullVersion() && className.includes('AchievementsButton-module__progressBarContainer')) {
|
if (isFullVersion() && className.includes('AchievementsButton-module__progressBarContainer')) {
|
||||||
TrueAchievements.injectAchievementsProgress($addedElm);
|
TrueAchievements.getInstance().injectAchievementsProgress($addedElm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +196,7 @@ export class GuideMenu {
|
|||||||
if (isFullVersion()) {
|
if (isFullVersion()) {
|
||||||
const $achievDetailPage = $addedElm.querySelector('div[class*=AchievementDetailPage]');
|
const $achievDetailPage = $addedElm.querySelector('div[class*=AchievementDetailPage]');
|
||||||
if ($achievDetailPage) {
|
if ($achievDetailPage) {
|
||||||
TrueAchievements.injectAchievementDetailPage($achievDetailPage as HTMLElement);
|
TrueAchievements.getInstance().injectAchievementDetailPage($achievDetailPage as HTMLElement);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,36 +7,45 @@ import { t } from "@utils/translation";
|
|||||||
import { SettingsNavigationDialog } from "./dialog/settings-dialog";
|
import { SettingsNavigationDialog } from "./dialog/settings-dialog";
|
||||||
import { PrefKey } from "@/enums/pref-keys";
|
import { PrefKey } from "@/enums/pref-keys";
|
||||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||||
|
import { BxLogger } from "@/utils/bx-logger";
|
||||||
|
|
||||||
export class HeaderSection {
|
export class HeaderSection {
|
||||||
static #$remotePlayBtn = createButton({
|
private static instance: HeaderSection;
|
||||||
classes: ['bx-header-remote-play-button', 'bx-gone'],
|
public static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection());
|
||||||
icon: BxIcon.REMOTE_PLAY,
|
private readonly LOG_TAG = 'HeaderSection';
|
||||||
title: t('remote-play'),
|
|
||||||
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR,
|
|
||||||
onClick: e => {
|
|
||||||
RemotePlayManager.getInstance().togglePopup();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
static #$settingsBtn = createButton({
|
private $btnRemotePlay: HTMLElement;
|
||||||
classes: ['bx-header-settings-button'],
|
private $btnSettings: HTMLElement;
|
||||||
label: '???',
|
private $buttonsWrapper: HTMLElement;
|
||||||
style: ButtonStyle.FROSTED | ButtonStyle.DROP_SHADOW | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_HEIGHT,
|
|
||||||
onClick: e => {
|
|
||||||
SettingsNavigationDialog.getInstance().show();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
static #$buttonsWrapper = CE('div', {},
|
private observer?: MutationObserver;
|
||||||
getPref(PrefKey.REMOTE_PLAY_ENABLED) ? HeaderSection.#$remotePlayBtn : null,
|
private timeoutId?: number | null;
|
||||||
HeaderSection.#$settingsBtn,
|
|
||||||
);
|
|
||||||
|
|
||||||
static #observer: MutationObserver;
|
constructor() {
|
||||||
static #timeout: number | null;
|
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) {
|
if (!$parent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -44,8 +53,8 @@ export class HeaderSection {
|
|||||||
const PREF_LATEST_VERSION = getPref(PrefKey.LATEST_VERSION);
|
const PREF_LATEST_VERSION = getPref(PrefKey.LATEST_VERSION);
|
||||||
|
|
||||||
// Setup Settings button
|
// Setup Settings button
|
||||||
const $btnSettings = HeaderSection.#$settingsBtn;
|
const $btnSettings = this.$btnSettings;
|
||||||
if (isElementVisible(HeaderSection.#$buttonsWrapper)) {
|
if (isElementVisible(this.$buttonsWrapper)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,38 +66,42 @@ export class HeaderSection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add the Settings button to the web page
|
// 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]');
|
let $target = document.querySelector('#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]');
|
||||||
if (!$target) {
|
if (!$target) {
|
||||||
$target = document.querySelector('div[class^=UnsupportedMarketPage-module__buttons]');
|
$target = document.querySelector('div[class^=UnsupportedMarketPage-module__buttons]');
|
||||||
}
|
}
|
||||||
|
|
||||||
$target && HeaderSection.#injectSettingsButton($target as HTMLElement);
|
$target && this.injectSettingsButton($target as HTMLElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
static showRemotePlayButton() {
|
private watchHeader() {
|
||||||
HeaderSection.#$remotePlayBtn.classList.remove('bx-gone');
|
|
||||||
}
|
|
||||||
|
|
||||||
static watchHeader() {
|
|
||||||
const $root = document.querySelector('#PageContent header') || document.querySelector('#root');
|
const $root = document.querySelector('#PageContent header') || document.querySelector('#root');
|
||||||
if (!$root) {
|
if (!$root) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout);
|
this.timeoutId && clearTimeout(this.timeoutId);
|
||||||
HeaderSection.#timeout = null;
|
this.timeoutId = null;
|
||||||
|
|
||||||
HeaderSection.#observer && HeaderSection.#observer.disconnect();
|
this.observer && this.observer.disconnect();
|
||||||
HeaderSection.#observer = new MutationObserver(mutationList => {
|
this.observer = new MutationObserver(mutationList => {
|
||||||
HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout);
|
this.timeoutId && clearTimeout(this.timeoutId);
|
||||||
HeaderSection.#timeout = window.setTimeout(HeaderSection.checkHeader, 2000);
|
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};
|
options?: {[index: string]: string};
|
||||||
multipleOptions?: {[index: string]: string};
|
multipleOptions?: {[index: string]: string};
|
||||||
unsupported?: boolean;
|
unsupported?: boolean;
|
||||||
unsupported_note?: string | HTMLElement;
|
unsupportedNote?: string | (() => HTMLElement);
|
||||||
note?: string | HTMLElement;
|
note?: string | (() => HTMLElement);
|
||||||
type?: SettingElementType;
|
type?: SettingElementType;
|
||||||
ready?: (setting: PreferenceSetting) => void;
|
ready?: (setting: PreferenceSetting) => void;
|
||||||
migrate?: (this: Preferences, savedPrefs: any, value: any) => 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;
|
default: any;
|
||||||
} & Partial<{
|
} & Partial<{
|
||||||
label: string;
|
label: string;
|
||||||
note: string | HTMLElement;
|
note: string | (() => HTMLElement);
|
||||||
experimental: boolean;
|
experimental: boolean;
|
||||||
unsupported: boolean;
|
unsupported: boolean;
|
||||||
unsupportedNote: string | HTMLElement;
|
unsupportedNote: string | (() => HTMLElement);
|
||||||
suggest: PartialRecord<SuggestedSettingCategory, any>,
|
suggest: PartialRecord<SuggestedSettingCategory, any>,
|
||||||
ready: (setting: SettingDefinition) => void;
|
ready: (setting: SettingDefinition) => void;
|
||||||
type: SettingElementType,
|
type: SettingElementType,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { BX_FLAGS } from "./bx-flags";
|
||||||
|
|
||||||
const enum TextColor {
|
const enum TextColor {
|
||||||
INFO = '#008746',
|
INFO = '#008746',
|
||||||
WARNING = '#c1a404',
|
WARNING = '#c1a404',
|
||||||
@ -10,7 +12,7 @@ export class BxLogger {
|
|||||||
static error = (tag: string, ...args: any[]) => BxLogger.log(TextColor.ERROR, tag, ...args);
|
static error = (tag: string, ...args: any[]) => BxLogger.log(TextColor.ERROR, tag, ...args);
|
||||||
|
|
||||||
private static log(color: string, tag: string, ...args: any) {
|
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;
|
let $elm;
|
||||||
const hasNs = 'xmlns' in props;
|
const hasNs = 'xmlns' in props;
|
||||||
|
|
||||||
|
// console.trace('createElement', elmName, props);
|
||||||
|
|
||||||
if (hasNs) {
|
if (hasNs) {
|
||||||
$elm = document.createElementNS(props.xmlns, elmName);
|
$elm = document.createElementNS(props.xmlns, elmName);
|
||||||
delete props.xmlns;
|
delete props.xmlns;
|
||||||
@ -111,11 +113,11 @@ const ButtonStyleIndices = Object.keys(ButtonStyleClass).map(i => parseInt(i));
|
|||||||
export function createButton<T=HTMLButtonElement>(options: BxButton): T {
|
export function createButton<T=HTMLButtonElement>(options: BxButton): T {
|
||||||
let $btn;
|
let $btn;
|
||||||
if (options.url) {
|
if (options.url) {
|
||||||
$btn = CE('a', {'class': 'bx-button'}) as HTMLAnchorElement;
|
$btn = CE<HTMLAnchorElement>('a', {'class': 'bx-button'});
|
||||||
$btn.href = options.url;
|
$btn.href = options.url;
|
||||||
$btn.target = '_blank';
|
$btn.target = '_blank';
|
||||||
} else {
|
} 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;
|
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 { isFullVersion } from "@macros/build" with {type: "macro"};
|
||||||
|
|
||||||
import { BxEvent } from "@utils/bx-event";
|
|
||||||
import { BX_FLAGS, NATIVE_FETCH } from "@utils/bx-flags";
|
import { BX_FLAGS, NATIVE_FETCH } from "@utils/bx-flags";
|
||||||
import { TouchController } from "@modules/touch-controller";
|
import { TouchController } from "@modules/touch-controller";
|
||||||
import { STATES } from "@utils/global";
|
import { STATES } from "@utils/global";
|
||||||
@ -29,9 +28,7 @@ function clearDbLogs(dbName: string, table: string) {
|
|||||||
const objectStore = db.transaction(table, 'readwrite').objectStore(table);
|
const objectStore = db.transaction(table, 'readwrite').objectStore(table);
|
||||||
const objectStoreRequest = objectStore.clear();
|
const objectStoreRequest = objectStore.clear();
|
||||||
|
|
||||||
objectStoreRequest.onsuccess = function() {
|
objectStoreRequest.onsuccess = () => BxLogger.info('clearDbLogs', `Cleared ${dbName}.${table}`);
|
||||||
console.log(`[Better xCloud] Cleared ${dbName}.${table}`);
|
|
||||||
};
|
|
||||||
} catch (ex) {}
|
} catch (ex) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,6 +131,7 @@ export function interceptHttpRequests() {
|
|||||||
'https://browser.events.data.microsoft.com',
|
'https://browser.events.data.microsoft.com',
|
||||||
'https://dc.services.visualstudio.com',
|
'https://dc.services.visualstudio.com',
|
||||||
'https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io',
|
'https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io',
|
||||||
|
'https://mscom.demdex.net',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,29 +170,42 @@ export function interceptHttpRequests() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let gamepassAllGames: string[] = [];
|
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> => {
|
(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;
|
let url = (typeof request === 'string') ? request : (request as Request).url;
|
||||||
|
|
||||||
// Check blocked URLs
|
// Check blocked URLs
|
||||||
for (let blocked of BLOCKED_URLS) {
|
for (let blocked of BLOCKED_URLS) {
|
||||||
if (!url.startsWith(blocked)) {
|
if (url.startsWith(blocked)) {
|
||||||
continue;
|
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')) {
|
// Ignore URLs
|
||||||
BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
|
const domain = (new URL(url)).hostname;
|
||||||
|
if (IGNORED_DOMAINS.includes(domain)) {
|
||||||
|
return NATIVE_FETCH(request, init);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.endsWith('/configuration')) {
|
// BxLogger.info('fetch', url);
|
||||||
BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override experimentals
|
// Override experimentals
|
||||||
if (url.startsWith('https://emerald.xboxservices.com/xboxcomfd/experimentation')) {
|
if (url.startsWith('https://emerald.xboxservices.com/xboxcomfd/experimentation')) {
|
||||||
@ -212,6 +223,7 @@ export function interceptHttpRequests() {
|
|||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
return NATIVE_FETCH(request, init);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ export class RootDialogObserver {
|
|||||||
}
|
}
|
||||||
} else if ($root.querySelector('div[class*=GuideDialog]')) {
|
} else if ($root.querySelector('div[class*=GuideDialog]')) {
|
||||||
// Guide menu
|
// Guide menu
|
||||||
GuideMenu.observe($addedElm);
|
GuideMenu.getInstance().observe($addedElm);
|
||||||
return true;
|
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',
|
requiredVariants: 'full',
|
||||||
label: t('enable-local-co-op-support'),
|
label: t('enable-local-co-op-support'),
|
||||||
default: false,
|
default: false,
|
||||||
note: CE<HTMLAnchorElement>('a', {
|
note: () => CE<HTMLAnchorElement>('a', {
|
||||||
href: 'https://github.com/redphx/better-xcloud/discussions/275',
|
href: 'https://github.com/redphx/better-xcloud/discussions/275',
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
}, t('enable-local-co-op-support-note')),
|
}, 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';
|
url = 'https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer';
|
||||||
}
|
}
|
||||||
|
|
||||||
setting.unsupportedNote = CE('a', {
|
setting.unsupportedNote = () => CE<HTMLAnchorElement>('a', {
|
||||||
href: url,
|
href: url,
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
}, '⚠️ ' + note);
|
}, '⚠️ ' + note);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { BxEvent } from "./bx-event";
|
|||||||
import { STATES } from "./global";
|
import { STATES } from "./global";
|
||||||
import { humanFileSize, secondsToHm } from "./html";
|
import { humanFileSize, secondsToHm } from "./html";
|
||||||
import { getPref } from "./settings-storages/global-settings-storage";
|
import { getPref } from "./settings-storages/global-settings-storage";
|
||||||
|
import { BxLogger } from "./bx-logger";
|
||||||
|
|
||||||
export enum StreamStat {
|
export enum StreamStat {
|
||||||
PING = 'ping',
|
PING = 'ping',
|
||||||
@ -95,6 +96,7 @@ type CurrentStats = {
|
|||||||
export class StreamStatsCollector {
|
export class StreamStatsCollector {
|
||||||
private static instance: StreamStatsCollector;
|
private static instance: StreamStatsCollector;
|
||||||
public static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector());
|
public static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector());
|
||||||
|
private readonly LOG_TAG = 'StreamStatsCollector';
|
||||||
|
|
||||||
// Collect in background - 60 seconds
|
// Collect in background - 60 seconds
|
||||||
static readonly INTERVAL_BACKGROUND = 60 * 1000;
|
static readonly INTERVAL_BACKGROUND = 60 * 1000;
|
||||||
@ -214,6 +216,10 @@ export class StreamStatsCollector {
|
|||||||
|
|
||||||
private lastVideoStat?: RTCInboundRtpStreamStats | null;
|
private lastVideoStat?: RTCInboundRtpStreamStats | null;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||||
|
}
|
||||||
|
|
||||||
async collect() {
|
async collect() {
|
||||||
const stats = await STATES.currentStream.peerConnection?.getStats();
|
const stats = await STATES.currentStream.peerConnection?.getStats();
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { CE } from "@utils/html";
|
import { CE } from "@utils/html";
|
||||||
|
import { BxLogger } from "./bx-logger";
|
||||||
|
|
||||||
type ToastOptions = {
|
type ToastOptions = {
|
||||||
instant?: boolean;
|
instant?: boolean;
|
||||||
@ -6,85 +7,100 @@ type ToastOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Toast {
|
export class Toast {
|
||||||
private static $wrapper: HTMLElement;
|
private static instance: Toast;
|
||||||
private static $msg: HTMLElement;
|
public static getInstance = () => Toast.instance ?? (Toast.instance = new Toast());
|
||||||
private static $status: HTMLElement;
|
private readonly LOG_TAG = 'Toast';
|
||||||
private static stack: Array<[string, string, ToastOptions]> = [];
|
|
||||||
private static isShowing = false;
|
|
||||||
|
|
||||||
private static timeout?: number | null;
|
private $wrapper: HTMLElement;
|
||||||
private static DURATION = 3000;
|
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 || {};
|
options = options || {};
|
||||||
|
|
||||||
const args = Array.from(arguments) as [string, string, ToastOptions];
|
const args = Array.from(arguments) as [string, string, ToastOptions];
|
||||||
if (options.instant) {
|
if (options.instant) {
|
||||||
// Clear stack
|
// Clear stack
|
||||||
Toast.stack = [args];
|
this.stack = [args];
|
||||||
Toast.showNext();
|
this.showNext();
|
||||||
} else {
|
} else {
|
||||||
Toast.stack.push(args);
|
this.stack.push(args);
|
||||||
!Toast.isShowing && Toast.showNext();
|
!this.isShowing && this.showNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static showNext() {
|
private showNext() {
|
||||||
if (!Toast.stack.length) {
|
if (!this.stack.length) {
|
||||||
Toast.isShowing = false;
|
this.isShowing = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.isShowing = true;
|
this.isShowing = true;
|
||||||
|
|
||||||
Toast.timeout && clearTimeout(Toast.timeout);
|
this.timeoutId && clearTimeout(this.timeoutId);
|
||||||
Toast.timeout = window.setTimeout(Toast.hide, Toast.DURATION);
|
this.timeoutId = window.setTimeout(this.hide.bind(this), this.DURATION);
|
||||||
|
|
||||||
// Get values from item
|
// Get values from item
|
||||||
const [msg, status, options] = Toast.stack.shift()!;
|
const [msg, status, options] = this.stack.shift()!;
|
||||||
|
|
||||||
if (options && options.html) {
|
if (options && options.html) {
|
||||||
Toast.$msg.innerHTML = msg;
|
this.$msg.innerHTML = msg;
|
||||||
} else {
|
} else {
|
||||||
Toast.$msg.textContent = msg;
|
this.$msg.textContent = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
Toast.$status.classList.remove('bx-gone');
|
this.$status.classList.remove('bx-gone');
|
||||||
Toast.$status.textContent = status;
|
this.$status.textContent = status;
|
||||||
} else {
|
} 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.remove('bx-offscreen', 'bx-hide');
|
||||||
classList.add('bx-show');
|
classList.add('bx-show');
|
||||||
}
|
}
|
||||||
|
|
||||||
private static hide() {
|
private hide() {
|
||||||
Toast.timeout = null;
|
this.timeoutId = null;
|
||||||
|
|
||||||
const classList = Toast.$wrapper.classList;
|
const classList = this.$wrapper.classList;
|
||||||
classList.remove('bx-show');
|
classList.remove('bx-show');
|
||||||
classList.add('bx-hide');
|
classList.add('bx-hide');
|
||||||
}
|
}
|
||||||
|
|
||||||
static setup() {
|
static show(msg: string, status?: string, options: Partial<ToastOptions> = {}) {
|
||||||
Toast.$wrapper = CE('div', {'class': 'bx-toast bx-offscreen'},
|
Toast.getInstance().show(msg, status, options);
|
||||||
Toast.$msg = CE('span', {'class': 'bx-toast-msg'}),
|
}
|
||||||
Toast.$status = CE('span', {'class': 'bx-toast-status'}),
|
|
||||||
);
|
|
||||||
|
|
||||||
Toast.$wrapper.addEventListener('transitionend', e => {
|
static showNext() {
|
||||||
const classList = Toast.$wrapper.classList;
|
Toast.getInstance().showNext();
|
||||||
if (classList.contains('bx-hide')) {
|
|
||||||
classList.remove('bx-offscreen', 'bx-hide');
|
|
||||||
classList.add('bx-offscreen');
|
|
||||||
|
|
||||||
Toast.showNext();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.documentElement.appendChild(Toast.$wrapper);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,55 @@
|
|||||||
import { BxIcon } from "./bx-icon";
|
import { BxIcon } from "./bx-icon";
|
||||||
|
import { BxLogger } from "./bx-logger";
|
||||||
import { AppInterface, SCRIPT_VARIANT, STATES } from "./global";
|
import { AppInterface, SCRIPT_VARIANT, STATES } from "./global";
|
||||||
import { ButtonStyle, CE, clearDataSet, createButton, getReactProps } from "./html";
|
import { ButtonStyle, CE, clearDataSet, createButton, getReactProps } from "./html";
|
||||||
import { t } from "./translation";
|
import { t } from "./translation";
|
||||||
|
|
||||||
export class TrueAchievements {
|
export class TrueAchievements {
|
||||||
private static $link = createButton({
|
private static instance: TrueAchievements;
|
||||||
label: t('true-achievements'),
|
public static getInstance = () => TrueAchievements.instance ?? (TrueAchievements.instance = new TrueAchievements());
|
||||||
url: '#',
|
private readonly LOG_TAG = 'TrueAchievements';
|
||||||
icon: BxIcon.TRUE_ACHIEVEMENTS,
|
|
||||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_LINK,
|
|
||||||
onClick: TrueAchievements.onClick,
|
|
||||||
}) as HTMLAnchorElement;
|
|
||||||
|
|
||||||
static $button = createButton({
|
private $link: HTMLElement;
|
||||||
label: t('true-achievements'),
|
private $button: HTMLElement;
|
||||||
title: t('true-achievements'),
|
private $hiddenLink: HTMLAnchorElement;
|
||||||
icon: BxIcon.TRUE_ACHIEVEMENTS,
|
|
||||||
style: ButtonStyle.FOCUSABLE,
|
|
||||||
onClick: TrueAchievements.onClick,
|
|
||||||
}) as 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();
|
e.preventDefault();
|
||||||
|
|
||||||
const dataset = TrueAchievements.$link.dataset;
|
|
||||||
TrueAchievements.open(true, dataset.xboxTitleId, dataset.id);
|
|
||||||
|
|
||||||
// Close all xCloud's dialogs
|
// Close all xCloud's dialogs
|
||||||
window.BX_EXPOSED.dialogRoutes?.closeAll();
|
window.BX_EXPOSED.dialogRoutes?.closeAll();
|
||||||
|
|
||||||
|
const dataset = this.$link.dataset;
|
||||||
|
this.open(true, dataset.xboxTitleId, dataset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static $hiddenLink = CE<HTMLAnchorElement>('a', {
|
private updateIds(xboxTitleId?: string, id?: string) {
|
||||||
target: '_blank',
|
const $link = this.$link;
|
||||||
});
|
const $button = this.$button;
|
||||||
|
|
||||||
private static updateIds(xboxTitleId?: string, id?: string) {
|
|
||||||
const $link = TrueAchievements.$link;
|
|
||||||
const $button = TrueAchievements.$button;
|
|
||||||
|
|
||||||
clearDataSet($link);
|
clearDataSet($link);
|
||||||
clearDataSet($button);
|
clearDataSet($button);
|
||||||
@ -52,7 +65,7 @@ export class TrueAchievements {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static injectAchievementsProgress($elm: HTMLElement) {
|
injectAchievementsProgress($elm: HTMLElement) {
|
||||||
// Only do this in Full version
|
// Only do this in Full version
|
||||||
if (SCRIPT_VARIANT !== 'full') {
|
if (SCRIPT_VARIANT !== 'full') {
|
||||||
return;
|
return;
|
||||||
@ -68,7 +81,7 @@ export class TrueAchievements {
|
|||||||
// Get xboxTitleId of the game
|
// Get xboxTitleId of the game
|
||||||
let xboxTitleId: string | number | undefined;
|
let xboxTitleId: string | number | undefined;
|
||||||
try {
|
try {
|
||||||
const $container = $parent.closest('div[class*=AchievementsPreview-module__container]') as HTMLElement;
|
const $container = $parent.closest<HTMLElement>('div[class*=AchievementsPreview-module__container]');
|
||||||
if ($container) {
|
if ($container) {
|
||||||
const props = getReactProps($container);
|
const props = getReactProps($container);
|
||||||
xboxTitleId = props.children.props.data.data.xboxTitleId;
|
xboxTitleId = props.children.props.data.data.xboxTitleId;
|
||||||
@ -76,24 +89,24 @@ export class TrueAchievements {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
if (!xboxTitleId) {
|
if (!xboxTitleId) {
|
||||||
xboxTitleId = TrueAchievements.getStreamXboxTitleId();
|
xboxTitleId = this.getStreamXboxTitleId();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof xboxTitleId !== 'undefined') {
|
if (typeof xboxTitleId !== 'undefined') {
|
||||||
xboxTitleId = xboxTitleId.toString();
|
xboxTitleId = xboxTitleId.toString();
|
||||||
}
|
}
|
||||||
TrueAchievements.updateIds(xboxTitleId);
|
this.updateIds(xboxTitleId);
|
||||||
|
|
||||||
if (document.documentElement.dataset.xdsPlatform === 'tv') {
|
if (document.documentElement.dataset.xdsPlatform === 'tv') {
|
||||||
$div.appendChild(TrueAchievements.$link);
|
$div.appendChild(this.$link);
|
||||||
} else {
|
} else {
|
||||||
$div.appendChild(TrueAchievements.$button);
|
$div.appendChild(this.$button);
|
||||||
}
|
}
|
||||||
|
|
||||||
$parent.appendChild($div);
|
$parent.appendChild($div);
|
||||||
}
|
}
|
||||||
|
|
||||||
static injectAchievementDetailPage($parent: HTMLElement) {
|
injectAchievementDetailPage($parent: HTMLElement) {
|
||||||
// Only do this in Full version
|
// Only do this in Full version
|
||||||
if (SCRIPT_VARIANT !== 'full') {
|
if (SCRIPT_VARIANT !== 'full') {
|
||||||
return;
|
return;
|
||||||
@ -109,7 +122,7 @@ export class TrueAchievements {
|
|||||||
const achievementList: XboxAchievement[] = props.children.props.data.data;
|
const achievementList: XboxAchievement[] = props.children.props.data.data;
|
||||||
|
|
||||||
// Get current achievement name
|
// 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;
|
const achievementName = getReactProps($header).children[0].props.achievementName;
|
||||||
|
|
||||||
// Find achievement based on name
|
// Find achievement based on name
|
||||||
@ -125,19 +138,19 @@ export class TrueAchievements {
|
|||||||
|
|
||||||
// Found achievement -> add TrueAchievements button
|
// Found achievement -> add TrueAchievements button
|
||||||
if (id) {
|
if (id) {
|
||||||
TrueAchievements.updateIds(xboxTitleId, id);
|
this.updateIds(xboxTitleId, id);
|
||||||
$parent.appendChild(TrueAchievements.$link);
|
$parent.appendChild(this.$link);
|
||||||
}
|
}
|
||||||
} catch (e) {};
|
} catch (e) {};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getStreamXboxTitleId() : number | undefined {
|
private getStreamXboxTitleId() : number | undefined {
|
||||||
return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId;
|
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') {
|
if (!xboxTitleId || xboxTitleId === 'undefined') {
|
||||||
xboxTitleId = TrueAchievements.getStreamXboxTitleId();
|
xboxTitleId = this.getStreamXboxTitleId();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AppInterface && AppInterface.openTrueAchievementsLink) {
|
if (AppInterface && AppInterface.openTrueAchievementsLink) {
|
||||||
@ -154,7 +167,7 @@ export class TrueAchievements {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TrueAchievements.$hiddenLink.href = url;
|
this.$hiddenLink.href = url;
|
||||||
TrueAchievements.$hiddenLink.click();
|
this.$hiddenLink.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import { NATIVE_FETCH } from "./bx-flags";
|
import { NATIVE_FETCH } from "./bx-flags";
|
||||||
|
import { BxLogger } from "./bx-logger";
|
||||||
import { STATES } from "./global";
|
import { STATES } from "./global";
|
||||||
|
|
||||||
export class XcloudApi {
|
export class XcloudApi {
|
||||||
private static instance: XcloudApi;
|
private static instance: XcloudApi;
|
||||||
public static getInstance = () => XcloudApi.instance ?? (XcloudApi.instance = new XcloudApi());
|
public static getInstance = () => XcloudApi.instance ?? (XcloudApi.instance = new XcloudApi());
|
||||||
|
private readonly LOG_TAG = 'XcloudApi';
|
||||||
|
|
||||||
private CACHE_TITLES: {[key: string]: XcloudTitleInfo} = {};
|
private CACHE_TITLES: {[key: string]: XcloudTitleInfo} = {};
|
||||||
private CACHE_WAIT_TIME: {[key: string]: XcloudWaitTimeInfo} = {};
|
private CACHE_WAIT_TIME: {[key: string]: XcloudWaitTimeInfo} = {};
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||||
|
}
|
||||||
|
|
||||||
async getTitleInfo(id: string): Promise<XcloudTitleInfo | null> {
|
async getTitleInfo(id: string): Promise<XcloudTitleInfo | null> {
|
||||||
if (id in this.CACHE_TITLES) {
|
if (id in this.CACHE_TITLES) {
|
||||||
return this.CACHE_TITLES[id];
|
return this.CACHE_TITLES[id];
|
||||||
|
@ -92,6 +92,8 @@ export class XcloudInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static async handlePlay(request: RequestInfo | URL, init?: RequestInit) {
|
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_TARGET_RESOLUTION = getPref(PrefKey.STREAM_TARGET_RESOLUTION);
|
||||||
const PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE);
|
const PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE);
|
||||||
|
|
||||||
@ -165,6 +167,8 @@ export class XcloudInterceptor {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
|
||||||
|
|
||||||
const obj = JSON.parse(text);
|
const obj = JSON.parse(text);
|
||||||
let overrides = JSON.parse(obj.clientStreamingConfigOverrides || '{}') || {};
|
let overrides = JSON.parse(obj.clientStreamingConfigOverrides || '{}') || {};
|
||||||
|
|
||||||
|
@ -54,8 +54,7 @@ export class XhomeInterceptor {
|
|||||||
|
|
||||||
private static async handleLogin(request: Request) {
|
private static async handleLogin(request: Request) {
|
||||||
try {
|
try {
|
||||||
const clone = (request as Request).clone();
|
const clone = request.clone();
|
||||||
|
|
||||||
const obj = await clone.json();
|
const obj = await clone.json();
|
||||||
obj.offeringId = 'xhome';
|
obj.offeringId = 'xhome';
|
||||||
|
|
||||||
@ -75,30 +74,30 @@ export class XhomeInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static async handleConfiguration(request: Request | URL) {
|
private static async handleConfiguration(request: Request | URL) {
|
||||||
|
BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
|
||||||
|
|
||||||
const response = await NATIVE_FETCH(request);
|
const response = await NATIVE_FETCH(request);
|
||||||
|
const obj = await response.clone().json();
|
||||||
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 serverDetails = obj.serverDetails;
|
const serverDetails = obj.serverDetails;
|
||||||
if (serverDetails.ipAddress) {
|
const pairs = [
|
||||||
XhomeInterceptor.consoleAddrs[serverDetails.ipAddress] = processPorts(serverDetails.port);
|
['ipAddress', 'port'],
|
||||||
}
|
['ipV4Address', 'ipV4Port'],
|
||||||
|
['ipV6Address', 'ipV6Port'],
|
||||||
|
];
|
||||||
|
|
||||||
if (serverDetails.ipV4Address) {
|
XhomeInterceptor.consoleAddrs = {};
|
||||||
XhomeInterceptor.consoleAddrs[serverDetails.ipV4Address] = processPorts(serverDetails.ipV4Port);
|
for (const pair in pairs) {
|
||||||
}
|
const [keyAddr, keyPort] = pair;
|
||||||
|
if (serverDetails[keyAddr]) {
|
||||||
if (serverDetails.ipV6Address) {
|
const port = serverDetails[keyPort];
|
||||||
XhomeInterceptor.consoleAddrs[serverDetails.ipV6Address] = processPorts(serverDetails.ipV6Port);
|
// 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);
|
response.json = () => Promise.resolve(obj);
|
||||||
@ -164,6 +163,8 @@ export class XhomeInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static async handlePlay(request: RequestInfo | URL) {
|
private static async handlePlay(request: RequestInfo | URL) {
|
||||||
|
BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
|
||||||
|
|
||||||
const clone = (request as Request).clone();
|
const clone = (request as Request).clone();
|
||||||
const body = await clone.json();
|
const body = await clone.json();
|
||||||
|
|
||||||
@ -196,23 +197,25 @@ export class XhomeInterceptor {
|
|||||||
|
|
||||||
headers['x-ms-device-info'] = JSON.stringify(deviceInfo);
|
headers['x-ms-device-info'] = JSON.stringify(deviceInfo);
|
||||||
|
|
||||||
const opts: {[index: string]: any} = {
|
const opts: Record<string, any> = {
|
||||||
method: clone.method,
|
method: clone.method,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Copy body
|
||||||
if (clone.method === 'POST') {
|
if (clone.method === 'POST') {
|
||||||
opts.body = await clone.text();
|
opts.body = await clone.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
let newUrl = request.url;
|
// Replace xCloud domain with xHome domain
|
||||||
if (!newUrl.includes('/servers/home')) {
|
let url = request.url;
|
||||||
const index = request.url.indexOf('.xboxlive.com');
|
if (!url.includes('/servers/home')) {
|
||||||
newUrl = STATES.remotePlay.server + request.url.substring(index + 13);
|
const parsed = new URL(url);
|
||||||
|
url = STATES.remotePlay.server + parsed.pathname;
|
||||||
}
|
}
|
||||||
|
|
||||||
request = new Request(newUrl, opts);
|
// Create new Request instance
|
||||||
let url = (typeof request === 'string') ? request : request.url;
|
request = new Request(url, opts);
|
||||||
|
|
||||||
// Get console IP
|
// Get console IP
|
||||||
if (url.includes('/configuration')) {
|
if (url.includes('/configuration')) {
|
||||||
@ -225,7 +228,7 @@ export class XhomeInterceptor {
|
|||||||
return XhomeInterceptor.handleLogin(request);
|
return XhomeInterceptor.handleLogin(request);
|
||||||
} else if (url.endsWith('/titles')) {
|
} else if (url.endsWith('/titles')) {
|
||||||
return XhomeInterceptor.handleTitles(request);
|
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);
|
return patchIceCandidates(request, XhomeInterceptor.consoleAddrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user