diff --git a/src/assets/css/button.styl b/src/assets/css/button.styl index e203f10..a63e686 100644 --- a/src/assets/css/button.styl +++ b/src/assets/css/button.styl @@ -69,7 +69,7 @@ height: var(--bx-button-height); &:not(:only-child) { - margin-right: 4px; + margin-right: 10px; } } @@ -117,3 +117,7 @@ button.bx-inactive { opacity: 0.2; background: transparent !important; } + +.bx-button-shortcut { + margin-top: 10px; +} diff --git a/src/assets/css/root.styl b/src/assets/css/root.styl index f6daa1a..0d2ce1e 100644 --- a/src/assets/css/root.styl +++ b/src/assets/css/root.styl @@ -5,7 +5,7 @@ --bx-monospaced-font: Consolas, "Courier New", Courier, monospace; --bx-promptfont-font: promptfont; - --bx-button-height: 36px; + --bx-button-height: 40px; --bx-default-button-color: #2d3036; --bx-default-button-hover-color: #515863; diff --git a/src/assets/svg/create-shortcut.svg b/src/assets/svg/create-shortcut.svg new file mode 100644 index 0000000..fe5dd0d --- /dev/null +++ b/src/assets/svg/create-shortcut.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/index.ts b/src/index.ts index f5ce570..00d63cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ import { updateVideoPlayer } from "./modules/stream/stream-settings-utils"; import { UiSection } from "./enums/ui-sections"; import { HeaderSection } from "./modules/ui/header"; import { GameTile } from "./modules/ui/game-tile"; +import { ProductDetailsPage } from "./modules/ui/product-details"; // Handle login page @@ -198,6 +199,13 @@ window.addEventListener(BxEvent.STREAM_ERROR_PAGE, e => { BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); }); +window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, e => { + const component = (e as any).component; + if (component === 'product-details') { + ProductDetailsPage.injectShortcutButton(); + } +}); + function unload() { if (!STATES.isPlaying) { return; diff --git a/src/modules/patcher.ts b/src/modules/patcher.ts index 78801f3..060c6c3 100644 --- a/src/modules/patcher.ts +++ b/src/modules/patcher.ts @@ -1,4 +1,4 @@ -import { SCRIPT_VERSION, STATES } from "@utils/global"; +import { AppInterface, SCRIPT_VERSION, STATES } from "@utils/global"; import { BX_FLAGS } from "@utils/bx-flags"; import { getPref, PrefKey } from "@utils/preferences"; import { VibrationManager } from "@modules/vibration-manager"; @@ -799,6 +799,22 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { str = str.substring(0, index) + codeSetCurrentlyFocusedInteractable + str.substring(index); return str; }, + + // product-details-page.js#2388, 24.17.20 + detectProductDetailsPage(str: string) { + let index = str.indexOf('{location:"ProductDetailPage",'); + if (index === -1) { + return false; + } + + index = str.indexOf('return', index - 40); + if (index === -1) { + return false; + } + + str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, {component: "product-details"});' + str.substring(index); + return str; + }, }; let PATCH_ORDERS: PatchArray = [ @@ -820,6 +836,7 @@ let PATCH_ORDERS: PatchArray = [ 'exposeDialogRoutes', 'enableTvRoutes', + AppInterface && 'detectProductDetailsPage', 'overrideStorageGetSettings', getPref(PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME) && 'patchSetCurrentlyFocusedInteractable', diff --git a/src/modules/ui/product-details.ts b/src/modules/ui/product-details.ts new file mode 100644 index 0000000..84d2b37 --- /dev/null +++ b/src/modules/ui/product-details.ts @@ -0,0 +1,36 @@ +import { BX_FLAGS } from "@/utils/bx-flags"; +import { BxIcon } from "@/utils/bx-icon"; +import { AppInterface } from "@/utils/global"; +import { ButtonStyle, createButton } from "@/utils/html"; +import { t } from "@/utils/translation"; + +export class ProductDetailsPage { + private static $btnShortcut = createButton({ + classes: ['bx-button-shortcut'], + icon: BxIcon.CREATE_SHORTCUT, + label: t('create-shortcut'), + style: ButtonStyle.FOCUSABLE, + tabIndex: 0, + onClick: e => { + AppInterface && AppInterface.createShortcut(window.location.pathname.substring(6)); + }, + }); + + private static shortcutTimeoutId: number | null = null; + + static injectShortcutButton() { + if (!AppInterface || BX_FLAGS.DeviceInfo?.deviceType !== 'android') { + return; + } + + ProductDetailsPage.shortcutTimeoutId && clearTimeout(ProductDetailsPage.shortcutTimeoutId); + ProductDetailsPage.shortcutTimeoutId = window.setTimeout(() => { + // Find action buttons container + const $container = document.querySelector('div[class*=ActionButtons-module__container]'); + if ($container) { + this.$btnShortcut.style.width = $container.getBoundingClientRect().width + 'px'; + $container.parentElement?.appendChild(ProductDetailsPage.$btnShortcut); + } + }, 500); + } +} diff --git a/src/utils/bx-event.ts b/src/utils/bx-event.ts index 97f7493..d629c6a 100644 --- a/src/utils/bx-event.ts +++ b/src/utils/bx-event.ts @@ -48,6 +48,8 @@ export enum BxEvent { XCLOUD_GUIDE_MENU_SHOWN = 'bx-xcloud-guide-menu-shown', XCLOUD_POLLING_MODE_CHANGED = 'bx-xcloud-polling-mode-changed', + + XCLOUD_RENDERING_COMPONENT = 'bx-xcloud-rendering-page', } export enum XcloudEvent { diff --git a/src/utils/bx-icon.ts b/src/utils/bx-icon.ts index 63b489f..f32ca45 100644 --- a/src/utils/bx-icon.ts +++ b/src/utils/bx-icon.ts @@ -1,6 +1,7 @@ import iconCommand from "@assets/svg/command.svg" with { type: "text" }; import iconController from "@assets/svg/controller.svg" with { type: "text" }; import iconCopy from "@assets/svg/copy.svg" with { type: "text" }; +import iconCreateShortcut from "@assets/svg/create-shortcut.svg" with { type: "text" }; import iconCursorText from "@assets/svg/cursor-text.svg" with { type: "text" }; import iconDisplay from "@assets/svg/display.svg" with { type: "text" }; import iconHome from "@assets/svg/home.svg" with { type: "text" }; @@ -11,9 +12,9 @@ import iconRefresh from "@assets/svg/refresh.svg" with { type: "text" }; import iconRemotePlay from "@assets/svg/remote-play.svg" with { type: "text" }; import iconStreamSettings from "@assets/svg/stream-settings.svg" with { type: "text" }; import iconStreamStats from "@assets/svg/stream-stats.svg" with { type: "text" }; -import iconTrash from "@assets/svg/trash.svg" with { type: "text" }; -import iconTouchControlEnable from "@assets/svg/touch-control-enable.svg" with { type: "text" }; import iconTouchControlDisable from "@assets/svg/touch-control-disable.svg" with { type: "text" }; +import iconTouchControlEnable from "@assets/svg/touch-control-enable.svg" with { type: "text" }; +import iconTrash from "@assets/svg/trash.svg" with { type: "text" }; import iconVirtualController from "@assets/svg/virtual-controller.svg" with { type: "text" }; // Game Bar @@ -37,6 +38,7 @@ export const BxIcon = { STREAM_STATS: iconStreamStats, COMMAND: iconCommand, CONTROLLER: iconController, + CREATE_SHORTCUT: iconCreateShortcut, DISPLAY: iconDisplay, HOME: iconHome, NATIVE_MKB: iconNativeMkb, diff --git a/src/utils/translation.ts b/src/utils/translation.ts index 83f02d2..dac4e12 100644 --- a/src/utils/translation.ts +++ b/src/utils/translation.ts @@ -76,6 +76,7 @@ const Texts = { "controller-shortcuts-xbox-note": "Button to open the Guide menu", "controller-vibration": "Controller vibration", "copy": "Copy", + "create-shortcut": "Create shortcut", "custom": "Custom", "deadzone-counterweight": "Deadzone counterweight", "decrease": "Decrease",