@decky/ui 4.0.3 → 4.1.0

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.
package/README.md CHANGED
@@ -25,3 +25,6 @@ If you would like a feature added to decky-frontend-lib, please request it via a
25
25
  If you want to start making a plugin with decky-frontend-lib, please direct your attention to the [decky-plugin-template](https://github.com/SteamDeckHomebrew/decky-plugin-template) repository.
26
26
 
27
27
  This library can be found on [npm](https://www.npmjs.com/package/decky-frontend-lib) and as such you can pull it without a local copy for your project as needed.
28
+
29
+ Tips for fixing failing module finds after Steam updates:
30
+ - `Object.entries(DFL)` can point out any undefined exports
@@ -1,3 +1,5 @@
1
1
  import { CommonUIModule } from '../webpack';
2
- export const ButtonItem = Object.values(CommonUIModule).find((mod) => mod?.render?.toString()?.includes('"highlightOnFocus","childrenContainerWidth"') ||
3
- mod?.render?.toString()?.includes('childrenContainerWidth:"min"'));
2
+ import { createPropListRegex } from '../utils';
3
+ const buttonItemRegex = createPropListRegex(["highlightOnFocus", "childrenContainerWidth"], false);
4
+ export const ButtonItem = Object.values(CommonUIModule).find((mod) => (mod?.render?.toString && buttonItemRegex.test(mod.render.toString())) ||
5
+ mod?.render?.toString?.().includes('childrenContainerWidth:"min"'));
@@ -1,8 +1,15 @@
1
1
  import { CommonUIModule } from '../webpack';
2
- const CommonDialogDivs = Object.values(CommonUIModule).filter((m) => typeof m === 'object' && m?.render?.toString().includes('"div",Object.assign({},'));
2
+ const CommonDialogDivs = Object.values(CommonUIModule).filter((m) => typeof m === 'object' && m?.render?.toString().includes('createElement("div",{...') ||
3
+ m?.render?.toString().includes('createElement("div",Object.assign({},'));
3
4
  const MappedDialogDivs = new Map(Object.values(CommonDialogDivs).map((m) => {
4
- const renderedDiv = m.render({});
5
- return [renderedDiv.props.className.split(' ')[0], m];
5
+ try {
6
+ const renderedDiv = m.render({});
7
+ return [renderedDiv.props.className.split(' ')[0], m];
8
+ }
9
+ catch (e) {
10
+ console.error("[DFL:Dialog]: failed to render common dialog component", e);
11
+ return [null, null];
12
+ }
6
13
  }));
7
14
  export const DialogHeader = MappedDialogDivs.get('DialogHeader');
8
15
  export const DialogSubHeader = MappedDialogDivs.get('DialogSubHeader');
@@ -12,8 +19,6 @@ export const DialogBodyText = MappedDialogDivs.get('DialogBodyText');
12
19
  export const DialogBody = MappedDialogDivs.get('DialogBody');
13
20
  export const DialogControlsSection = MappedDialogDivs.get('DialogControlsSection');
14
21
  export const DialogControlsSectionHeader = MappedDialogDivs.get('DialogControlsSectionHeader');
15
- export const DialogButtonPrimary = Object.values(CommonUIModule).find((mod) => mod?.render?.toString()?.includes('DialogButton') && mod?.render?.toString()?.includes('Primary'));
16
- export const DialogButtonSecondary = Object.values(CommonUIModule).find((mod) => mod?.render?.toString()?.includes('Object.assign({type:"button"') &&
17
- mod?.render?.toString()?.includes('DialogButton') &&
18
- mod?.render?.toString()?.includes('Secondary'));
22
+ export const DialogButtonPrimary = Object.values(CommonUIModule).find((mod) => mod?.render?.toString()?.includes('"DialogButton","_DialogLayout","Primary"'));
23
+ export const DialogButtonSecondary = Object.values(CommonUIModule).find((mod) => mod?.render?.toString()?.includes('"DialogButton","_DialogLayout","Secondary"'));
19
24
  export const DialogButton = DialogButtonSecondary;
@@ -1,3 +1,5 @@
1
1
  import { CommonUIModule } from '../webpack';
2
+ import { createPropListRegex } from '../utils';
2
3
  export const Dropdown = Object.values(CommonUIModule).find((mod) => mod?.prototype?.SetSelectedOption && mod?.prototype?.BuildMenu);
3
- export const DropdownItem = Object.values(CommonUIModule).find((mod) => mod?.toString()?.includes('"dropDownControlRef","description"'));
4
+ const dropdownItemRegex = createPropListRegex(["dropDownControlRef", "description"], false);
5
+ export const DropdownItem = Object.values(CommonUIModule).find((mod) => mod?.toString && dropdownItemRegex.test(mod.toString()));
@@ -1,2 +1,4 @@
1
1
  import { findModuleExport } from '../webpack';
2
- export const Focusable = findModuleExport((e) => e?.render?.toString()?.includes('["flow-children","onActivate","onCancel","focusClassName",'));
2
+ import { createPropListRegex } from '../utils';
3
+ const focusableRegex = createPropListRegex(["flow-children", "onActivate", "onCancel", "focusClassName", "focusWithinClassName"]);
4
+ export const Focusable = findModuleExport((e) => e?.render?.toString && focusableRegex.test(e.render.toString()));
@@ -1,6 +1,7 @@
1
1
  import { fakeRenderComponent } from '../utils';
2
2
  import { findModuleExport } from '../webpack';
3
- export const showContextMenu = findModuleExport((e) => typeof e === 'function' && e.toString().includes('stopPropagation))'));
3
+ export const showContextMenu = findModuleExport((e) => typeof e === 'function' && e.toString().includes('GetContextMenuManagerFromWindow(')
4
+ && e.toString().includes('.CreateContextMenuInstance('));
4
5
  export const Menu = findModuleExport((e) => e?.prototype?.HideIfSubmenu && e?.prototype?.HideMenu);
5
6
  export const MenuGroup = findModuleExport((e) => (e?.toString()?.includes?.('bInGamepadUI:') &&
6
7
  fakeRenderComponent(() => e({ overview: { appid: 7 } }), { useContext: () => ({ IN_GAMEPADUI: true }) })?.type?.prototype?.RenderSubMenu) ||
@@ -1,4 +1,6 @@
1
1
  import { findModuleExport } from '../webpack';
2
+ import { createPropListRegex } from '../utils';
2
3
  export const ProgressBar = findModuleExport((e) => e?.toString()?.includes('.ProgressBar,"standard"=='));
3
4
  export const ProgressBarWithInfo = findModuleExport((e) => e?.toString()?.includes('.ProgressBarFieldStatus},'));
4
- export const ProgressBarItem = findModuleExport((e) => e?.toString()?.includes('"indeterminate","nTransitionSec"'));
5
+ const progressBarItemRegex = createPropListRegex(["indeterminate", "nTransitionSec", "nProgress"]);
6
+ export const ProgressBarItem = findModuleExport((e) => e?.toString && progressBarItemRegex.test(e.toString()));
@@ -1,2 +1,4 @@
1
1
  import { findModuleExport } from '../webpack';
2
- export const SidebarNavigation = findModuleExport((e) => e?.toString()?.includes('"disableRouteReporting"'));
2
+ import { createPropListRegex } from '../utils';
3
+ const sidebarNavigationRegex = createPropListRegex(["pages", "fnSetNavigateToPage", "disableRouteReporting"]);
4
+ export const SidebarNavigation = findModuleExport((e) => e?.toString && sidebarNavigationRegex.test(e.toString()));
@@ -1,2 +1,2 @@
1
1
  import { IconsModule } from '../webpack';
2
- export const Spinner = Object.values(IconsModule).find((mod) => mod?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(mod.toString()));
2
+ export const Spinner = Object.values(IconsModule)?.find((mod) => mod?.toString && /Spinner\)}\)?,.\.createElement\(\"path\",{d:\"M18 /.test(mod.toString()));
@@ -4,6 +4,7 @@ declare global {
4
4
  SP_REACT: typeof React;
5
5
  }
6
6
  }
7
+ export declare function createPropListRegex(propList: string[], fromStart?: boolean): RegExp;
7
8
  export declare function fakeRenderComponent(fun: Function, customHooks?: any): any;
8
9
  export declare function wrapReactType(node: any, prop?: any): any;
9
10
  export declare function wrapReactClass(node: any, prop?: any): any;
@@ -2,6 +2,16 @@ var __setFunctionName = (this && this.__setFunctionName) || function (f, name, p
2
2
  if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
3
3
  return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
4
4
  };
5
+ export function createPropListRegex(propList, fromStart = true) {
6
+ let regexString = fromStart ? "const\{" : "";
7
+ propList.forEach((prop, propIdx) => {
8
+ regexString += `"?${prop}"?:[a-zA-Z_$]{1,2}`;
9
+ if (propIdx < propList.length - 1) {
10
+ regexString += ",";
11
+ }
12
+ });
13
+ return new RegExp(regexString);
14
+ }
5
15
  export function fakeRenderComponent(fun, customHooks = {}) {
6
16
  const hooks = window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher
7
17
  .current;
package/dist/webpack.js CHANGED
@@ -98,5 +98,5 @@ export const CommonUIModule = modules.find((m) => {
98
98
  }
99
99
  return false;
100
100
  });
101
- export const IconsModule = findModuleByExport((e) => e?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(e.toString()));
101
+ export const IconsModule = findModuleByExport((e) => e?.toString && /Spinner\)}\)?,.\.createElement\(\"path\",{d:\"M18 /.test(e.toString()));
102
102
  export const ReactRouter = findModuleByExport((e) => e.computeRootMatch);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decky/ui",
3
- "version": "4.0.3",
3
+ "version": "4.1.0",
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",
@@ -2,13 +2,15 @@ import { FC } from 'react';
2
2
 
3
3
  import { CommonUIModule } from '../webpack';
4
4
  import { ItemProps } from './Item';
5
+ import { createPropListRegex } from '../utils';
5
6
 
6
7
  export interface ButtonItemProps extends ItemProps {
7
8
  onClick?(e: MouseEvent): void;
8
9
  disabled?: boolean;
9
10
  }
11
+ const buttonItemRegex = createPropListRegex(["highlightOnFocus", "childrenContainerWidth"], false);
10
12
  export const ButtonItem = Object.values(CommonUIModule).find(
11
13
  (mod: any) =>
12
- mod?.render?.toString()?.includes('"highlightOnFocus","childrenContainerWidth"') ||
13
- mod?.render?.toString()?.includes('childrenContainerWidth:"min"'),
14
+ (mod?.render?.toString && buttonItemRegex.test(mod.render.toString())) ||
15
+ mod?.render?.toString?.().includes('childrenContainerWidth:"min"'),
14
16
  ) as FC<ButtonItemProps>;
@@ -52,13 +52,19 @@ export interface DialogButtonProps extends DialogCommonProps, FooterLegendProps
52
52
  }
53
53
 
54
54
  const CommonDialogDivs = Object.values(CommonUIModule).filter(
55
- (m: any) => typeof m === 'object' && m?.render?.toString().includes('"div",Object.assign({},'),
55
+ (m: any) => typeof m === 'object' && m?.render?.toString().includes('createElement("div",{...') ||
56
+ m?.render?.toString().includes('createElement("div",Object.assign({},'),
56
57
  );
57
58
  const MappedDialogDivs = new Map(
58
59
  Object.values(CommonDialogDivs).map((m: any) => {
59
- const renderedDiv = m.render({});
60
- // Take only the first class name segment as it identifies the element we want
61
- return [renderedDiv.props.className.split(' ')[0], m];
60
+ try {
61
+ const renderedDiv = m.render({});
62
+ // Take only the first class name segment as it identifies the element we want
63
+ return [renderedDiv.props.className.split(' ')[0], m];
64
+ } catch (e) {
65
+ console.error("[DFL:Dialog]: failed to render common dialog component", e);
66
+ return [null, null];
67
+ }
62
68
  }),
63
69
  );
64
70
 
@@ -72,14 +78,11 @@ export const DialogControlsSection = MappedDialogDivs.get('DialogControlsSection
72
78
  export const DialogControlsSectionHeader = MappedDialogDivs.get('DialogControlsSectionHeader') as FC<DialogCommonProps>;
73
79
 
74
80
  export const DialogButtonPrimary = Object.values(CommonUIModule).find(
75
- (mod: any) => mod?.render?.toString()?.includes('DialogButton') && mod?.render?.toString()?.includes('Primary'),
81
+ (mod: any) => mod?.render?.toString()?.includes('"DialogButton","_DialogLayout","Primary"'),
76
82
  ) as FC<DialogButtonProps>;
77
83
 
78
84
  export const DialogButtonSecondary = Object.values(CommonUIModule).find(
79
- (mod: any) =>
80
- mod?.render?.toString()?.includes('Object.assign({type:"button"') &&
81
- mod?.render?.toString()?.includes('DialogButton') &&
82
- mod?.render?.toString()?.includes('Secondary'),
85
+ (mod: any) => mod?.render?.toString()?.includes('"DialogButton","_DialogLayout","Secondary"')
83
86
  ) as FC<DialogButtonProps>;
84
87
 
85
88
  // This is the "main" button. The Primary can act as a submit button,
@@ -2,6 +2,7 @@ import { ReactNode, FC } from 'react';
2
2
 
3
3
  import { CommonUIModule } from '../webpack';
4
4
  import { ItemProps } from './Item';
5
+ import { createPropListRegex } from '../utils';
5
6
 
6
7
  export interface SingleDropdownOption {
7
8
  data: any;
@@ -44,6 +45,7 @@ export const Dropdown = Object.values(CommonUIModule).find(
44
45
 
45
46
  export interface DropdownItemProps extends DropdownProps, ItemProps {}
46
47
 
48
+ const dropdownItemRegex = createPropListRegex(["dropDownControlRef", "description"], false);
47
49
  export const DropdownItem = Object.values(CommonUIModule).find((mod: any) =>
48
- mod?.toString()?.includes('"dropDownControlRef","description"'),
50
+ mod?.toString && dropdownItemRegex.test(mod.toString()),
49
51
  ) as FC<DropdownItemProps>;
@@ -2,6 +2,7 @@ import { HTMLAttributes, ReactNode, RefAttributes, FC } from 'react';
2
2
 
3
3
  import { Export, findModuleExport } from '../webpack';
4
4
  import { FooterLegendProps } from './FooterLegend';
5
+ import { createPropListRegex } from '../utils';
5
6
 
6
7
  export interface FocusableProps extends HTMLAttributes<HTMLDivElement>, FooterLegendProps {
7
8
  children: ReactNode;
@@ -13,6 +14,8 @@ export interface FocusableProps extends HTMLAttributes<HTMLDivElement>, FooterLe
13
14
  onCancel?: (e: CustomEvent) => void;
14
15
  }
15
16
 
17
+ const focusableRegex = createPropListRegex(["flow-children", "onActivate", "onCancel", "focusClassName", "focusWithinClassName"]);
18
+
16
19
  export const Focusable = findModuleExport((e: Export) =>
17
- e?.render?.toString()?.includes('["flow-children","onActivate","onCancel","focusClassName",'),
20
+ e?.render?.toString && focusableRegex.test(e.render.toString())
18
21
  ) as FC<FocusableProps & RefAttributes<HTMLDivElement>>;
@@ -5,7 +5,8 @@ import { Export, findModuleExport } from '../webpack';
5
5
  import { FooterLegendProps } from './FooterLegend';
6
6
 
7
7
  export const showContextMenu: (children: ReactNode, parent?: EventTarget) => void = findModuleExport(
8
- (e: Export) => typeof e === 'function' && e.toString().includes('stopPropagation))'),
8
+ (e: Export) => typeof e === 'function' && e.toString().includes('GetContextMenuManagerFromWindow(')
9
+ && e.toString().includes('.CreateContextMenuInstance('),
9
10
  );
10
11
 
11
12
  export interface MenuProps extends FooterLegendProps {
@@ -2,6 +2,7 @@ import { ReactNode, FC } from 'react';
2
2
 
3
3
  import { Export, findModuleExport } from '../webpack';
4
4
  import { ItemProps } from './Item';
5
+ import { createPropListRegex } from '../utils';
5
6
 
6
7
  export interface ProgressBarItemProps extends ItemProps {
7
8
  indeterminate?: boolean;
@@ -30,6 +31,7 @@ export const ProgressBarWithInfo = findModuleExport((e: Export) =>
30
31
  e?.toString()?.includes('.ProgressBarFieldStatus},'),
31
32
  ) as FC<ProgressBarWithInfoProps>;
32
33
 
34
+ const progressBarItemRegex = createPropListRegex(["indeterminate", "nTransitionSec", "nProgress"]);
33
35
  export const ProgressBarItem = findModuleExport((e: Export) =>
34
- e?.toString()?.includes('"indeterminate","nTransitionSec"'),
36
+ e?.toString && progressBarItemRegex.test(e.toString()),
35
37
  ) as FC<ProgressBarItemProps>;
@@ -1,6 +1,7 @@
1
1
  import { ReactNode, FC } from 'react';
2
2
 
3
3
  import { Export, findModuleExport } from '../webpack';
4
+ import { createPropListRegex } from '../utils';
4
5
 
5
6
  export interface SidebarNavigationPage {
6
7
  title: ReactNode;
@@ -23,6 +24,7 @@ export interface SidebarNavigationProps {
23
24
  onPageRequested?: (page: string) => void;
24
25
  }
25
26
 
27
+ const sidebarNavigationRegex = createPropListRegex(["pages", "fnSetNavigateToPage", "disableRouteReporting"]);
26
28
  export const SidebarNavigation = findModuleExport((e: Export) =>
27
- e?.toString()?.includes('"disableRouteReporting"'),
29
+ e?.toString && sidebarNavigationRegex.test(e.toString()),
28
30
  ) as FC<SidebarNavigationProps>;
@@ -3,6 +3,6 @@ import { FC, SVGAttributes } from 'react';
3
3
  import { IconsModule } from '../webpack';
4
4
 
5
5
  // TODO type this and other icons?
6
- export const Spinner = Object.values(IconsModule).find(
7
- (mod: any) => mod?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(mod.toString()),
6
+ export const Spinner = Object.values(IconsModule)?.find(
7
+ (mod: any) => mod?.toString && /Spinner\)}\)?,.\.createElement\(\"path\",{d:\"M18 /.test(mod.toString()),
8
8
  ) as FC<SVGAttributes<SVGElement>>;
package/src/index.ts CHANGED
@@ -13,8 +13,8 @@ export * from './class-mapper';
13
13
  * @deprecated use @decky/api instead
14
14
  */
15
15
  export const definePlugin = (fn: any): any => {
16
- return (...args: any[]) => {
17
- // TODO: Maybe wrap this
18
- return fn(...args);
19
- };
16
+ return (...args: any[]) => {
17
+ // TODO: Maybe wrap this
18
+ return fn(...args);
19
+ };
20
20
  };
@@ -8,6 +8,28 @@ declare global {
8
8
  }
9
9
  }
10
10
 
11
+ /**
12
+ * Create a Regular Expression to search for a React component that uses certain props in order.
13
+ *
14
+ * @export
15
+ * @param {string[]} propList Ordererd list of properties to search for
16
+ * @returns {RegExp} RegEx to call .test(component.toString()) on
17
+ */
18
+ export function createPropListRegex(propList: string[], fromStart: boolean = true): RegExp {
19
+ let regexString = fromStart ? "const\{" : "";
20
+ propList.forEach((prop: any, propIdx) => {
21
+ regexString += `"?${prop}"?:[a-zA-Z_$]{1,2}`;
22
+ if (propIdx < propList.length - 1) {
23
+ regexString += ",";
24
+ }
25
+ });
26
+
27
+ // TODO provide a way to enable this
28
+ // console.debug(`[DFL:Utils] createPropListRegex generated regex "${regexString}" for props`, propList);
29
+
30
+ return new RegExp(regexString);
31
+ }
32
+
11
33
  export function fakeRenderComponent(fun: Function, customHooks: any = {}): any {
12
34
  const hooks = (window.SP_REACT as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher
13
35
  .current;
package/src/webpack.ts CHANGED
@@ -130,7 +130,7 @@ export const CommonUIModule = modules.find((m: Module) => {
130
130
  });
131
131
 
132
132
  export const IconsModule = findModuleByExport(
133
- (e) => e?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(e.toString()),
133
+ (e) => e?.toString && /Spinner\)}\)?,.\.createElement\(\"path\",{d:\"M18 /.test(e.toString()),
134
134
  );
135
135
 
136
136
  export const ReactRouter = findModuleByExport((e) => e.computeRootMatch);