summaryrefslogtreecommitdiff
path: root/frontend/src/router-hook.tsx
diff options
context:
space:
mode:
authorAAGaming <aagaming@riseup.net>2024-07-09 02:35:24 -0400
committerAAGaming <aagaming@riseup.net>2024-08-03 14:04:19 -0400
commit28c7254ef6952d9504472ebcbb05238b50aa6086 (patch)
tree2c9dd8359ad06a2b4cbb38763fe7ae845b4489e0 /frontend/src/router-hook.tsx
parentdcfaf11696edbed2a9c0be01a7c37e8faab773bc (diff)
downloaddecky-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.tsx237
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();
}
}