From 03efa528c86c835a263c38195566cb3571567784 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Sat, 19 Oct 2024 16:53:55 +0700 Subject: [PATCH] Android: add Shortcut & Wallpaper menu to Game Card's context menu --- src/index.ts | 52 +------------- src/modules/ui/product-details.ts | 15 ++-- src/utils/root-dialog-observer.ts | 114 ++++++++++++++++++++++++++++++ src/utils/utils.ts | 12 ++++ 4 files changed, 132 insertions(+), 61 deletions(-) create mode 100644 src/utils/root-dialog-observer.ts diff --git a/src/index.ts b/src/index.ts index 8d9d053..bab30c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,6 +42,7 @@ import { StreamUiHandler } from "./modules/stream/stream-ui"; import { UserAgent } from "./utils/user-agent"; import { XboxApi } from "./utils/xbox-api"; import { StreamStatsCollector } from "./utils/stream-stats-collector"; +import { RootDialogObserver } from "./utils/root-dialog-observer"; // Handle login page if (window.location.pathname.includes('/auth/msa')) { @@ -328,55 +329,6 @@ isFullVersion() && window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, e => { }); -function observeRootDialog($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 && $addedElm.className) { - // Make sure it's Guide dialog - if ($root.querySelector('div[class*=GuideDialog]')) { - GuideMenu.observe($addedElm); - } - } - } - - const shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0); - if (shown !== beingShown) { - beingShown = shown; - BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED); - } - } - }); - observer.observe($root, {subtree: true, childList: true}); -} - -function 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(); - observeRootDialog($target); - break; - } - }; - }); - observer.observe(document.documentElement, {subtree: true, childList: true}); -} - - function main() { if (getPref(PrefKey.GAME_MSFS2020_FORCE_NATIVE_MKB)) { BX_FLAGS.ForceNativeMkbTitles.push('9PMQDM08SNK9'); @@ -397,7 +349,7 @@ function main() { disableAdobeAudienceManager(); } - waitForRootDialog(); + RootDialogObserver.waitForRootDialog(); // Setup UI addCss(); diff --git a/src/modules/ui/product-details.ts b/src/modules/ui/product-details.ts index bcff0a5..4c9a9c2 100644 --- a/src/modules/ui/product-details.ts +++ b/src/modules/ui/product-details.ts @@ -3,6 +3,7 @@ import { BxIcon } from "@/utils/bx-icon"; import { AppInterface } from "@/utils/global"; import { ButtonStyle, CE, createButton } from "@/utils/html"; import { t } from "@/utils/translation"; +import { parseDetailsPath } from "@/utils/utils"; export class ProductDetailsPage { private static $btnShortcut = AppInterface && createButton({ @@ -20,17 +21,9 @@ export class ProductDetailsPage { label: t('wallpaper'), style: ButtonStyle.FOCUSABLE, tabIndex: 0, - onClick: async e => { - try { - const matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(window.location.pathname); - if (!matches?.groups) { - return; - } - - const titleSlug = matches.groups.titleSlug.replaceAll('\%' + '7C', '-'); - const productId = matches.groups.productId; - AppInterface.downloadWallpapers(titleSlug, productId); - } catch (e) {} + onClick: e => { + const details = parseDetailsPath(window.location.pathname); + details && AppInterface.downloadWallpapers(details.titleSlug, details.productId); }, }); diff --git a/src/utils/root-dialog-observer.ts b/src/utils/root-dialog-observer.ts new file mode 100644 index 0000000..21487d6 --- /dev/null +++ b/src/utils/root-dialog-observer.ts @@ -0,0 +1,114 @@ +import { GuideMenu } from "@/modules/ui/guide-menu"; +import { BxEvent } from "./bx-event"; +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"; + + +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, + tabIndex: 0, + 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, + tabIndex: 0, + 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('div[class^=MruContextMenu],div[class^=GameCardContextMenu]'); + if ($gameCardMenu) { + RootDialogObserver.handleGameCardMenu($gameCardMenu); + return true; + } + } else if ($root.querySelector('div[class*=GuideDialog]')) { + // Guide menu + GuideMenu.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 && $addedElm.className) { + RootDialogObserver.handleAddedElement($root, $addedElm); + } + } + + const shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0); + if (shown !== beingShown) { + beingShown = shown; + BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_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}); + } +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 6d7b827..effd889 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -120,3 +120,15 @@ export function productTitleToSlug(title: string): string { .replace(/ /g, '-') .toLowerCase(); } + +export function parseDetailsPath(path: string) { + const matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(path); + if (!matches?.groups) { + return; + } + + const titleSlug = matches.groups.titleSlug.replaceAll('\%' + '7C', '-'); + const productId = matches.groups.productId; + + return {titleSlug, productId}; +}