@decky/ui 4.2.2 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,7 @@
1
- import { fakeRenderComponent } from '../utils';
2
- import { findModuleExport } from '../webpack';
1
+ import { findModuleByExport, findModuleExport } from '../webpack';
3
2
  export const showContextMenu = findModuleExport((e) => typeof e === 'function' && e.toString().includes('GetContextMenuManagerFromWindow(')
4
3
  && e.toString().includes('.CreateContextMenuInstance('));
5
4
  export const Menu = findModuleExport((e) => e?.prototype?.HideIfSubmenu && e?.prototype?.HideMenu);
6
- export const MenuGroup = findModuleExport((e) => (e?.toString()?.includes?.('bInGamepadUI:') &&
7
- fakeRenderComponent(() => e({ overview: { appid: 7 } }), { useContext: () => ({ IN_GAMEPADUI: true }) })?.type?.prototype?.RenderSubMenu) ||
8
- (e?.prototype?.RenderSubMenu && e?.prototype?.ShowSubMenu));
5
+ const MenuGoupModule = findModuleByExport(e => e?.prototype?.Focus && e?.prototype?.OnOKButton && e?.prototype?.render?.toString().includes?.(`"emphasis"==this.props.tone`));
6
+ export const MenuGroup = MenuGoupModule && Object.values(MenuGoupModule).find((e) => typeof e == "function" && e?.toString()?.includes("bInGamepadUI:"));
9
7
  export const MenuItem = findModuleExport((e) => e?.render?.toString()?.includes('bPlayAudio:') || (e?.prototype?.OnOKButton && e?.prototype?.OnMouseEnter));
@@ -1,5 +1,6 @@
1
1
  export * from './patcher';
2
2
  export * from './react';
3
+ export * from './react-patching';
3
4
  export * from './static-classes';
4
5
  declare global {
5
6
  var FocusNavController: any;
@@ -1,5 +1,6 @@
1
1
  export * from './patcher';
2
2
  export * from './react';
3
+ export * from './react-patching';
3
4
  export * from './static-classes';
4
5
  export function joinClassNames(...classes) {
5
6
  return classes.join(' ');
@@ -2,7 +2,7 @@ export declare let callOriginal: symbol;
2
2
  export interface PatchOptions {
3
3
  singleShot?: boolean;
4
4
  }
5
- type GenericPatchHandler = (args: any[], ret?: any) => any;
5
+ export type GenericPatchHandler = (args: any[], ret?: any) => any;
6
6
  export interface Patch {
7
7
  original: Function;
8
8
  property: string;
@@ -15,4 +15,3 @@ export interface Patch {
15
15
  export declare function beforePatch(object: any, property: string, handler: (args: any[]) => any, options?: PatchOptions): Patch;
16
16
  export declare function afterPatch(object: any, property: string, handler: (args: any[], ret: any) => any, options?: PatchOptions): Patch;
17
17
  export declare function replacePatch(object: any, property: string, handler: (args: any[]) => any, options?: PatchOptions): Patch;
18
- export {};
@@ -0,0 +1,7 @@
1
+ import { GenericPatchHandler } from './patcher';
2
+ export type GenericNodeStep = (node: any) => any;
3
+ export type NodeStep = GenericNodeStep;
4
+ export type ReactPatchHandler = GenericPatchHandler;
5
+ export declare function setReactPatcherLoggingEnabled(value?: boolean): void;
6
+ export declare function setReactPatcherPerformanceLoggingEnabled(value?: boolean): void;
7
+ export declare function createReactTreePatcher(steps: NodeStep[], handler: GenericPatchHandler, debugName?: string): GenericPatchHandler;
@@ -0,0 +1,75 @@
1
+ import Logger from '../logger';
2
+ import { afterPatch } from './patcher';
3
+ import { wrapReactClass, wrapReactType } from './react';
4
+ let loggingEnabled = false;
5
+ let perfLoggingEnabled = false;
6
+ export function setReactPatcherLoggingEnabled(value = true) { loggingEnabled = value; }
7
+ ;
8
+ export function setReactPatcherPerformanceLoggingEnabled(value = true) { perfLoggingEnabled = value; }
9
+ ;
10
+ function patchComponent(node, handler, steps, step, caches, logger, prop = 'type') {
11
+ loggingEnabled && logger.group('Patching node:', node);
12
+ switch (typeof node?.[prop]) {
13
+ case 'function':
14
+ const patch = afterPatch(node, prop, steps[step + 1] ? createStepHandler(handler, steps, step + 1, caches, logger) : handler);
15
+ loggingEnabled && logger.debug('Patched a function component', patch);
16
+ break;
17
+ case 'object':
18
+ if (node[prop]?.prototype?.render) {
19
+ wrapReactClass(node);
20
+ const patch = afterPatch(node[prop].prototype, 'render', steps[step + 1] ? createStepHandler(handler, steps, step + 1, caches, logger) : handler);
21
+ loggingEnabled && logger.debug('Patched class component', patch);
22
+ }
23
+ else {
24
+ loggingEnabled && logger.debug('Patching forwardref/memo');
25
+ wrapReactType(node, prop);
26
+ patchComponent(node[prop], handler, steps, step, caches, logger, node[prop]?.render ? 'render' : 'type');
27
+ }
28
+ break;
29
+ default:
30
+ logger.error('Unhandled component type', node);
31
+ break;
32
+ }
33
+ loggingEnabled && logger.groupEnd();
34
+ }
35
+ function handleStep(tree, handler, steps, step, caches, logger) {
36
+ const startTime = (loggingEnabled || perfLoggingEnabled) ? performance.now() : 0;
37
+ const stepHandler = steps[step];
38
+ const cache = caches[step] || (caches[step] = new Map());
39
+ loggingEnabled && logger.debug(`Patch step ${step} running`, { tree, stepHandler, step, caches });
40
+ const node = stepHandler(tree);
41
+ if (node && node.type) {
42
+ loggingEnabled && logger.debug('Found node', node);
43
+ }
44
+ else if (node) {
45
+ loggingEnabled && logger.error('Found node without type. Something is probably wrong.', node);
46
+ return tree;
47
+ }
48
+ else {
49
+ loggingEnabled && logger.warn('Found no node. Depending on your usecase, this might be fine.', node);
50
+ return tree;
51
+ }
52
+ let cachedType;
53
+ if (cachedType = cache.get(node.type)) {
54
+ loggingEnabled && logger.debug('Found cached patched component', node);
55
+ node.type = cachedType;
56
+ (loggingEnabled || perfLoggingEnabled) && logger.debug(`Patch step ${step} took ${performance.now() - startTime}ms with cache`);
57
+ return tree;
58
+ }
59
+ const originalType = node.type;
60
+ patchComponent(node, handler, steps, step, caches, logger);
61
+ cache.set(originalType, node.type);
62
+ (loggingEnabled || perfLoggingEnabled) && logger.debug(`Patch step ${step} took ${performance.now() - startTime}ms`);
63
+ return tree;
64
+ }
65
+ ;
66
+ function createStepHandler(handler, steps, step, caches, logger) {
67
+ loggingEnabled && logger.debug(`Creating handler for step ${step}`);
68
+ return (_, tree) => handleStep(tree, handler, steps, step, caches, logger);
69
+ }
70
+ export function createReactTreePatcher(steps, handler, debugName = 'ReactPatch') {
71
+ const caches = [];
72
+ const logger = new Logger(`ReactTreePatcher -> ${debugName}`);
73
+ loggingEnabled && logger.debug('Init with options:', steps, debugName);
74
+ return createStepHandler(handler, steps, 0, caches, logger);
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decky/ui",
3
- "version": "4.2.2",
3
+ "version": "4.3.1",
4
4
  "description": "A library for interacting with the Steam frontend in Decky plugins and elsewhere.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,7 +1,6 @@
1
1
  import { FC, ReactNode } from 'react';
2
2
 
3
- import { fakeRenderComponent } from '../utils';
4
- import { Export, findModuleExport } from '../webpack';
3
+ import { Export, findModuleByExport, findModuleExport } from '../webpack';
5
4
  import { FooterLegendProps } from './FooterLegend';
6
5
 
7
6
  export const showContextMenu: (children: ReactNode, parent?: EventTarget) => void = findModuleExport(
@@ -26,12 +25,8 @@ export interface MenuGroupProps {
26
25
  children?: ReactNode;
27
26
  }
28
27
 
29
- export const MenuGroup: FC<MenuGroupProps> = findModuleExport(
30
- (e: Export) =>
31
- (e?.toString()?.includes?.('bInGamepadUI:') &&
32
- fakeRenderComponent(() => e({ overview: { appid: 7 } }), {useContext: () => ({IN_GAMEPADUI: true})})?.type?.prototype?.RenderSubMenu) ||
33
- (e?.prototype?.RenderSubMenu && e?.prototype?.ShowSubMenu)
34
- );
28
+ const MenuGoupModule = findModuleByExport(e => e?.prototype?.Focus && e?.prototype?.OnOKButton && e?.prototype?.render?.toString().includes?.(`"emphasis"==this.props.tone`));
29
+ export const MenuGroup: FC<MenuGroupProps> = MenuGoupModule && Object.values(MenuGoupModule).find((e: Export) => typeof e == "function" && e?.toString()?.includes("bInGamepadUI:"));
35
30
  export interface MenuItemProps extends FooterLegendProps {
36
31
  bInteractableItem?: boolean;
37
32
  onClick?(evt: Event): void;
@@ -1,5 +1,6 @@
1
1
  export * from './patcher';
2
2
  export * from './react';
3
+ export * from './react-patching';
3
4
  export * from './static-classes';
4
5
 
5
6
  declare global {
@@ -6,7 +6,7 @@ export interface PatchOptions {
6
6
  singleShot?: boolean;
7
7
  }
8
8
 
9
- type GenericPatchHandler = (args: any[], ret?: any) => any;
9
+ export type GenericPatchHandler = (args: any[], ret?: any) => any;
10
10
 
11
11
  export interface Patch {
12
12
  original: Function;
@@ -0,0 +1,100 @@
1
+ import Logger from '../logger';
2
+ import { GenericPatchHandler, afterPatch } from './patcher';
3
+ import { wrapReactClass, wrapReactType } from './react';
4
+
5
+ // TODO max size limit? could implement as a class extending map perhaps
6
+ type PatchedComponentCache = Map<any, any>;
7
+ export type GenericNodeStep = (node: any) => any;
8
+ // to patch a specific method of a class component other than render. TODO implement
9
+ // export type ClassNodeStep = [finder: GenericNodeStep, method: string];
10
+ // export type NodeStep = GenericNodeStep | ClassNodeStep;
11
+ export type NodeStep = GenericNodeStep;
12
+ export type ReactPatchHandler = GenericPatchHandler;
13
+
14
+ // These will get *very* spammy.
15
+ let loggingEnabled = false;
16
+ let perfLoggingEnabled = false;
17
+
18
+ export function setReactPatcherLoggingEnabled(value: boolean = true) { loggingEnabled = value };
19
+ export function setReactPatcherPerformanceLoggingEnabled(value: boolean = true) { perfLoggingEnabled = value };
20
+
21
+ function patchComponent(node: any, handler: GenericPatchHandler, steps: NodeStep[], step: number, caches: PatchedComponentCache[], logger: Logger, prop: string = 'type') {
22
+ loggingEnabled && logger.group('Patching node:', node);
23
+ // We need to take extra care to not mutate the original node.type
24
+ switch (typeof node?.[prop]) {
25
+ case 'function':
26
+ // Function component
27
+ const patch = afterPatch(node, prop, steps[step + 1] ? createStepHandler(handler, steps, step + 1, caches, logger) : handler);
28
+ loggingEnabled && logger.debug('Patched a function component', patch);
29
+ break;
30
+ case 'object':
31
+ if (node[prop]?.prototype?.render) {
32
+ // Class component
33
+ // TODO handle patching custom methods
34
+ wrapReactClass(node);
35
+ const patch = afterPatch(node[prop].prototype, 'render', steps[step + 1] ? createStepHandler(handler, steps, step + 1, caches, logger) : handler);
36
+ loggingEnabled && logger.debug('Patched class component', patch);
37
+ } else {
38
+ loggingEnabled && logger.debug('Patching forwardref/memo');
39
+ wrapReactType(node, prop);
40
+ // Step down the object
41
+ patchComponent(node[prop], handler, steps, step, caches, logger, node[prop]?.render ? 'render' : 'type');
42
+ }
43
+ break;
44
+ default:
45
+ logger.error('Unhandled component type', node);
46
+ break;
47
+ }
48
+ loggingEnabled && logger.groupEnd();
49
+ }
50
+
51
+ function handleStep(tree: any, handler: GenericPatchHandler, steps: NodeStep[], step: number, caches: PatchedComponentCache[], logger: Logger): any {
52
+ const startTime = (loggingEnabled || perfLoggingEnabled) ? performance.now() : 0;
53
+ const stepHandler = steps[step];
54
+ const cache = caches[step] || (caches[step] = new Map());
55
+ loggingEnabled && logger.debug(`Patch step ${step} running`, { tree, stepHandler, step, caches });
56
+
57
+ const node = stepHandler(tree);
58
+ if (node && node.type) {
59
+ loggingEnabled && logger.debug('Found node', node);
60
+ } else if (node) {
61
+ loggingEnabled && logger.error('Found node without type. Something is probably wrong.', node);
62
+ return tree;
63
+ } else {
64
+ loggingEnabled && logger.warn('Found no node. Depending on your usecase, this might be fine.', node);
65
+ return tree;
66
+ }
67
+
68
+ let cachedType;
69
+ if (cachedType = cache.get(node.type)) {
70
+ loggingEnabled && logger.debug('Found cached patched component', node);
71
+ node.type = cachedType;
72
+ (loggingEnabled || perfLoggingEnabled) && logger.debug(`Patch step ${step} took ${performance.now() - startTime}ms with cache`);
73
+ return tree;
74
+ }
75
+
76
+ const originalType = node.type;
77
+
78
+ patchComponent(node, handler, steps, step, caches, logger);
79
+
80
+ cache.set(originalType, node.type);
81
+
82
+ (loggingEnabled || perfLoggingEnabled) && logger.debug(`Patch step ${step} took ${performance.now() - startTime}ms`);
83
+ return tree;
84
+ };
85
+
86
+ function createStepHandler(handler: GenericPatchHandler, steps: NodeStep[], step: number, caches: PatchedComponentCache[], logger: Logger) {
87
+ loggingEnabled && logger.debug(`Creating handler for step ${step}`);
88
+ return (_: any, tree: any) => handleStep(tree, handler, steps, step, caches, logger);
89
+ }
90
+
91
+ // TODO handle createReactTreePatcher inside handler and cache it (or warn)
92
+ export function createReactTreePatcher(steps: NodeStep[], handler: GenericPatchHandler, debugName: string = 'ReactPatch'): GenericPatchHandler {
93
+ const caches: PatchedComponentCache[] = [];
94
+
95
+ const logger = new Logger(`ReactTreePatcher -> ${debugName}`);
96
+
97
+ loggingEnabled && logger.debug('Init with options:', steps, debugName);
98
+
99
+ return createStepHandler(handler, steps, 0, caches, logger);
100
+ }