Stop using MutationObserver in root-dialog

This commit is contained in:
redphx
2025-02-07 22:02:58 +07:00
parent 585ee82776
commit 2f8c776133
7 changed files with 100 additions and 291 deletions

View File

@@ -25,6 +25,7 @@ import { BxLogger } from "@utils/bx-logger";
import { GameBar } from "./modules/game-bar/game-bar";
import { ScreenshotManager } from "./utils/screenshot-manager";
import { NativeMkbHandler } from "./modules/mkb/native-mkb-handler";
import { GuideMenu } from "./modules/ui/guide-menu";
import { updateVideoPlayer } from "./modules/stream/stream-settings-utils";
import { BlockFeature, NativeMkbMode, TouchControllerMode, UiSection } from "./enums/pref-values";
import { HeaderSection } from "./modules/ui/header";
@@ -45,8 +46,7 @@ import { SettingsManager } from "./modules/settings-manager";
import { Toast } from "./utils/toast";
import { WebGPUPlayer } from "./modules/player/webgpu/webgpu-player";
import { StreamUiHandler } from "./modules/stream/stream-ui";
import { RootDialogObserver } from "./utils/root-dialog-observer";
import { GuideMenu } from "./modules/ui/guide-menu";
import { TrueAchievements } from "./utils/true-achievements";
SettingsManager.getInstance();
@@ -264,6 +264,25 @@ BxEventBus.Script.on('ui.error.rendered', () => {
BxEventBus.Stream.emit('state.stopped', {});
});
BxEventBus.Script.on('ui.guideHome.rendered', () => {
const $root = document.querySelector<HTMLElement>('#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]');
$root && GuideMenu.getInstance().injectHome($root, STATES.isPlaying);
});
BxEventBus.Script.on('ui.guideAchievementProgress.rendered', () => {
const $elm = document.querySelector('#gamepass-dialog-root button[class*=AchievementsButton-module__progressBarContainer]');
if ($elm) {
TrueAchievements.getInstance().injectAchievementsProgress($elm as HTMLElement);
}
});
BxEventBus.Script.on('ui.guideAchievementDetail.rendered', () => {
const $elm = document.querySelector('#gamepass-dialog-root div[class^=AchievementDetailPage-module]');
if ($elm) {
TrueAchievements.getInstance().injectAchievementDetailPage($elm as HTMLElement);
}
});
BxEventBus.Stream.on('ui.streamMenu.rendered', async () => {
await StreamUiHandler.handleStreamMenu();
});
@@ -403,12 +422,9 @@ function main() {
disableAdobeAudienceManager();
}
RootDialogObserver.waitForRootDialog();
// Setup UI
addCss();
GuideMenu.getInstance().addEventListeners();
StreamStatsCollector.setupEvents();
StreamBadges.setupEvents();
StreamStats.setupEvents();

View File

@@ -1164,6 +1164,16 @@ ${subsVar} = subs;
return PatcherUtils.injectUseEffect(str, index, 'Stream', 'ui.streamMenu.rendered');
},
injectGuideHomeUseEffect(str: string) {
let index = str.indexOf('"HomeLandingPage-module__authenticatedContentContainer');
index > -1 && (index = PatcherUtils.lastIndexOf(str, 'return', index, 200));
if (index < 0) {
return false;
}
return PatcherUtils.injectUseEffect(str, index, 'Script', 'ui.guideHome.rendered');
},
injectCreatePortal(str: string) {
let index = str.indexOf('.createPortal=function');
index > -1 && (index = PatcherUtils.indexOf(str, '{', index, 50, true));
@@ -1174,6 +1184,26 @@ ${subsVar} = subs;
str = PatcherUtils.insertAt(str, index, codeCreatePortal);
return str;
},
injectAchievementsProgressUseEffect(str: string) {
let index = str.indexOf('"AchievementsButton-module__progressBarContainer');
index > -1 && (index = PatcherUtils.lastIndexOf(str, 'return', index, 200));
if (index < 0) {
return false;
}
return PatcherUtils.injectUseEffect(str, index, 'Script', 'ui.guideAchievementProgress.rendered');
},
injectAchievementsDetailUseEffect(str: string) {
let index = str.indexOf('GuideAchievementDetail.useParams()');
index > -1 && (index = PatcherUtils.lastIndexOf(str, 'const', index, 200));
if (index < 0) {
return false;
}
return PatcherUtils.injectUseEffect(str, index, 'Script', 'ui.guideAchievementDetail.rendered');
},
};
let PATCH_ORDERS = PatcherUtils.filterPatches([
@@ -1185,8 +1215,11 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([
'exposeReactCreateComponent',
'injectCreatePortal',
'injectGuideHomeUseEffect',
'injectHeaderUseEffect',
'injectErrorPageUseEffect',
'injectAchievementsProgressUseEffect',
'injectAchievementsDetailUseEffect',
'gameCardCustomIcons',
// 'gameCardPassTitle',

View File

@@ -1,11 +1,7 @@
import { isFullVersion } from "@macros/build" with { type: "macro" };
import { BxEvent } from "@/utils/bx-event";
import { AppInterface, STATES } from "@/utils/global";
import { createButton, ButtonStyle, CE } from "@/utils/html";
import { t } from "@/utils/translation";
import { SettingsDialog } from "./dialog/settings-dialog";
import { TrueAchievements } from "@/utils/true-achievements";
import { BxIcon } from "@/utils/bx-icon";
import { BxEventBus } from "@/utils/bx-event-bus";
import { getGlobalPref } from "@/utils/pref-utils";
@@ -141,11 +137,9 @@ export class GuideMenu {
}
injectHome($root: HTMLElement, isPlaying = false) {
if (isFullVersion()) {
const $achievementsProgress = $root.querySelector('button[class*=AchievementsButton-module__progressBarContainer]');
if ($achievementsProgress) {
TrueAchievements.getInstance().injectAchievementsProgress($achievementsProgress as HTMLElement);
}
const $buttons = this.renderButtons();
if ($root.contains($buttons)) {
return;
}
// Find the element to add buttons to
@@ -169,67 +163,7 @@ export class GuideMenu {
return false;
}
const $buttons = this.renderButtons();
$buttons.dataset.isPlaying = isPlaying.toString();
$target.insertAdjacentElement('afterend', $buttons);
}
private onShown = async (e: Event) => {
const where = (e as any).where as GuideMenuTab;
if (where === GuideMenuTab.HOME) {
const $root = document.querySelector<HTMLElement>('#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]');
$root && this.injectHome($root, STATES.isPlaying);
}
}
addEventListeners() {
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown);
}
observe($addedElm: HTMLElement) {
let className = $addedElm.className;
// Fix custom buttons disappearing in Guide Menu (#551)
if (!className) {
className = $addedElm.firstElementChild?.className ?? '';
}
if (!className || className.startsWith('bx-')) {
return;
}
// TrueAchievements
if (isFullVersion() && className.includes('AchievementsButton-module__progressBarContainer')) {
TrueAchievements.getInstance().injectAchievementsProgress($addedElm);
return;
}
if (!className.startsWith('NavigationAnimation') &&
!className.startsWith('DialogRoutes') &&
!className.startsWith('Dialog-module__container')) {
return;
}
// Achievement Details page
if (isFullVersion()) {
const $achievDetailPage = $addedElm.querySelector('div[class*=AchievementDetailPage]');
if ($achievDetailPage) {
TrueAchievements.getInstance().injectAchievementDetailPage($achievDetailPage as HTMLElement);
return;
}
}
// Find navigation bar
const $selectedTab = $addedElm.querySelector('div[class^=NavigationMenu] button[aria-selected=true');
if ($selectedTab) {
let $elm: Element | null = $selectedTab;
let index;
for (index = 0; ($elm = $elm?.previousElementSibling); index++);
if (index === 0) {
BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: GuideMenuTab.HOME });
}
}
}
}

View File

@@ -38,6 +38,10 @@ type ScriptEvents = {
'ui.header.rendered': {},
'ui.error.rendered': {},
'ui.guideHome.rendered': {},
'ui.guideAchievementProgress.rendered': {},
'ui.guideAchievementDetail.rendered': {},
};
type StreamEvents = {

View File

@@ -1,112 +0,0 @@
import { GuideMenu } from "@/modules/ui/guide-menu";
import { BX_FLAGS } from "./bx-flags";
import { BxLogger } from "./bx-logger";
import { BxIcon } from "./bx-icon";
import { AppInterface } from "./global";
import { createButton, ButtonStyle } from "./html";
import { t } from "./translation";
import { parseDetailsPath } from "./utils";
import { BxEventBus } from "./bx-event-bus";
export class RootDialogObserver {
private static $btnShortcut = AppInterface && createButton({
icon: BxIcon.CREATE_SHORTCUT,
label: t('create-shortcut'),
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_CASE | ButtonStyle.NORMAL_LINK,
onClick: e => {
window.BX_EXPOSED.dialogRoutes?.closeAll();
const $btn = (e.target as HTMLElement).closest('button');
AppInterface.createShortcut($btn?.dataset.path);
},
});
private static $btnWallpaper = AppInterface && createButton({
icon: BxIcon.DOWNLOAD,
label: t('wallpaper'),
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_CASE | ButtonStyle.NORMAL_LINK,
onClick: e => {
window.BX_EXPOSED.dialogRoutes?.closeAll();
const $btn = (e.target as HTMLElement).closest('button');
const details = parseDetailsPath($btn!.dataset.path!);
details && AppInterface.downloadWallpapers(details.titleSlug, details.productId);
},
});
private static handleGameCardMenu($root: HTMLElement) {
const $detail = $root.querySelector('a[href^="/play/"]') as HTMLAnchorElement;
if (!$detail) {
return;
}
const path = $detail.getAttribute('href')!;
RootDialogObserver.$btnShortcut.dataset.path = path;
RootDialogObserver.$btnWallpaper.dataset.path = path;
$root.append(RootDialogObserver.$btnShortcut, RootDialogObserver.$btnWallpaper);
}
private static handleAddedElement($root: HTMLElement, $addedElm: HTMLElement): boolean {
if (AppInterface && $addedElm.className.startsWith('SlideSheet-module__container')) {
// Game card's context menu
const $gameCardMenu = $addedElm.querySelector<HTMLElement>('div[class^=MruContextMenu],div[class^=GameCardContextMenu]');
if ($gameCardMenu) {
RootDialogObserver.handleGameCardMenu($gameCardMenu);
return true;
}
} else if ($root.querySelector('div[class*=GuideDialog]')) {
// Guide menu
GuideMenu.getInstance().observe($addedElm);
return true;
}
return false;
}
private static observe($root: HTMLElement) {
let beingShown = false;
const observer = new MutationObserver(mutationList => {
for (const mutation of mutationList) {
if (mutation.type !== 'childList') {
continue;
}
BX_FLAGS.Debug && BxLogger.warning('RootDialog', 'added', mutation.addedNodes);
if (mutation.addedNodes.length === 1) {
const $addedElm = mutation.addedNodes[0];
if ($addedElm instanceof HTMLElement) {
RootDialogObserver.handleAddedElement($root, $addedElm);
}
}
const shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);
if (shown !== beingShown) {
beingShown = shown;
BxEventBus.Script.emit(shown ? 'dialog.shown' : 'dialog.dismissed', {});
}
}
});
observer.observe($root, { subtree: true, childList: true });
}
public static waitForRootDialog() {
const observer = new MutationObserver(mutationList => {
for (const mutation of mutationList) {
if (mutation.type !== 'childList') {
continue;
}
const $target = mutation.target as HTMLElement;
if ($target.id && $target.id === 'gamepass-dialog-root') {
observer.disconnect();
RootDialogObserver.observe($target);
break;
}
};
});
observer.observe(document.documentElement, { subtree: true, childList: true });
}
}