From 28c7254ef6952d9504472ebcbb05238b50aa6086 Mon Sep 17 00:00:00 2001
From: AAGaming <aagaming@riseup.net>
Date: Tue, 9 Jul 2024 02:35:24 -0400
Subject: initial implementation of new router and qam hooks

---
 frontend/src/router-hook.tsx | 237 +++++++++++++++++++++++--------------------
 1 file changed, 128 insertions(+), 109 deletions(-)

(limited to 'frontend/src/router-hook.tsx')

diff --git a/frontend/src/router-hook.tsx b/frontend/src/router-hook.tsx
index e3325913..9aba497e 100644
--- a/frontend/src/router-hook.tsx
+++ b/frontend/src/router-hook.tsx
@@ -1,4 +1,4 @@
-import { ErrorBoundary, Focusable, Patch, afterPatch } from '@decky/ui';
+import { ErrorBoundary, Focusable, Patch, afterPatch, beforePatch, findInReactTree, findModuleByExport, findModuleExport, getReactRoot, sleep } from '@decky/ui';
 import { FC, ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
 import type { Route } from 'react-router';
 
@@ -25,13 +25,13 @@ declare global {
 const isPatched = Symbol('is patched');
 
 class RouterHook extends Logger {
-  private router: any;
-  private memoizedRouter: any;
-  private gamepadWrapper: any;
   private routerState: DeckyRouterState = new DeckyRouterState();
   private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState();
-  private wrapperPatch: Patch;
-  private routerPatch?: Patch;
+  private renderedComponents: ReactElement[] = [];
+  private Route: any;
+  private DeckyWrapper = this.routerWrapper.bind(this);
+  private DeckyGlobalComponentsWrapper = this.globalComponentsWrapper.bind(this);
+  private toReplace = new Map<string, ReactNode>();
   public routes?: any[];
 
   constructor() {
@@ -41,114 +41,133 @@ class RouterHook extends Logger {
     window.__ROUTER_HOOK_INSTANCE?.deinit?.();
     window.__ROUTER_HOOK_INSTANCE = this;
 
-    this.gamepadWrapper = Focusable;
-
-    let Route: new () => Route;
-    // Used to store the new replicated routes we create to allow routes to be unpatched.
-    const processList = (
-      routeList: any[],
-      routes: Map<string, RouterEntry> | null,
-      routePatches: Map<string, Set<RoutePatch>>,
-      save: boolean,
-    ) => {
-      this.debug('Route list: ', routeList);
-      if (save) this.routes = routeList;
-      let routerIndex = routeList.length;
-      if (routes) {
-        if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
-          if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
-          const newRouterArray: (ReactElement | JSX.Element)[] = [];
-          routes.forEach(({ component, props }, path) => {
-            newRouterArray.push(
-              <Route path={path} {...props}>
-                <ErrorBoundary>{createElement(component)}</ErrorBoundary>
-              </Route>,
-            );
-          });
-          routeList[routerIndex] = newRouterArray;
-        }
+    (async()=> {
+      const root = getReactRoot(document.getElementById('root') as any);
+      // TODO be more specific, this is horrible and very very slow
+      const findRouterNode = () =>findInReactTree(root, node => typeof node?.pendingProps?.loggedIn == "undefined" && node?.type?.toString().includes("Settings.Root()"));
+      let routerNode = findRouterNode();
+      while (!routerNode) {
+        this.warn(
+          'Failed to find Router node, reattempting in 5 seconds.',
+        );
+        await sleep(5000);
+        routerNode = findRouterNode();
       }
-      routeList.forEach((route: Route, index: number) => {
-        const replaced = toReplace.get(route?.props?.path as string);
-        if (replaced) {
-          routeList[index].props.children = replaced;
-          toReplace.delete(route?.props?.path as string);
-        }
-        if (route?.props?.path && routePatches.has(route.props.path as string)) {
-          toReplace.set(
-            route?.props?.path as string,
-            // @ts-ignore
-            routeList[index].props.children,
-          );
-          routePatches.get(route.props.path as string)?.forEach((patch) => {
-            const oType = routeList[index].props.children.type;
-            routeList[index].props.children = patch({
-              ...routeList[index].props,
-              children: {
-                ...cloneElement(routeList[index].props.children),
-                type: routeList[index].props.children[isPatched] ? oType : (props) => createElement(oType, props),
-              },
-            }).children;
-            routeList[index].props.children[isPatched] = true;
-          });
+      if (routerNode) {
+        this.debug("routerNode", routerNode);
+        // Patch the component globally
+        afterPatch(routerNode.elementType, "type", this.handleRouterRender.bind(this));
+        // Swap out the current instance
+        routerNode.type = routerNode.elementType.type;
+        if (routerNode?.alternate) {
+          routerNode.alternate.type = routerNode.type;
         }
-      });
-    };
-    let toReplace = new Map<string, ReactNode>();
-    const DeckyWrapper = ({ children }: { children: ReactElement }) => {
-      const { routes, routePatches } = useDeckyRouterState();
-      const mainRouteList = children.props.children[0].props.children;
-      const ingameRouteList = children.props.children[1].props.children; // /appoverlay and /apprunning
-      processList(mainRouteList, routes, routePatches, true);
-      processList(ingameRouteList, null, routePatches, false);
-
-      this.debug('Rerendered routes list');
-      return children;
-    };
+        // Force a full rerender via our custom error boundary
+        routerNode?.return?.stateNode?._deckyForceRerender?.();
+      }
+    })();
+  }
+
+  public handleRouterRender(_: any, ret: any) {
+    const DeckyWrapper = this.DeckyWrapper;
+    const DeckyGlobalComponentsWrapper = this.DeckyGlobalComponentsWrapper;
+    if (!this.Route)
+      // TODO make more redundant
+      this.Route = ret.props.children[0].props.children.find((x: any) => x.props.path == '/createaccount').type;
+    if (ret._decky) {
+      return ret;
+    }
+    const returnVal = (
+      <>
+        <DeckyRouterStateContextProvider deckyRouterState={this.routerState}>
+          <DeckyWrapper>{ret}</DeckyWrapper>
+        </DeckyRouterStateContextProvider>
+        <DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
+          <DeckyGlobalComponentsWrapper />
+        </DeckyGlobalComponentsStateContextProvider>
+      </>
+    );
+    (returnVal as any)._decky = true;
+    return returnVal;
+  }
 
-    let renderedComponents: ReactElement[] = [];
+  private globalComponentsWrapper () {
+    const { components } = useDeckyGlobalComponentsState();
+    if (this.renderedComponents.length != components.size) {
+      this.debug('Rerendering global components');
+      this.renderedComponents = Array.from(components.values()).map((GComponent) => <GComponent />);
+    }
+    return <>{this.renderedComponents}</>;
+  };
 
-    const DeckyGlobalComponentsWrapper = () => {
-      const { components } = useDeckyGlobalComponentsState();
-      if (renderedComponents.length != components.size) {
-        this.debug('Rerendering global components');
-        renderedComponents = Array.from(components.values()).map((GComponent) => <GComponent />);
-      }
-      return <>{renderedComponents}</>;
-    };
-
-    this.wrapperPatch = afterPatch(this.gamepadWrapper, 'render', (_: any, ret: any) => {
-      if (ret?.props?.children?.props?.children?.length == 5 || ret?.props?.children?.props?.children?.length == 4) {
-        const idx = ret?.props?.children?.props?.children?.length == 4 ? 1 : 2;
-        const potentialSettingsRootString =
-          ret.props.children.props.children[idx]?.props?.children?.[0]?.type?.type?.toString() || '';
-        if (potentialSettingsRootString?.includes('Settings.Root()')) {
-          if (!this.router) {
-            this.router = ret.props.children.props.children[idx]?.props?.children?.[0]?.type;
-            this.routerPatch = afterPatch(this.router, 'type', (_: any, ret: any) => {
-              if (!Route)
-                Route = ret.props.children[0].props.children.find((x: any) => x.props.path == '/createaccount').type;
-              const returnVal = (
-                <DeckyRouterStateContextProvider deckyRouterState={this.routerState}>
-                  <DeckyWrapper>{ret}</DeckyWrapper>
-                </DeckyRouterStateContextProvider>
-              );
-              return returnVal;
-            });
-            this.memoizedRouter = memo(this.router.type);
-            this.memoizedRouter.isDeckyRouter = true;
-          }
-          ret.props.children.props.children.push(
-            <DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
-              <DeckyGlobalComponentsWrapper />
-            </DeckyGlobalComponentsStateContextProvider>,
+  private routerWrapper({ children }: { children: ReactElement }) {
+    // Used to store the new replicated routes we create to allow routes to be unpatched.
+
+    const { routes, routePatches } = useDeckyRouterState();
+    // TODO make more redundant
+    if (!children?.props?.children?.[0]?.props?.children) {
+      console.log("routerWrapper wrong component?", children)
+      return children;
+    }
+    const mainRouteList = children.props.children[0].props.children;
+    const ingameRouteList = children.props.children[1].props.children; // /appoverlay and /apprunning
+    this.processList(mainRouteList, routes, routePatches, true);
+    this.processList(ingameRouteList, null, routePatches, false);
+
+    this.debug('Rerendered routes list');
+    return children;
+  };
+
+  private processList(
+    routeList: any[],
+    routes: Map<string, RouterEntry> | null,
+    routePatches: Map<string, Set<RoutePatch>>,
+    save: boolean,
+  ) {
+    const Route = this.Route;
+    this.debug('Route list: ', routeList);
+    if (save) this.routes = routeList;
+    let routerIndex = routeList.length;
+    if (routes) {
+      if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
+        if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
+        const newRouterArray: (ReactElement | JSX.Element)[] = [];
+        routes.forEach(({ component, props }, path) => {
+          newRouterArray.push(
+            <Route path={path} {...props}>
+              <ErrorBoundary>{createElement(component)}</ErrorBoundary>
+            </Route>,
           );
-          ret.props.children.props.children[idx].props.children[0].type = this.memoizedRouter;
-        }
+        });
+        routeList[routerIndex] = newRouterArray;
+      }
+    }
+    routeList.forEach((route: Route, index: number) => {
+      const replaced = this.toReplace.get(route?.props?.path as string);
+      if (replaced) {
+        routeList[index].props.children = replaced;
+        this.toReplace.delete(route?.props?.path as string);
+      }
+      if (route?.props?.path && routePatches.has(route.props.path as string)) {
+        this.toReplace.set(
+          route?.props?.path as string,
+          // @ts-ignore
+          routeList[index].props.children,
+        );
+        routePatches.get(route.props.path as string)?.forEach((patch) => {
+          const oType = routeList[index].props.children.type;
+          routeList[index].props.children = patch({
+            ...routeList[index].props,
+            children: {
+              ...cloneElement(routeList[index].props.children),
+              type: routeList[index].props.children[isPatched] ? oType : (props) => createElement(oType, props),
+            },
+          }).children;
+          routeList[index].props.children[isPatched] = true;
+        });
       }
-      return ret;
     });
-  }
+  };
 
   addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) {
     this.routerState.addRoute(path, component, props);
@@ -175,8 +194,8 @@ class RouterHook extends Logger {
   }
 
   deinit() {
-    this.wrapperPatch.unpatch();
-    this.routerPatch?.unpatch();
+    // this.wrapperPatch.unpatch();
+    // this.routerPatch?.unpatch();
   }
 }
 
-- 
cgit v1.2.3

