summaryrefslogtreecommitdiff
path: root/frontend/src/router-hook.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/router-hook.tsx')
-rw-r--r--frontend/src/router-hook.tsx220
1 files changed, 128 insertions, 92 deletions
diff --git a/frontend/src/router-hook.tsx b/frontend/src/router-hook.tsx
index 8dcdd9b8..e9ac9e0f 100644
--- a/frontend/src/router-hook.tsx
+++ b/frontend/src/router-hook.tsx
@@ -6,7 +6,9 @@ import {
findInTree,
findModuleByExport,
getReactRoot,
+ injectFCTrampoline,
sleep,
+ wrapReactType,
} from '@decky/ui';
import { FC, ReactElement, ReactNode, cloneElement, createElement } from 'react';
import type { Route } from 'react-router';
@@ -23,6 +25,7 @@ import {
RouterEntry,
useDeckyRouterState,
} from './components/DeckyRouterState';
+import { UIMode } from './enums';
import Logger from './logger';
declare global {
@@ -31,18 +34,18 @@ declare global {
}
}
-export enum UIMode {
- BigPicture = 4,
- Desktop = 7,
-}
-
const isPatched = Symbol('is patched');
class RouterHook extends Logger {
private routerState: DeckyRouterState = new DeckyRouterState();
private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState();
- private renderedComponents: ReactElement[] = [];
+ private renderedComponents = new Map<UIMode, ReactElement[]>([
+ [UIMode.BigPicture, []],
+ [UIMode.Desktop, []],
+ ]);
private Route: any;
+ private DesktopRoute: any;
+ private wrappedDesktopLibraryMemo?: any;
private DeckyGamepadRouterWrapper = this.gamepadRouterWrapper.bind(this);
private DeckyDesktopRouterWrapper = this.desktopRouterWrapper.bind(this);
private DeckyGlobalComponentsWrapper = this.globalComponentsWrapper.bind(this);
@@ -76,6 +79,21 @@ class RouterHook extends Logger {
this.error('Failed to find router stack module');
}
+ const routerModule = findModuleByExport((e) => e?.displayName == 'Router');
+ if (routerModule) {
+ this.DesktopRoute = Object.values(routerModule).find(
+ (e) =>
+ typeof e == 'function' &&
+ e?.prototype?.render?.toString()?.includes('props.computedMatch') &&
+ e?.prototype?.render?.toString()?.includes('.Children.count('),
+ );
+ if (!this.DesktopRoute) {
+ this.error('Failed to find DesktopRoute component');
+ }
+ } else {
+ this.error('Failed to find router module, desktop routes will not work');
+ }
+
this.modeChangeRegistration = SteamClient.UI.RegisterForUIModeChanged((mode: UIMode) => {
this.debug(`UI mode changed to ${mode}`);
if (this.patchedModes.has(mode)) return;
@@ -88,7 +106,7 @@ class RouterHook extends Logger {
break;
// Not fully implemented yet
case UIMode.Desktop:
- this.debug("Patching desktop router");
+ this.debug('Patching desktop router');
this.patchDesktopRouter();
break;
default:
@@ -109,7 +127,7 @@ class RouterHook extends Logger {
await this.waitForUnlock();
let routerNode = findRouterNode();
while (!routerNode) {
- this.warn('Failed to find Router node, reattempting in 5 seconds.');
+ this.warn('Failed to find GamepadUI Router node, reattempting in 5 seconds.');
await sleep(5000);
await this.waitForUnlock();
routerNode = findRouterNode();
@@ -130,49 +148,34 @@ class RouterHook extends Logger {
}
}
- // Currently unused
private async patchDesktopRouter() {
const root = getReactRoot(document.getElementById('root') as any);
const findRouterNode = () =>
- findInReactTree(root, (node) => node?.elementType?.type?.toString?.()?.includes('bShowDesktopUIContent:'));
+ findInReactTree(root, (node) => {
+ const typeStr = node?.elementType?.toString?.();
+ return (
+ typeStr &&
+ typeStr?.includes('.IsMainDesktopWindow') &&
+ typeStr?.includes('.IN_STEAMUI_SHARED_CONTEXT') &&
+ typeStr?.includes('.ContentFrame') &&
+ typeStr?.includes('.Console()')
+ );
+ });
let routerNode = findRouterNode();
while (!routerNode) {
- this.warn('Failed to find Router node, reattempting in 5 seconds.');
+ this.warn('Failed to find DesktopUI Router node, reattempting in 5 seconds.');
await sleep(5000);
routerNode = findRouterNode();
}
if (routerNode) {
- // this.debug("desktop router node", routerNode);
// Patch the component globally
- this.desktopRouterPatch = afterPatch(routerNode.elementType, 'type', this.handleDesktopRouterRender.bind(this));
- // Swap out the current instance
- routerNode.type = routerNode.elementType.type;
- if (routerNode?.alternate) {
- routerNode.alternate.type = routerNode.type;
- }
+ const patchedRenderer = injectFCTrampoline(routerNode.elementType);
+ this.desktopRouterPatch = afterPatch(patchedRenderer, 'component', this.handleDesktopRouterRender.bind(this));
// Force a full rerender via our custom error boundary
const errorBoundaryNode = findInTree(routerNode, (e) => e?.stateNode?._deckyForceRerender, {
walkable: ['return'],
});
errorBoundaryNode?.stateNode?._deckyForceRerender?.();
- // this.debug("desktop router node", routerNode);
- // // Patch the component globally
- // this.desktopRouterPatch = afterPatch(routerNode.type.prototype, 'render', this.handleDesktopRouterRender.bind(this));
- // const stateNodeClone = { render: routerNode.stateNode.render } as any;
- // // Patch the current instance. render is readonly so we have to do this.
- // Object.assign(stateNodeClone, routerNode.stateNode);
- // Object.setPrototypeOf(stateNodeClone, Object.getPrototypeOf(routerNode.stateNode));
- // this.desktopRouterFirstInstancePatch = afterPatch(stateNodeClone, 'render', this.handleDesktopRouterRender.bind(this));
- // routerNode.stateNode = stateNodeClone;
- // // Swap out the current instance
- // if (routerNode?.alternate) {
- // routerNode.alternate.type = routerNode.type;
- // routerNode.alternate.stateNode = routerNode.stateNode;
- // }
- // routerNode.stateNode.forceUpdate();
- // Force a full rerender via our custom error boundary
- // const errorBoundaryNode = findInTree(routerNode, e => e?.stateNode?._deckyForceRerender, { walkable: ["return"] });
- // errorBoundaryNode?.stateNode?._deckyForceRerender?.();
}
}
@@ -196,10 +199,18 @@ class RouterHook extends Logger {
const returnVal = (
<>
<DeckyRouterStateContextProvider deckyRouterState={this.routerState}>
+ <style>
+ {`
+ .deckyDesktopDialogPaddingHack + * .DialogContent_InnerWidth {
+ max-width: unset !important;
+ }
+ `}
+ </style>
+ <div className="deckyDesktopDialogPaddingHack" />
<DeckyDesktopRouterWrapper>{ret}</DeckyDesktopRouterWrapper>
</DeckyRouterStateContextProvider>
<DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
- <DeckyGlobalComponentsWrapper />
+ <DeckyGlobalComponentsWrapper uiMode={UIMode.Desktop} />
</DeckyGlobalComponentsStateContextProvider>
</>
);
@@ -219,7 +230,7 @@ class RouterHook extends Logger {
<DeckyGamepadRouterWrapper>{ret}</DeckyGamepadRouterWrapper>
</DeckyRouterStateContextProvider>
<DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
- <DeckyGlobalComponentsWrapper />
+ <DeckyGlobalComponentsWrapper uiMode={UIMode.BigPicture} />
</DeckyGlobalComponentsStateContextProvider>
</>
);
@@ -227,13 +238,21 @@ class RouterHook extends Logger {
return returnVal;
}
- private globalComponentsWrapper() {
+ private globalComponentsWrapper({ uiMode }: { uiMode: UIMode }) {
const { components } = useDeckyGlobalComponentsState();
- if (this.renderedComponents.length != components.size) {
- this.debug('Rerendering global components');
- this.renderedComponents = Array.from(components.values()).map((GComponent) => <GComponent />);
+ const componentsForMode = components.get(uiMode);
+ if (!componentsForMode) {
+ this.warn(`Couldn't find global components map for uimode ${uiMode}`);
+ return null;
+ }
+ if (!this.renderedComponents.has(uiMode) || this.renderedComponents.get(uiMode)?.length != componentsForMode.size) {
+ this.debug('Rerendering global components for uiMode', uiMode);
+ this.renderedComponents.set(
+ uiMode,
+ Array.from(componentsForMode.values()).map((GComponent) => <GComponent />),
+ );
}
- return <>{this.renderedComponents}</>;
+ return <>{this.renderedComponents.get(uiMode)}</>;
}
private gamepadRouterWrapper({ children }: { children: ReactElement }) {
@@ -247,8 +266,8 @@ class RouterHook extends Logger {
}
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.processList(mainRouteList, routes, routePatches.get(UIMode.BigPicture), true, this.Route);
+ this.processList(ingameRouteList, null, routePatches.get(UIMode.BigPicture), false, this.Route);
this.debug('Rerendered gamepadui routes list');
return children;
@@ -256,22 +275,38 @@ class RouterHook extends Logger {
private desktopRouterWrapper({ children }: { children: ReactElement }) {
// Used to store the new replicated routes we create to allow routes to be unpatched.
- this.debug('desktop router wrapper render', children);
const { routes, routePatches } = useDeckyRouterState();
- const routeList = findInReactTree(
+ const mainRouteList = findInReactTree(
children,
- (node) => node?.length > 2 && node?.find((elem: any) => elem?.props?.path == '/library/home'),
+ (node) => node?.length > 2 && node?.find((elem: any) => elem?.props?.path == '/console'),
);
- if (!routeList) {
+ if (!mainRouteList) {
this.debug('routerWrapper wrong component?', children);
return children;
}
- const library = children.props.children[1].props.children.props;
- if (!Array.isArray(library.children)) {
- library.children = [library.children];
+ this.processList(mainRouteList, routes, routePatches.get(UIMode.Desktop), true, this.DesktopRoute);
+ const libraryRouteWrapper = mainRouteList.find(
+ (r: any) => r?.props && 'cm' in r.props && 'bShowDesktopUIContent' in r.props,
+ );
+ if (!this.wrappedDesktopLibraryMemo) {
+ wrapReactType(libraryRouteWrapper);
+ afterPatch(libraryRouteWrapper.type, 'type', (_, ret) => {
+ const { routePatches } = useDeckyRouterState();
+ const libraryRouteList = findInReactTree(
+ ret,
+ (node) => node?.length > 1 && node?.find((elem: any) => elem?.props?.path == '/library/downloads'),
+ );
+ if (!libraryRouteList) {
+ this.warn('failed to find library route list', ret);
+ return ret;
+ }
+ this.processList(libraryRouteList, null, routePatches.get(UIMode.Desktop), false, this.DesktopRoute);
+ return ret;
+ });
+ this.wrappedDesktopLibraryMemo = libraryRouteWrapper.type;
+ } else {
+ libraryRouteWrapper.type = this.wrappedDesktopLibraryMemo;
}
- this.debug('library', library);
- this.processList(library.children, routes, routePatches, true);
this.debug('Rerendered desktop routes list');
return children;
@@ -279,11 +314,11 @@ class RouterHook extends Logger {
private processList(
routeList: any[],
- routes: Map<string, RouterEntry> | null,
- routePatches: Map<string, Set<RoutePatch>>,
+ routes: Map<string, RouterEntry> | null | undefined,
+ routePatches: Map<string, Set<RoutePatch>> | null | undefined,
save: boolean,
+ RouteComponent: any,
) {
- const Route = this.Route;
this.debug('Route list: ', routeList);
if (save) this.routes = routeList;
let routerIndex = routeList.length;
@@ -293,59 +328,60 @@ class RouterHook extends Logger {
const newRouterArray: (ReactElement | JSX.Element)[] = [];
routes.forEach(({ component, props }, path) => {
newRouterArray.push(
- <Route path={path} {...props}>
+ <RouteComponent path={path} {...props}>
<ErrorBoundary>{createElement(component)}</ErrorBoundary>
- </Route>,
+ </RouteComponent>,
);
});
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;
- });
- }
- });
+ routePatches &&
+ 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;
+ });
+ }
+ });
}
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) {
this.routerState.addRoute(path, component, props);
}
- addPatch(path: string, patch: RoutePatch) {
- return this.routerState.addPatch(path, patch);
+ addPatch(path: string, patch: RoutePatch, uiMode: UIMode = UIMode.BigPicture) {
+ return this.routerState.addPatch(path, patch, uiMode);
}
- addGlobalComponent(name: string, component: FC) {
- this.globalComponentsState.addComponent(name, component);
+ addGlobalComponent(name: string, component: FC, uiMode: UIMode = UIMode.BigPicture) {
+ this.globalComponentsState.addComponent(name, component, uiMode);
}
- removeGlobalComponent(name: string) {
- this.globalComponentsState.removeComponent(name);
+ removeGlobalComponent(name: string, uiMode: UIMode = UIMode.BigPicture) {
+ this.globalComponentsState.removeComponent(name, uiMode);
}
- removePatch(path: string, patch: RoutePatch) {
- this.routerState.removePatch(path, patch);
+ removePatch(path: string, patch: RoutePatch, uiMode: UIMode = UIMode.BigPicture) {
+ this.routerState.removePatch(path, patch, uiMode);
}
removeRoute(path: string) {