Optimize + refactor code

This commit is contained in:
redphx
2024-10-21 20:50:12 +07:00
parent 075b15aa48
commit de76364a46
44 changed files with 1794 additions and 1274 deletions

View File

@@ -2,6 +2,7 @@ import { GamepadKey } from "@/enums/mkb";
import { PrefKey } from "@/enums/pref-keys";
import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler";
import { BxEvent } from "@/utils/bx-event";
import { BxLogger } from "@/utils/bx-logger";
import { STATES } from "@/utils/global";
import { CE, isElementVisible } from "@/utils/html";
import { setNearby } from "@/utils/navigation-utils";
@@ -89,6 +90,7 @@ export abstract class NavigationDialog {
export class NavigationDialogManager {
private static instance: NavigationDialogManager;
public static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager());
private readonly LOG_TAG = 'NavigationDialogManager';
private static readonly GAMEPAD_POLLING_INTERVAL = 50;
private static readonly GAMEPAD_KEYS = [
@@ -136,7 +138,9 @@ export class NavigationDialogManager {
private $container: HTMLElement;
private dialog: NavigationDialog | null = null;
constructor() {
private constructor() {
BxLogger.info(this.LOG_TAG, 'constructor()');
this.$overlay = CE('div', {class: 'bx-navigation-dialog-overlay bx-gone'});
this.$overlay.addEventListener('click', e => {
e.preventDefault();
@@ -185,17 +189,17 @@ export class NavigationDialogManager {
const rect = $select.getBoundingClientRect();
let $label;
let $label: HTMLElement;
let width = Math.ceil(rect.width);
if (!width) {
return;
}
if (($select as HTMLSelectElement).multiple) {
$label = $parent.querySelector('.bx-select-value') as HTMLElement;
$label = $parent.querySelector<HTMLElement>('.bx-select-value')!;
width += 20; // Add checkbox's width
} else {
$label = $parent.querySelector('div') as HTMLElement;
$label = $parent.querySelector<HTMLElement>('div')!;
}
// Set min-width

View File

@@ -7,11 +7,13 @@ import { t } from "@/utils/translation";
import { RemotePlayConsoleState, RemotePlayManager } from "@/modules/remote-play-manager";
import { BxSelectElement } from "@/web-components/bx-select";
import { BxEvent } from "@/utils/bx-event";
import { BxLogger } from "@/utils/bx-logger";
export class RemotePlayNavigationDialog extends NavigationDialog {
private static instance: RemotePlayNavigationDialog;
public static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog());
private readonly LOG_TAG = 'RemotePlayNavigationDialog';
private readonly STATE_LABELS: Record<RemotePlayConsoleState, string> = {
[RemotePlayConsoleState.ON]: t('powered-on'),
@@ -22,8 +24,9 @@ export class RemotePlayNavigationDialog extends NavigationDialog {
$container!: HTMLElement;
constructor() {
private constructor() {
super();
BxLogger.info(this.LOG_TAG, 'constructor()');
this.setupDialog();
}
@@ -124,7 +127,7 @@ export class RemotePlayNavigationDialog extends NavigationDialog {
}
focusIfNeeded(): void {
const $btnConnect = this.$container.querySelector('.bx-remote-play-device-wrapper button') as HTMLElement;
const $btnConnect = this.$container.querySelector<HTMLElement>('.bx-remote-play-device-wrapper button');
$btnConnect && $btnConnect.focus();
}
}

View File

@@ -27,12 +27,13 @@ import { ControllerDeviceVibration, getPref, getPrefDefinition, setPref, StreamT
import { SettingElement, type BxHtmlSettingElement } from "@/utils/setting-element";
import type { RecommendedSettings, SettingDefinition, SuggestedSettingCategory as SuggestedSettingProfile } from "@/types/setting-definition";
import { FullscreenText } from "../fullscreen-text";
import { BxLogger } from "@/utils/bx-logger";
type SettingTabContentItem = Partial<{
pref: PrefKey;
label: string;
note: string;
note: string | (() => HTMLElement);
experimental: string;
content: HTMLElement | (() => HTMLElement);
options: {[key: string]: string};
@@ -51,24 +52,29 @@ type SettingTabContent = {
unsupportedNote?: string | Text | null;
helpUrl?: string;
content?: any;
lazyContent?: boolean | (() => HTMLElement);
items?: Array<SettingTabContentItem | PrefKey | (($parent: HTMLElement) => void) | false>;
requiredVariants?: BuildVariant | Array<BuildVariant>;
};
type SettingTab = {
icon: SVGElement;
group: 'global';
items: Array<SettingTabContent | false>;
group: SettingTabGroup,
items: Array<SettingTabContent | false> | (() => Array<SettingTabContent | false>);
requiredVariants?: BuildVariant | Array<BuildVariant>;
lazyContent?: boolean;
};
type SettingTabGroup = 'global' | 'stream' | 'controller' | 'mkb' | 'native-mkb' | 'shortcuts' | 'stats';
export class SettingsNavigationDialog extends NavigationDialog {
private static instance: SettingsNavigationDialog;
public static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog());
private readonly LOG_TAG = 'SettingsNavigationDialog';
$container!: HTMLElement;
private $tabs!: HTMLElement;
private $settings!: HTMLElement;
private $tabContents!: HTMLElement;
private $btnReload!: HTMLElement;
private $btnGlobalReload!: HTMLButtonElement;
@@ -326,8 +332,8 @@ export class SettingsNavigationDialog extends NavigationDialog {
// xCloud version
($parent) => {
try {
const appVersion = (document.querySelector('meta[name=gamepass-app-version]') as HTMLMetaElement).content;
const appDate = new Date((document.querySelector('meta[name=gamepass-app-date]') as HTMLMetaElement).content).toISOString().substring(0, 10);
const appVersion = document.querySelector<HTMLMetaElement>('meta[name=gamepass-app-version]')!.content;
const appDate = new Date(document.querySelector<HTMLMetaElement>('meta[name=gamepass-app-date]')!.content).toISOString().substring(0, 10);
$parent.appendChild(CE('div', {
class: 'bx-settings-app-version',
}, `xCloud website version ${appVersion} (${appDate})`));
@@ -380,7 +386,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
disabled: !getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL),
},
onCreated: (setting: SettingTabContentItem, $elm: HTMLElement) => {
const $range = $elm.querySelector('input[type=range') as HTMLInputElement;
const $range = $elm.querySelector<HTMLInputElement>('input[type=range')!;
window.addEventListener(BxEvent.SETTINGS_CHANGED, e => {
const { storageKey, settingKey, settingValue } = e as any;
if (storageKey !== StorageKey.GLOBAL || settingKey !== PrefKey.AUDIO_VOLUME) {
@@ -511,11 +517,11 @@ export class SettingsNavigationDialog extends NavigationDialog {
}],
}];
private readonly TAB_VIRTUAL_CONTROLLER_ITEMS: Array<SettingTabContent | false> = [{
private readonly TAB_VIRTUAL_CONTROLLER_ITEMS: (() => Array<SettingTabContent | false>) = () => [{
group: 'mkb',
label: t('virtual-controller'),
helpUrl: 'https://better-xcloud.github.io/mouse-and-keyboard/',
content: isFullVersion() && MkbRemapper.INSTANCE.render(),
content: MkbRemapper.getInstance().render(),
}];
private readonly TAB_NATIVE_MKB_ITEMS: Array<SettingTabContent | false> = [{
@@ -535,7 +541,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
}] : [],
}];
private readonly TAB_SHORTCUTS_ITEMS: Array<SettingTabContent | false> = [{
private readonly TAB_SHORTCUTS_ITEMS: (() => Array<SettingTabContent | false>) = () => [{
requiredVariants: 'full',
group: 'controller-shortcuts',
label: t('controller-shortcuts'),
@@ -576,56 +582,59 @@ export class SettingsNavigationDialog extends NavigationDialog {
],
}];
private readonly SETTINGS_UI: Array<SettingTab> = [
{
icon: BxIcon.HOME,
private readonly SETTINGS_UI: Record<SettingTabGroup, SettingTab> = {
global: {
group: 'global',
icon: BxIcon.HOME,
items: this.TAB_GLOBAL_ITEMS,
},
{
icon: BxIcon.DISPLAY,
stream: {
group: 'stream',
icon: BxIcon.DISPLAY,
items: this.TAB_DISPLAY_ITEMS,
},
{
icon: BxIcon.CONTROLLER,
controller: {
group: 'controller',
icon: BxIcon.CONTROLLER,
items: this.TAB_CONTROLLER_ITEMS,
requiredVariants: 'full',
},
isFullVersion() && getPref(PrefKey.MKB_ENABLED) && {
icon: BxIcon.VIRTUAL_CONTROLLER,
mkb: isFullVersion() && getPref(PrefKey.MKB_ENABLED) && {
group: 'mkb',
icon: BxIcon.VIRTUAL_CONTROLLER,
items: this.TAB_VIRTUAL_CONTROLLER_ITEMS,
lazyContent: true,
requiredVariants: 'full',
},
isFullVersion() && AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' && {
icon: BxIcon.NATIVE_MKB,
'native-mkb': isFullVersion() && AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' && {
group: 'native-mkb',
icon: BxIcon.NATIVE_MKB,
items: this.TAB_NATIVE_MKB_ITEMS,
requiredVariants: 'full',
},
{
icon: BxIcon.COMMAND,
shortcuts: {
group: 'shortcuts',
icon: BxIcon.COMMAND,
items: this.TAB_SHORTCUTS_ITEMS,
lazyContent: true,
requiredVariants: 'full',
},
{
icon: BxIcon.STREAM_STATS,
stats: {
group: 'stats',
icon: BxIcon.STREAM_STATS,
items: this.TAB_STATS_ITEMS,
},
];
};
constructor() {
private constructor() {
super();
BxLogger.info(this.LOG_TAG, 'constructor()');
this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn;
this.setupDialog();
@@ -653,7 +662,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
// Trigger event
const $selectUserAgent = document.querySelector(`#bx_setting_${PrefKey.USER_AGENT_PROFILE}`) as HTMLSelectElement;
const $selectUserAgent = document.querySelector<HTMLSelectElement>(`#bx_setting_${PrefKey.USER_AGENT_PROFILE}`);
if ($selectUserAgent) {
$selectUserAgent.disabled = true;
BxEvent.dispatch($selectUserAgent, 'input', {});
@@ -757,8 +766,11 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
// Get labels
for (const settingTab of this.SETTINGS_UI) {
if (!settingTab || !settingTab.items) {
let settingTabGroup: keyof typeof this.SETTINGS_UI;
for (settingTabGroup in this.SETTINGS_UI) {
const settingTab = this.SETTINGS_UI[settingTabGroup];
if (!settingTab || !settingTab.items || typeof settingTab.items === 'function') {
continue;
}
@@ -901,7 +913,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
let prefKey: PrefKey;
for (prefKey in settings) {
const suggestedValue = settings[prefKey];
const $checkBox = $content.querySelector(`#bx_suggest_${prefKey}`) as HTMLInputElement;
const $checkBox = $content.querySelector<HTMLInputElement>(`#bx_suggest_${prefKey}`)!;
if (!$checkBox.checked || $checkBox.disabled) {
continue;
}
@@ -961,36 +973,57 @@ export class SettingsNavigationDialog extends NavigationDialog {
}, t('suggest-settings-link')),
);
$btnSuggest?.insertAdjacentElement('afterend', $content);
$btnSuggest.insertAdjacentElement('afterend', $content);
}
private onTabClicked(e: Event) {
const $svg = (e.target as SVGElement).closest('svg')!;
// Render tab content lazily
if (!!$svg.dataset.lazy) {
// Remove attribute
delete $svg.dataset.lazy;
// Render data
const settingTab = this.SETTINGS_UI[$svg.dataset.group as SettingTabGroup];
const items = (settingTab.items as Function)();
const $tabContent = this.renderTabContent.call(this, settingTab, items);
this.$tabContents.appendChild($tabContent);
}
// Switch tab
let $child: HTMLElement;
const children = Array.from(this.$tabContents.children) as HTMLElement[];
for ($child of children) {
if ($child.dataset.tabGroup === $svg.dataset.group) {
// Show tab content
$child.classList.remove('bx-gone');
// Calculate size of controller-friendly select boxes
if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
this.dialogManager.calculateSelectBoxes($child as HTMLElement);
}
} else {
// Hide tab content
$child.classList.add('bx-gone');
}
}
// Highlight current tab button
for (const $child of Array.from(this.$tabs.children)) {
$child.classList.remove('bx-active');
}
$svg.classList.add('bx-active');
}
private renderTab(settingTab: SettingTab) {
const $svg = createSvgIcon(settingTab.icon as any);
$svg.dataset.group = settingTab.group;
$svg.tabIndex = 0;
settingTab.lazyContent && ($svg.dataset.lazy = settingTab.lazyContent.toString());
$svg.addEventListener('click', e => {
// Switch tab
for (const $child of Array.from(this.$settings.children)) {
if ($child.getAttribute('data-tab-group') === settingTab.group) {
$child.classList.remove('bx-gone');
// Calculate size of controller-friendly select boxes
if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
this.dialogManager.calculateSelectBoxes($child as HTMLElement);
}
} else {
$child.classList.add('bx-gone');
}
}
// Highlight current tab button
for (const $child of Array.from(this.$tabs.children)) {
$child.classList.remove('bx-active');
}
$svg.classList.add('bx-active');
});
$svg.addEventListener('click', this.onTabClicked.bind(this));
return $svg;
}
@@ -1137,10 +1170,19 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
let label = prefDefinition?.label || setting.label;
let note = prefDefinition?.note || setting.note;
let unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote;
let note: string | undefined | (() => HTMLElement) | HTMLElement = prefDefinition?.note || setting.note;
let unsupportedNote: string | undefined | (() => HTMLElement) | HTMLElement = prefDefinition?.unsupportedNote || setting.unsupportedNote;
const experimental = prefDefinition?.experimental || setting.experimental;
// Render note lazily
if (typeof note === 'function') {
note = note();
}
if (typeof unsupportedNote === 'function') {
unsupportedNote = unsupportedNote();
}
if (settingTabContent.label && setting.pref) {
if (prefDefinition?.suggest) {
typeof prefDefinition.suggest.lowest !== 'undefined' && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest);
@@ -1195,9 +1237,101 @@ export class SettingsNavigationDialog extends NavigationDialog {
!prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control);
}
private renderTabContent(settingTab: SettingTab, items: Array<SettingTabContent | false>): HTMLElement {
const $tabContent = CE('div', {
class: 'bx-gone',
'data-tab-group': settingTab.group,
});
for (const settingTabContent of items) {
if (!settingTabContent) {
continue;
}
if (!this.isSupportedVariant(settingTabContent.requiredVariants)) {
continue;
}
// Don't render other settings in unsupported regions
if (!this.renderFullSettings && settingTab.group === 'global' && settingTabContent.group !== 'general' && settingTabContent.group !== 'footer') {
continue;
}
let label = settingTabContent.label;
// If label is "Better xCloud" => create a link to Releases page
if (label === t('better-xcloud')) {
label += ' ' + SCRIPT_VERSION;
if (SCRIPT_VARIANT === 'lite') {
label += ' (Lite)';
}
label = createButton({
label: label,
url: 'https://github.com/redphx/better-xcloud/releases',
style: ButtonStyle.NORMAL_CASE | ButtonStyle.FROSTED | ButtonStyle.FOCUSABLE,
});
}
if (label) {
const $title = CE('h2', {
_nearby: {
orientation: 'horizontal',
}
},
CE('span', {}, label),
settingTabContent.helpUrl && createButton({
icon: BxIcon.QUESTION,
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
url: settingTabContent.helpUrl,
title: t('help'),
}),
);
$tabContent.appendChild($title);
}
// Add note
if (settingTabContent.unsupportedNote) {
const $note = CE('b', {class: 'bx-note-unsupported'}, settingTabContent.unsupportedNote);
$tabContent.appendChild($note);
}
// Don't render settings if this is an unsupported feature
if (settingTabContent.unsupported) {
continue;
}
// Add content DOM
if (settingTabContent.content) {
$tabContent.appendChild(settingTabContent.content);
continue;
}
// Render list of settings
settingTabContent.items = settingTabContent.items || [];
for (const setting of settingTabContent.items) {
if (setting === false) {
continue;
}
if (typeof setting === 'function') {
setting.apply(this, [$tabContent]);
continue;
}
this.renderSettingRow(settingTab, $tabContent, settingTabContent, setting);
}
}
return $tabContent;
}
private setupDialog() {
let $tabs: HTMLElement;
let $settings: HTMLElement;
let $tabContents: HTMLElement;
const $container = CE('div', {
class: 'bx-settings-dialog',
@@ -1245,7 +1379,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
),
),
$settings = CE('div', {
$tabContents = CE('div', {
class: 'bx-settings-tab-contents',
_nearby: {
orientation: 'vertical',
@@ -1264,7 +1398,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
this.$container = $container;
this.$tabs = $tabs;
this.$settings = $settings;
this.$tabContents = $tabContents;
// Close dialog when not clicking on any child elements in the dialog
$container.addEventListener('click', e => {
@@ -1275,7 +1409,10 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
});
for (const settingTab of this.SETTINGS_UI) {
let settingTabGroup: keyof typeof this.SETTINGS_UI
for (settingTabGroup in this.SETTINGS_UI) {
const settingTab = this.SETTINGS_UI[settingTabGroup];
if (!settingTab) {
continue;
}
@@ -1293,95 +1430,13 @@ export class SettingsNavigationDialog extends NavigationDialog {
const $svg = this.renderTab(settingTab);
$tabs.appendChild($svg);
const $tabContent = CE('div', {
class: 'bx-gone',
'data-tab-group': settingTab.group,
});
for (const settingTabContent of settingTab.items) {
if (settingTabContent === false) {
continue;
}
if (!this.isSupportedVariant(settingTabContent.requiredVariants)) {
continue;
}
// Don't render other settings in unsupported regions
if (!this.renderFullSettings && settingTab.group === 'global' && settingTabContent.group !== 'general' && settingTabContent.group !== 'footer') {
continue;
}
let label = settingTabContent.label;
// If label is "Better xCloud" => create a link to Releases page
if (label === t('better-xcloud')) {
label += ' ' + SCRIPT_VERSION;
if (SCRIPT_VARIANT === 'lite') {
label += ' (Lite)';
}
label = createButton({
label: label,
url: 'https://github.com/redphx/better-xcloud/releases',
style: ButtonStyle.NORMAL_CASE | ButtonStyle.FROSTED | ButtonStyle.FOCUSABLE,
});
}
if (label) {
const $title = CE('h2', {
_nearby: {
orientation: 'horizontal',
}
},
CE('span', {}, label),
settingTabContent.helpUrl && createButton({
icon: BxIcon.QUESTION,
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
url: settingTabContent.helpUrl,
title: t('help'),
}),
);
$tabContent.appendChild($title);
}
// Add note
if (settingTabContent.unsupportedNote) {
const $note = CE('b', {class: 'bx-note-unsupported'}, settingTabContent.unsupportedNote);
$tabContent.appendChild($note);
}
// Don't render settings if this is an unsupported feature
if (settingTabContent.unsupported) {
continue;
}
// Add content DOM
if (settingTabContent.content) {
$tabContent.appendChild(settingTabContent.content);
continue;
}
// Render list of settings
settingTabContent.items = settingTabContent.items || [];
for (const setting of settingTabContent.items) {
if (setting === false) {
continue;
}
if (typeof setting === 'function') {
setting.apply(this, [$tabContent]);
continue;
}
this.renderSettingRow(settingTab, $tabContent, settingTabContent, setting);
}
// Don't render lazy tab content
if (typeof settingTab.items === 'function') {
continue;
}
$settings.appendChild($tabContent);
const $tabContent = this.renderTabContent.call(this, settingTab, settingTab.items);
$tabContents.appendChild($tabContent);
}
// Select first tab
@@ -1398,13 +1453,13 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
private focusActiveTab() {
const $currentTab = this.$tabs!.querySelector('.bx-active') as HTMLElement;
const $currentTab = this.$tabs!.querySelector<HTMLElement>('.bx-active');
$currentTab && $currentTab.focus();
return true;
}
private focusVisibleSetting(type: 'first' | 'last' = 'first'): boolean {
const controls = Array.from(this.$settings.querySelectorAll('div[data-tab-group]:not(.bx-gone) > *'));
const controls = Array.from(this.$tabContents.querySelectorAll('div[data-tab-group]:not(.bx-gone) > *'));
if (!controls.length) {
return false;
}
@@ -1450,7 +1505,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
private jumpToSettingGroup(direction: 'next' | 'previous'): boolean {
const $tabContent = this.$settings.querySelector('div[data-tab-group]:not(.bx-gone)');
const $tabContent = this.$tabContents.querySelector('div[data-tab-group]:not(.bx-gone)');
if (!$tabContent) {
return false;
}
@@ -1461,7 +1516,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
$header = $tabContent.querySelector('h2');
} else {
// Find the parent element
const $parent = $focusing.closest('[data-tab-group] > *') as HTMLElement;
const $parent = $focusing.closest<HTMLElement>('[data-tab-group] > *');
const siblingProperty = direction === 'next' ? 'nextSibling' : 'previousSibling';
let $tmp = $parent;

View File

@@ -1,12 +1,15 @@
import { BxLogger } from "@/utils/bx-logger";
import { CE } from "@/utils/html";
export class FullscreenText {
private static instance: FullscreenText;
public static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText());
private readonly LOG_TAG = 'FullscreenText';
$text: HTMLElement;
constructor() {
private constructor() {
BxLogger.info(this.LOG_TAG, 'constructor()');
this.$text = CE('div', {
class: 'bx-fullscreen-text bx-gone',
});

View File

@@ -13,101 +13,104 @@ export enum GuideMenuTab {
}
export class GuideMenu {
static #BUTTONS = {
scriptSettings: createButton({
label: t('better-xcloud'),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY,
onClick: e => {
// Wait until the Guide dialog is closed
window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, e => {
setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50);
}, {once: true});
private static instance: GuideMenu;
public static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu());
// Close all xCloud's dialogs
GuideMenu.#closeGuideMenu();
},
}),
private $renderedButtons?: HTMLElement;
closeApp: AppInterface && createButton({
icon: BxIcon.POWER,
label: t('close-app'),
title: t('close-app'),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.DANGER,
onClick: e => {
AppInterface.closeApp();
},
attributes: {
'data-state': 'normal',
},
}),
reloadPage: createButton({
icon: BxIcon.REFRESH,
label: t('reload-page'),
title: t('reload-page'),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
onClick: e => {
if (STATES.isPlaying) {
confirm(t('confirm-reload-stream')) && window.location.reload();
} else {
window.location.reload();
}
// Close all xCloud's dialogs
GuideMenu.#closeGuideMenu();
},
}),
backToHome: createButton({
icon: BxIcon.HOME,
label: t('back-to-home'),
title: t('back-to-home'),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
onClick: e => {
confirm(t('back-to-home-confirm')) && (window.location.href = window.location.href.substring(0, 31));
// Close all xCloud's dialogs
GuideMenu.#closeGuideMenu();
},
attributes: {
'data-state': 'playing',
},
}),
}
static #$renderedButtons: HTMLElement;
static #closeGuideMenu() {
closeGuideMenu() {
if (window.BX_EXPOSED.dialogRoutes) {
window.BX_EXPOSED.dialogRoutes.closeAll();
return;
}
// Use alternative method for Lite version
const $btnClose = document.querySelector('#gamepass-dialog-root button[class^=Header-module__closeButton]') as HTMLElement;
const $btnClose = document.querySelector<HTMLElement>('#gamepass-dialog-root button[class^=Header-module__closeButton]');
$btnClose && $btnClose.click();
}
static #renderButtons() {
if (GuideMenu.#$renderedButtons) {
return GuideMenu.#$renderedButtons;
private renderButtons() {
if (this.$renderedButtons) {
return this.$renderedButtons;
}
const buttons = {
scriptSettings: createButton({
label: t('better-xcloud'),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY,
onClick: (() => {
// Wait until the Guide dialog is closed
window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, e => {
setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50);
}, {once: true});
// Close all xCloud's dialogs
this.closeGuideMenu();
}).bind(this),
}),
closeApp: AppInterface && createButton({
icon: BxIcon.POWER,
label: t('close-app'),
title: t('close-app'),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.DANGER,
onClick: e => {
AppInterface.closeApp();
},
attributes: {
'data-state': 'normal',
},
}),
reloadPage: createButton({
icon: BxIcon.REFRESH,
label: t('reload-page'),
title: t('reload-page'),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
onClick: (() => {
// Close all xCloud's dialogs
this.closeGuideMenu();
if (STATES.isPlaying) {
confirm(t('confirm-reload-stream')) && window.location.reload();
} else {
window.location.reload();
}
}).bind(this),
}),
backToHome: createButton({
icon: BxIcon.HOME,
label: t('back-to-home'),
title: t('back-to-home'),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
onClick: (() => {
// Close all xCloud's dialogs
this.closeGuideMenu();
confirm(t('back-to-home-confirm')) && (window.location.href = window.location.href.substring(0, 31));
}).bind(this),
attributes: {
'data-state': 'playing',
},
}),
};
const buttonsLayout = [
buttons.scriptSettings,
[
buttons.backToHome,
buttons.reloadPage,
buttons.closeApp,
],
];
const $div = CE('div', {
class: 'bx-guide-home-buttons',
});
const buttons = [
GuideMenu.#BUTTONS.scriptSettings,
[
GuideMenu.#BUTTONS.backToHome,
GuideMenu.#BUTTONS.reloadPage,
GuideMenu.#BUTTONS.closeApp,
],
];
for (const $button of buttons) {
for (const $button of buttonsLayout) {
if (!$button) {
continue;
}
@@ -123,15 +126,15 @@ export class GuideMenu {
}
}
GuideMenu.#$renderedButtons = $div;
this.$renderedButtons = $div;
return $div;
}
static #injectHome($root: HTMLElement, isPlaying = false) {
injectHome($root: HTMLElement, isPlaying = false) {
if (isFullVersion()) {
const $achievementsProgress = $root.querySelector('button[class*=AchievementsButton-module__progressBarContainer]');
if ($achievementsProgress) {
TrueAchievements.injectAchievementsProgress($achievementsProgress as HTMLElement);
TrueAchievements.getInstance().injectAchievementsProgress($achievementsProgress as HTMLElement);
}
}
@@ -142,7 +145,7 @@ export class GuideMenu {
$target = $root.querySelector('a[class*=QuitGameButton]');
// Hide xCloud's Home button
const $btnXcloudHome = $root.querySelector('div[class^=HomeButtonWithDivider]') as HTMLElement;
const $btnXcloudHome = $root.querySelector<HTMLElement>('div[class^=HomeButtonWithDivider]');
$btnXcloudHome && ($btnXcloudHome.style.display = 'none');
} else {
// Last divider
@@ -156,29 +159,30 @@ export class GuideMenu {
return false;
}
const $buttons = GuideMenu.#renderButtons();
const $buttons = this.renderButtons();
$buttons.dataset.isPlaying = isPlaying.toString();
$target.insertAdjacentElement('afterend', $buttons);
}
static async #onShown(e: Event) {
async onShown(e: Event) {
const where = (e as any).where as GuideMenuTab;
if (where === GuideMenuTab.HOME) {
const $root = document.querySelector('#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]') as HTMLElement;
$root && GuideMenu.#injectHome($root, STATES.isPlaying);
const $root = document.querySelector<HTMLElement>('#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]');
$root && this.injectHome($root, STATES.isPlaying);
}
}
static addEventListeners() {
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown);
addEventListeners() {
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown.bind(this));
}
static observe($addedElm: HTMLElement) {
observe($addedElm: HTMLElement) {
const className = $addedElm.className;
// TrueAchievements
if (isFullVersion() && className.includes('AchievementsButton-module__progressBarContainer')) {
TrueAchievements.injectAchievementsProgress($addedElm);
TrueAchievements.getInstance().injectAchievementsProgress($addedElm);
return;
}
@@ -192,7 +196,7 @@ export class GuideMenu {
if (isFullVersion()) {
const $achievDetailPage = $addedElm.querySelector('div[class*=AchievementDetailPage]');
if ($achievDetailPage) {
TrueAchievements.injectAchievementDetailPage($achievDetailPage as HTMLElement);
TrueAchievements.getInstance().injectAchievementDetailPage($achievDetailPage as HTMLElement);
return;
}
}

View File

@@ -7,36 +7,45 @@ import { t } from "@utils/translation";
import { SettingsNavigationDialog } from "./dialog/settings-dialog";
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { BxLogger } from "@/utils/bx-logger";
export class HeaderSection {
static #$remotePlayBtn = createButton({
classes: ['bx-header-remote-play-button', 'bx-gone'],
icon: BxIcon.REMOTE_PLAY,
title: t('remote-play'),
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR,
onClick: e => {
RemotePlayManager.getInstance().togglePopup();
},
});
private static instance: HeaderSection;
public static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection());
private readonly LOG_TAG = 'HeaderSection';
static #$settingsBtn = createButton({
classes: ['bx-header-settings-button'],
label: '???',
style: ButtonStyle.FROSTED | ButtonStyle.DROP_SHADOW | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_HEIGHT,
onClick: e => {
SettingsNavigationDialog.getInstance().show();
},
});
private $btnRemotePlay: HTMLElement;
private $btnSettings: HTMLElement;
private $buttonsWrapper: HTMLElement;
static #$buttonsWrapper = CE('div', {},
getPref(PrefKey.REMOTE_PLAY_ENABLED) ? HeaderSection.#$remotePlayBtn : null,
HeaderSection.#$settingsBtn,
);
private observer?: MutationObserver;
private timeoutId?: number | null;
static #observer: MutationObserver;
static #timeout: number | null;
constructor() {
BxLogger.info(this.LOG_TAG, 'constructor()');
static #injectSettingsButton($parent?: HTMLElement) {
this.$btnRemotePlay = createButton({
classes: ['bx-header-remote-play-button', 'bx-gone'],
icon: BxIcon.REMOTE_PLAY,
title: t('remote-play'),
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR,
onClick: e => RemotePlayManager.getInstance().togglePopup(),
});
this.$btnSettings = createButton({
classes: ['bx-header-settings-button'],
label: '???',
style: ButtonStyle.FROSTED | ButtonStyle.DROP_SHADOW | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_HEIGHT,
onClick: e => SettingsNavigationDialog.getInstance().show(),
});
this.$buttonsWrapper = CE('div', {},
getPref(PrefKey.REMOTE_PLAY_ENABLED) ? this.$btnRemotePlay : null,
this.$btnSettings,
);
}
private injectSettingsButton($parent?: HTMLElement) {
if (!$parent) {
return;
}
@@ -44,8 +53,8 @@ export class HeaderSection {
const PREF_LATEST_VERSION = getPref(PrefKey.LATEST_VERSION);
// Setup Settings button
const $btnSettings = HeaderSection.#$settingsBtn;
if (isElementVisible(HeaderSection.#$buttonsWrapper)) {
const $btnSettings = this.$btnSettings;
if (isElementVisible(this.$buttonsWrapper)) {
return;
}
@@ -57,38 +66,42 @@ export class HeaderSection {
}
// Add the Settings button to the web page
$parent.appendChild(HeaderSection.#$buttonsWrapper);
$parent.appendChild(this.$buttonsWrapper);
}
static checkHeader() {
private checkHeader() {
let $target = document.querySelector('#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]');
if (!$target) {
$target = document.querySelector('div[class^=UnsupportedMarketPage-module__buttons]');
}
$target && HeaderSection.#injectSettingsButton($target as HTMLElement);
$target && this.injectSettingsButton($target as HTMLElement);
}
static showRemotePlayButton() {
HeaderSection.#$remotePlayBtn.classList.remove('bx-gone');
}
static watchHeader() {
private watchHeader() {
const $root = document.querySelector('#PageContent header') || document.querySelector('#root');
if (!$root) {
return;
}
HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout);
HeaderSection.#timeout = null;
this.timeoutId && clearTimeout(this.timeoutId);
this.timeoutId = null;
HeaderSection.#observer && HeaderSection.#observer.disconnect();
HeaderSection.#observer = new MutationObserver(mutationList => {
HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout);
HeaderSection.#timeout = window.setTimeout(HeaderSection.checkHeader, 2000);
this.observer && this.observer.disconnect();
this.observer = new MutationObserver(mutationList => {
this.timeoutId && clearTimeout(this.timeoutId);
this.timeoutId = window.setTimeout(this.checkHeader.bind(this), 2000);
});
HeaderSection.#observer.observe($root, {subtree: true, childList: true});
this.observer.observe($root, {subtree: true, childList: true});
HeaderSection.checkHeader();
this.checkHeader();
}
showRemotePlayButton() {
this.$btnRemotePlay.classList.remove('bx-gone');
}
static watchHeader() {
HeaderSection.getInstance().watchHeader();
}
}