summaryrefslogtreecommitdiff
path: root/frontend/src/menu-hook.tsx
diff options
context:
space:
mode:
authorAAGaming <aa@mail.catvibers.me>2022-12-31 21:53:39 -0500
committerAAGaming <aa@mail.catvibers.me>2022-12-31 21:53:39 -0500
commitfdbc508fa8cb9ad3629486cacc1c92abb2500794 (patch)
tree06bdb7db18dfcad7b577821cf048dd5f6ecdfc71 /frontend/src/menu-hook.tsx
parent81fbd0f83f11d5074bb945f0f6d7b6508e9d32d7 (diff)
downloaddecky-loader-fdbc508fa8cb9ad3629486cacc1c92abb2500794.tar.gz
decky-loader-fdbc508fa8cb9ad3629486cacc1c92abb2500794.zip
Main menu and overlay patching API
Diffstat (limited to 'frontend/src/menu-hook.tsx')
-rw-r--r--frontend/src/menu-hook.tsx212
1 files changed, 212 insertions, 0 deletions
diff --git a/frontend/src/menu-hook.tsx b/frontend/src/menu-hook.tsx
new file mode 100644
index 00000000..d4bc5410
--- /dev/null
+++ b/frontend/src/menu-hook.tsx
@@ -0,0 +1,212 @@
+import {
+ CustomMainMenuItem,
+ ItemPatch,
+ MainMenuItem,
+ OverlayPatch,
+ afterPatch,
+ findInReactTree,
+ sleep,
+} from 'decky-frontend-lib';
+import { FC } from 'react';
+import { ReactNode, cloneElement, createElement } from 'react';
+
+import { DeckyMenuState, DeckyMenuStateContextProvider, useDeckyMenuState } from './components/DeckyMenuState';
+import Logger from './logger';
+
+declare global {
+ interface Window {
+ __MENU_HOOK_INSTANCE: any;
+ }
+}
+
+class MenuHook extends Logger {
+ private menuRenderer?: any;
+ private originalRenderer?: any;
+ private menuState: DeckyMenuState = new DeckyMenuState();
+
+ constructor() {
+ super('MenuHook');
+
+ this.log('Initialized');
+ window.__MENU_HOOK_INSTANCE?.deinit?.();
+ window.__MENU_HOOK_INSTANCE = this;
+ }
+
+ init() {
+ const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
+ let outerMenuRoot: any;
+ const findMenuRoot = (currentNode: any, iters: number): any => {
+ if (iters >= 60) {
+ // currently 54
+ return null;
+ }
+ if (currentNode?.memoizedProps?.navID == 'MainNavMenuContainer') {
+ this.log(`Menu root was found in ${iters} recursion cycles`);
+ return currentNode;
+ }
+ if (currentNode.child) {
+ let node = findMenuRoot(currentNode.child, iters + 1);
+ if (node !== null) return node;
+ }
+ if (currentNode.sibling) {
+ let node = findMenuRoot(currentNode.sibling, iters + 1);
+ if (node !== null) return node;
+ }
+ return null;
+ };
+
+ (async () => {
+ outerMenuRoot = findMenuRoot(tree, 0);
+ while (!outerMenuRoot) {
+ this.error(
+ 'Failed to find Menu root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.',
+ );
+ await sleep(5000);
+ outerMenuRoot = findMenuRoot(tree, 0);
+ }
+ this.log('found outermenuroot', outerMenuRoot);
+ const menuRenderer = outerMenuRoot.return;
+ this.menuRenderer = menuRenderer;
+ this.originalRenderer = menuRenderer.type;
+ let toReplace = new Map<string, ReactNode>();
+
+ let patchedInnerMenu: any;
+ let overlayComponentManager: any;
+
+ const DeckyOverlayComponentManager = () => {
+ const { overlayComponents } = useDeckyMenuState();
+
+ return <>{overlayComponents.values()}</>;
+ };
+
+ const DeckyInnerMenuWrapper = (props: { innerProps: any }) => {
+ const { overlayPatches } = useDeckyMenuState();
+
+ const rendererRet = this.originalRenderer(props.innerProps);
+
+ // Find the first array of children, this contains [mainmenu, overlay]
+ const childArray = findInReactTree(rendererRet, (x) => x?.[0]?.type);
+
+ // Insert the overlay components manager
+ if (!overlayComponentManager) {
+ overlayComponentManager = <DeckyOverlayComponentManager />;
+ }
+
+ childArray.push(overlayComponentManager);
+
+ // This must be cached in patchedInnerMenu to prevent re-renders
+ if (patchedInnerMenu) {
+ childArray[0].type = patchedInnerMenu;
+ } else {
+ afterPatch(childArray[0], 'type', (menuArgs, ret) => {
+ const { itemPatches, items } = useDeckyMenuState();
+
+ const itemList = ret.props.children;
+
+ // Add custom menu items
+ if (items.size > 0) {
+ const button = findInReactTree(ret.props.children, (x) =>
+ x?.type?.toString()?.includes('exactRouteMatch:'),
+ );
+
+ const MenuItemComponent: FC<MainMenuItem> = button.type;
+
+ items.forEach((item) => {
+ let realIndex = 0; // there are some non-item things in the array
+ let count = 0;
+ itemList.forEach((i: any) => {
+ if (count == item.index) return;
+ if (i?.type == MenuItemComponent) count++;
+ realIndex++;
+ });
+ itemList.splice(realIndex, 0, createElement(MenuItemComponent, item));
+ });
+ }
+
+ // Apply and revert patches
+ itemList.forEach((item: { props: MainMenuItem }, index: number) => {
+ if (!item?.props?.route) return;
+ const replaced = toReplace.get(item?.props?.route as string);
+ if (replaced) {
+ itemList[index] = replaced;
+ toReplace.delete(item?.props.route as string);
+ }
+ if (item?.props?.route && itemPatches.has(item.props.route as string)) {
+ toReplace.set(item?.props?.route as string, itemList[index]);
+ itemPatches.get(item.props.route as string)?.forEach((patch) => {
+ const oType = itemList[index].type;
+ itemList[index] = patch({
+ ...cloneElement(itemList[index]),
+ type: (props) => createElement(oType, props),
+ });
+ });
+ }
+ });
+
+ return ret;
+ });
+ patchedInnerMenu = childArray[0].type;
+ }
+
+ // Apply patches to the overlay
+ if (childArray[1]) {
+ overlayPatches.forEach((patch) => (childArray[1] = patch(childArray[1])));
+ }
+
+ return rendererRet;
+ };
+
+ const DeckyOuterMenuWrapper = (props: any) => {
+ return (
+ <DeckyMenuStateContextProvider deckyMenuState={this.menuState}>
+ <DeckyInnerMenuWrapper innerProps={props} />
+ </DeckyMenuStateContextProvider>
+ );
+ };
+ menuRenderer.type = DeckyOuterMenuWrapper;
+ if (menuRenderer.alternate) {
+ menuRenderer.alternate.type = menuRenderer.type;
+ }
+ this.log('Finished initial injection');
+ })();
+ }
+
+ deinit() {
+ this.menuRenderer.type = this.originalRenderer;
+ this.menuRenderer.alternate.type = this.menuRenderer.type;
+ }
+
+ addItem(item: CustomMainMenuItem) {
+ return this.menuState.addItem(item);
+ }
+
+ addPatch(path: string, patch: ItemPatch) {
+ return this.menuState.addPatch(path, patch);
+ }
+
+ addOverlayPatch(patch: OverlayPatch) {
+ return this.menuState.addOverlayPatch(patch);
+ }
+
+ addOverlayComponent(component: ReactNode) {
+ return this.menuState.addOverlayComponent(component);
+ }
+
+ removePatch(path: string, patch: ItemPatch) {
+ return this.menuState.removePatch(path, patch);
+ }
+
+ removeItem(item: CustomMainMenuItem) {
+ return this.menuState.removeItem(item);
+ }
+
+ removeOverlayPatch(patch: OverlayPatch) {
+ return this.menuState.removeOverlayPatch(patch);
+ }
+
+ removeOverlayComponent(component: ReactNode) {
+ return this.menuState.removeOverlayComponent(component);
+ }
+}
+
+export default MenuHook;