diff options
| author | AAGaming <aagaming@riseup.net> | 2024-07-09 02:35:24 -0400 |
|---|---|---|
| committer | AAGaming <aagaming@riseup.net> | 2024-08-03 14:04:19 -0400 |
| commit | 28c7254ef6952d9504472ebcbb05238b50aa6086 (patch) | |
| tree | 2c9dd8359ad06a2b4cbb38763fe7ae845b4489e0 /frontend/src/router-hook.tsx | |
| parent | dcfaf11696edbed2a9c0be01a7c37e8faab773bc (diff) | |
| download | decky-loader-28c7254ef6952d9504472ebcbb05238b50aa6086.tar.gz decky-loader-28c7254ef6952d9504472ebcbb05238b50aa6086.zip | |
initial implementation of new router and qam hooks
Diffstat (limited to 'frontend/src/router-hook.tsx')
| -rw-r--r-- | frontend/src/router-hook.tsx | 237 |
1 files changed, 128 insertions, 109 deletions
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(); } } |
