@decky/ui 4.0.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.
Files changed (150) hide show
  1. package/LICENSE +504 -0
  2. package/README.md +27 -0
  3. package/dist/class-mapper.d.ts +7 -0
  4. package/dist/class-mapper.js +26 -0
  5. package/dist/components/Button.d.ts +5 -0
  6. package/dist/components/Button.js +2 -0
  7. package/dist/components/ButtonItem.d.ts +7 -0
  8. package/dist/components/ButtonItem.js +3 -0
  9. package/dist/components/Carousel.d.ts +19 -0
  10. package/dist/components/Carousel.js +2 -0
  11. package/dist/components/ControlsList.d.ts +6 -0
  12. package/dist/components/ControlsList.js +2 -0
  13. package/dist/components/Dialog.d.ts +32 -0
  14. package/dist/components/Dialog.js +19 -0
  15. package/dist/components/DialogCheckbox.d.ts +17 -0
  16. package/dist/components/DialogCheckbox.js +14 -0
  17. package/dist/components/Dropdown.d.ts +34 -0
  18. package/dist/components/Dropdown.js +3 -0
  19. package/dist/components/Field.d.ts +22 -0
  20. package/dist/components/Field.js +2 -0
  21. package/dist/components/FocusRing.d.ts +9 -0
  22. package/dist/components/FocusRing.js +2 -0
  23. package/dist/components/Focusable.d.ts +12 -0
  24. package/dist/components/Focusable.js +2 -0
  25. package/dist/components/FooterLegend.d.ts +66 -0
  26. package/dist/components/FooterLegend.js +32 -0
  27. package/dist/components/Item.d.ts +11 -0
  28. package/dist/components/Item.js +1 -0
  29. package/dist/components/Marquee.d.ts +14 -0
  30. package/dist/components/Marquee.js +2 -0
  31. package/dist/components/Menu.d.ts +29 -0
  32. package/dist/components/Menu.js +8 -0
  33. package/dist/components/Modal.d.ts +52 -0
  34. package/dist/components/Modal.js +29 -0
  35. package/dist/components/Panel.d.ts +11 -0
  36. package/dist/components/Panel.js +4 -0
  37. package/dist/components/ProgressBar.d.ts +21 -0
  38. package/dist/components/ProgressBar.js +4 -0
  39. package/dist/components/Scroll.d.ts +7 -0
  40. package/dist/components/Scroll.js +5 -0
  41. package/dist/components/SidebarNavigation.d.ts +21 -0
  42. package/dist/components/SidebarNavigation.js +2 -0
  43. package/dist/components/SliderField.d.ts +26 -0
  44. package/dist/components/SliderField.js +2 -0
  45. package/dist/components/Spinner.d.ts +2 -0
  46. package/dist/components/Spinner.js +2 -0
  47. package/dist/components/SteamSpinner.d.ts +2 -0
  48. package/dist/components/SteamSpinner.js +2 -0
  49. package/dist/components/Tabs.d.ts +16 -0
  50. package/dist/components/Tabs.js +48 -0
  51. package/dist/components/TextField.d.ts +22 -0
  52. package/dist/components/TextField.js +2 -0
  53. package/dist/components/Toggle.d.ts +8 -0
  54. package/dist/components/Toggle.js +2 -0
  55. package/dist/components/ToggleField.d.ts +9 -0
  56. package/dist/components/ToggleField.js +2 -0
  57. package/dist/components/index.d.ts +25 -0
  58. package/dist/components/index.js +25 -0
  59. package/dist/custom-components/ColorPickerModal.d.ts +12 -0
  60. package/dist/custom-components/ColorPickerModal.js +87 -0
  61. package/dist/custom-components/ReorderableList.d.ts +26 -0
  62. package/dist/custom-components/ReorderableList.js +84 -0
  63. package/dist/custom-components/SuspensefulImage.d.ts +7 -0
  64. package/dist/custom-components/SuspensefulImage.js +27 -0
  65. package/dist/custom-components/index.d.ts +3 -0
  66. package/dist/custom-components/index.js +3 -0
  67. package/dist/custom-hooks/index.d.ts +1 -0
  68. package/dist/custom-hooks/index.js +1 -0
  69. package/dist/custom-hooks/useQuickAccessVisible.d.ts +1 -0
  70. package/dist/custom-hooks/useQuickAccessVisible.js +22 -0
  71. package/dist/deck-hooks/index.d.ts +1 -0
  72. package/dist/deck-hooks/index.js +1 -0
  73. package/dist/deck-hooks/useParams.d.ts +1 -0
  74. package/dist/deck-hooks/useParams.js +2 -0
  75. package/dist/globals/SteamClient.d.ts +307 -0
  76. package/dist/globals/SteamClient.js +1 -0
  77. package/dist/globals/index.d.ts +2 -0
  78. package/dist/globals/index.js +2 -0
  79. package/dist/globals/stores.d.ts +50 -0
  80. package/dist/globals/stores.js +1 -0
  81. package/dist/index.d.ts +10 -0
  82. package/dist/index.js +10 -0
  83. package/dist/logger.d.ts +17 -0
  84. package/dist/logger.js +46 -0
  85. package/dist/modules/Router.d.ts +110 -0
  86. package/dist/modules/Router.js +102 -0
  87. package/dist/modules/index.d.ts +1 -0
  88. package/dist/modules/index.js +1 -0
  89. package/dist/plugin.d.ts +68 -0
  90. package/dist/plugin.js +5 -0
  91. package/dist/utils/index.d.ts +12 -0
  92. package/dist/utils/index.js +23 -0
  93. package/dist/utils/patcher.d.ts +18 -0
  94. package/dist/utils/patcher.js +103 -0
  95. package/dist/utils/react.d.ts +18 -0
  96. package/dist/utils/react.js +74 -0
  97. package/dist/utils/static-classes.d.ts +52 -0
  98. package/dist/utils/static-classes.js +28 -0
  99. package/dist/webpack.d.ts +21 -0
  100. package/dist/webpack.js +102 -0
  101. package/package.json +82 -0
  102. package/src/class-mapper.ts +34 -0
  103. package/src/components/Button.tsx +8 -0
  104. package/src/components/ButtonItem.tsx +14 -0
  105. package/src/components/Carousel.ts +25 -0
  106. package/src/components/ControlsList.tsx +13 -0
  107. package/src/components/Dialog.tsx +86 -0
  108. package/src/components/DialogCheckbox.tsx +36 -0
  109. package/src/components/Dropdown.tsx +49 -0
  110. package/src/components/Field.tsx +28 -0
  111. package/src/components/FocusRing.ts +15 -0
  112. package/src/components/Focusable.tsx +18 -0
  113. package/src/components/FooterLegend.ts +67 -0
  114. package/src/components/Item.tsx +12 -0
  115. package/src/components/Marquee.tsx +20 -0
  116. package/src/components/Menu.tsx +57 -0
  117. package/src/components/Modal.tsx +119 -0
  118. package/src/components/Panel.tsx +26 -0
  119. package/src/components/ProgressBar.tsx +35 -0
  120. package/src/components/Scroll.tsx +15 -0
  121. package/src/components/SidebarNavigation.tsx +28 -0
  122. package/src/components/SliderField.tsx +33 -0
  123. package/src/components/Spinner.tsx +8 -0
  124. package/src/components/SteamSpinner.tsx +7 -0
  125. package/src/components/Tabs.tsx +127 -0
  126. package/src/components/TextField.tsx +28 -0
  127. package/src/components/Toggle.tsx +14 -0
  128. package/src/components/ToggleField.tsx +15 -0
  129. package/src/components/index.ts +25 -0
  130. package/src/custom-components/ColorPickerModal.tsx +132 -0
  131. package/src/custom-components/ReorderableList.tsx +183 -0
  132. package/src/custom-components/SuspensefulImage.tsx +44 -0
  133. package/src/custom-components/index.ts +3 -0
  134. package/src/custom-hooks/index.ts +1 -0
  135. package/src/custom-hooks/useQuickAccessVisible.tsx +63 -0
  136. package/src/deck-hooks/index.ts +1 -0
  137. package/src/deck-hooks/useParams.ts +15 -0
  138. package/src/globals/SteamClient.ts +321 -0
  139. package/src/globals/index.ts +2 -0
  140. package/src/globals/stores.ts +50 -0
  141. package/src/index.ts +11 -0
  142. package/src/logger.ts +77 -0
  143. package/src/modules/Router.tsx +174 -0
  144. package/src/modules/index.ts +1 -0
  145. package/src/plugin.tsx +96 -0
  146. package/src/utils/index.ts +43 -0
  147. package/src/utils/patcher.ts +160 -0
  148. package/src/utils/react.ts +119 -0
  149. package/src/utils/static-classes.ts +1104 -0
  150. package/src/webpack.ts +136 -0
package/src/logger.ts ADDED
@@ -0,0 +1,77 @@
1
+ const bgStyle1 = 'background: #16a085; color: black;';
2
+
3
+ export const log = (name: string, ...args: any[]) => {
4
+ console.log(
5
+ `%c @decky/ui %c ${name} %c`,
6
+ bgStyle1,
7
+ 'background: #1abc9c; color: black;',
8
+ 'background: transparent;',
9
+ ...args,
10
+ );
11
+ };
12
+
13
+ export const group = (name: string, ...args: any[]) => {
14
+ console.group(
15
+ `%c @decky/ui %c ${name} %c`,
16
+ bgStyle1,
17
+ 'background: #1abc9c; color: black;',
18
+ 'background: transparent;',
19
+ ...args,
20
+ );
21
+ };
22
+
23
+ export const groupEnd = (name: string, ...args: any[]) => {
24
+ console.groupEnd();
25
+ if (args?.length > 0)
26
+ console.log(
27
+ `^ %c @decky/ui %c ${name} %c`,
28
+ bgStyle1,
29
+ 'background: #1abc9c; color: black;',
30
+ 'background: transparent;',
31
+ ...args,
32
+ );
33
+ };
34
+
35
+ export const debug = (name: string, ...args: any[]) => {
36
+ console.debug(`%c @decky/ui %c ${name} %c`, bgStyle1, 'background: #1abc9c; color: black;', 'color: blue;', ...args);
37
+ };
38
+
39
+ export const warn = (name: string, ...args: any[]) => {
40
+ console.warn(`%c @decky/ui %c ${name} %c`, bgStyle1, 'background: #ffbb00; color: black;', 'color: blue;', ...args);
41
+ };
42
+
43
+ export const error = (name: string, ...args: any[]) => {
44
+ console.error(`%c @decky/ui %c ${name} %c`, bgStyle1, 'background: #FF0000;', 'background: transparent;', ...args);
45
+ };
46
+
47
+ class Logger {
48
+ constructor(private name: string) {
49
+ this.name = name;
50
+ }
51
+
52
+ log(...args: any[]) {
53
+ log(this.name, ...args);
54
+ }
55
+
56
+ debug(...args: any[]) {
57
+ debug(this.name, ...args);
58
+ }
59
+
60
+ warn(...args: any[]) {
61
+ warn(this.name, ...args);
62
+ }
63
+
64
+ error(...args: any[]) {
65
+ error(this.name, ...args);
66
+ }
67
+
68
+ group(...args: any[]) {
69
+ group(this.name, ...args);
70
+ }
71
+
72
+ groupEnd(...args: any[]) {
73
+ groupEnd(this.name, ...args);
74
+ }
75
+ }
76
+
77
+ export default Logger;
@@ -0,0 +1,174 @@
1
+ import { sleep } from '../utils';
2
+ import { Export, findModuleExport } from '../webpack';
3
+
4
+ export enum SideMenu {
5
+ None,
6
+ Main,
7
+ QuickAccess,
8
+ }
9
+
10
+ export enum QuickAccessTab {
11
+ Notifications,
12
+ RemotePlayTogetherControls,
13
+ VoiceChat,
14
+ Friends,
15
+ Settings,
16
+ Perf,
17
+ Help,
18
+ Music,
19
+ Decky = 999,
20
+ }
21
+
22
+ export enum DisplayStatus {
23
+ Invalid = 0,
24
+ Launching = 1,
25
+ Uninstalling = 2,
26
+ Installing = 3,
27
+ Running = 4,
28
+ Validating = 5,
29
+ Updating = 6,
30
+ Downloading = 7,
31
+ Synchronizing = 8,
32
+ ReadyToInstall = 9,
33
+ ReadyToPreload = 10,
34
+ ReadyToLaunch = 11,
35
+ RegionRestricted = 12,
36
+ PresaleOnly = 13,
37
+ InvalidPlatform = 14,
38
+ PreloadComplete = 16,
39
+ BorrowerLocked = 17,
40
+ UpdatePaused = 18,
41
+ UpdateQueued = 19,
42
+ UpdateRequired = 20,
43
+ UpdateDisabled = 21,
44
+ DownloadPaused = 22,
45
+ DownloadQueued = 23,
46
+ DownloadRequired = 24,
47
+ DownloadDisabled = 25,
48
+ LicensePending = 26,
49
+ LicenseExpired = 27,
50
+ AvailForFree = 28,
51
+ AvailToBorrow = 29,
52
+ AvailGuestPass = 30,
53
+ Purchase = 31,
54
+ Unavailable = 32,
55
+ NotLaunchable = 33,
56
+ CloudError = 34,
57
+ CloudOutOfDate = 35,
58
+ Terminating = 36,
59
+ }
60
+
61
+ export type AppOverview = {
62
+ appid: string;
63
+ display_name: string;
64
+ display_status: DisplayStatus;
65
+ sort_as: string;
66
+ };
67
+
68
+ export interface MenuStore {
69
+ OpenSideMenu(sideMenu: SideMenu): void;
70
+ OpenQuickAccessMenu(quickAccessTab?: QuickAccessTab): void;
71
+ OpenMainMenu(): void;
72
+ }
73
+
74
+ export interface WindowRouter {
75
+ BrowserWindow: Window;
76
+ MenuStore: MenuStore;
77
+ Navigate(path: string): void;
78
+ NavigateToChat(): void;
79
+ NavigateToSteamWeb(url: string): void;
80
+ NavigateBack(): void;
81
+ }
82
+
83
+ export interface WindowStore {
84
+ GamepadUIMainWindowInstance?: WindowRouter; // Current
85
+ SteamUIWindows: WindowRouter[];
86
+ OverlayWindows: WindowRouter[]; // Used by desktop GamepadUI
87
+ }
88
+
89
+ export interface Router {
90
+ WindowStore?: WindowStore;
91
+ CloseSideMenus(): void;
92
+ Navigate(path: string): void;
93
+ NavigateToAppProperties(): void;
94
+ NavigateToExternalWeb(url: string): void;
95
+ NavigateToInvites(): void;
96
+ NavigateToChat(): void;
97
+ NavigateToLibraryTab(): void;
98
+ NavigateToLayoutPreview(e: unknown): void;
99
+ OpenPowerMenu(unknown?: any): void;
100
+ get RunningApps(): AppOverview[];
101
+ get MainRunningApp(): AppOverview | undefined;
102
+ }
103
+
104
+ export const Router = findModuleExport((e: Export) => e.Navigate && e.NavigationManager) as Router;
105
+
106
+ export interface Navigation {
107
+ Navigate(path: string): void;
108
+ NavigateBack(): void;
109
+ NavigateToAppProperties(): void;
110
+ NavigateToExternalWeb(url: string): void;
111
+ NavigateToInvites(): void;
112
+ NavigateToChat(): void;
113
+ NavigateToLibraryTab(): void;
114
+ NavigateToLayoutPreview(e: unknown): void;
115
+ NavigateToSteamWeb(url: string): void;
116
+ OpenSideMenu(sideMenu: SideMenu): void;
117
+ OpenQuickAccessMenu(quickAccessTab?: QuickAccessTab): void;
118
+ OpenMainMenu(): void;
119
+ OpenPowerMenu(unknown?: any): void;
120
+ CloseSideMenus(): void;
121
+ }
122
+
123
+ export let Navigation = {} as Navigation;
124
+
125
+ try {
126
+ (async () => {
127
+ let InternalNavigators: any = {};
128
+ if (!Router.NavigateToAppProperties || (Router as unknown as any).deckyShim) {
129
+ function initInternalNavigators() {
130
+ try {
131
+ InternalNavigators = findModuleExport((e: Export) => e.GetNavigator && e.SetNavigator)?.GetNavigator();
132
+ } catch (e) {
133
+ console.error('[DFL:Router]: Failed to init internal navigators, trying again');
134
+ }
135
+ }
136
+ initInternalNavigators();
137
+ while (!InternalNavigators?.AppProperties) {
138
+ console.log('[DFL:Router]: Trying to init internal navigators again');
139
+ await sleep(2000);
140
+ initInternalNavigators();
141
+ }
142
+ }
143
+ const newNavigation = {
144
+ Navigate: Router.Navigate?.bind(Router),
145
+ NavigateBack: Router.WindowStore?.GamepadUIMainWindowInstance?.NavigateBack?.bind(
146
+ Router.WindowStore.GamepadUIMainWindowInstance,
147
+ ),
148
+ NavigateToAppProperties: InternalNavigators?.AppProperties || Router.NavigateToAppProperties?.bind(Router),
149
+ NavigateToExternalWeb: InternalNavigators?.ExternalWeb || Router.NavigateToExternalWeb?.bind(Router),
150
+ NavigateToInvites: InternalNavigators?.Invites || Router.NavigateToInvites?.bind(Router),
151
+ NavigateToChat: InternalNavigators?.Chat || Router.NavigateToChat?.bind(Router),
152
+ NavigateToLibraryTab: InternalNavigators?.LibraryTab || Router.NavigateToLibraryTab?.bind(Router),
153
+ NavigateToLayoutPreview: Router.NavigateToLayoutPreview?.bind(Router),
154
+ NavigateToSteamWeb: Router.WindowStore?.GamepadUIMainWindowInstance?.NavigateToSteamWeb?.bind(
155
+ Router.WindowStore.GamepadUIMainWindowInstance,
156
+ ),
157
+ OpenSideMenu: Router.WindowStore?.GamepadUIMainWindowInstance?.MenuStore.OpenSideMenu?.bind(
158
+ Router.WindowStore.GamepadUIMainWindowInstance.MenuStore,
159
+ ),
160
+ OpenQuickAccessMenu: Router.WindowStore?.GamepadUIMainWindowInstance?.MenuStore.OpenQuickAccessMenu?.bind(
161
+ Router.WindowStore.GamepadUIMainWindowInstance.MenuStore,
162
+ ),
163
+ OpenMainMenu: Router.WindowStore?.GamepadUIMainWindowInstance?.MenuStore.OpenMainMenu?.bind(
164
+ Router.WindowStore.GamepadUIMainWindowInstance.MenuStore,
165
+ ),
166
+ CloseSideMenus: Router.CloseSideMenus?.bind(Router),
167
+ OpenPowerMenu: Router.OpenPowerMenu?.bind(Router),
168
+ } as Navigation;
169
+
170
+ Object.assign(Navigation, newNavigation);
171
+ })();
172
+ } catch (e) {
173
+ console.error('[DFL:Router]: Error initializing Navigation interface', e);
174
+ }
@@ -0,0 +1 @@
1
+ export * from './Router';
package/src/plugin.tsx ADDED
@@ -0,0 +1,96 @@
1
+ import type { ComponentType, ReactNode } from 'react';
2
+ import { RouteProps } from 'react-router';
3
+
4
+ export interface Plugin {
5
+ title: JSX.Element;
6
+ icon: JSX.Element;
7
+ content?: JSX.Element;
8
+ onDismount?(): void;
9
+ alwaysRender?: boolean;
10
+ }
11
+
12
+ interface ServerResponseSuccess<TRes> {
13
+ success: true;
14
+ result: TRes;
15
+ }
16
+
17
+ interface ServerResponseError {
18
+ success: false;
19
+ result: string;
20
+ }
21
+
22
+ export type ServerResponse<TRes> = ServerResponseSuccess<TRes> | ServerResponseError;
23
+
24
+ export type RoutePatch = (route: RouteProps) => RouteProps;
25
+
26
+ export interface RouterHook {
27
+ addRoute(path: string, component: ComponentType, props?: Omit<RouteProps, 'path' | 'children'>): void;
28
+ addPatch(path: string, patch: RoutePatch): RoutePatch;
29
+ addGlobalComponent(name: string, component: ComponentType): void;
30
+ removeRoute(path: string): void;
31
+ removePatch(path: string, patch: RoutePatch): void;
32
+ removeGlobalComponent(name: string): void;
33
+ }
34
+
35
+ export interface ToastData {
36
+ title: ReactNode;
37
+ body: ReactNode;
38
+ onClick?: () => void;
39
+ logo?: ReactNode;
40
+ icon?: ReactNode;
41
+ className?: string;
42
+ contentClassName?: string;
43
+ duration?: number;
44
+ critical?: boolean;
45
+ eType?: number;
46
+ sound?: number;
47
+ playSound?: boolean;
48
+ showToast?: boolean;
49
+ }
50
+
51
+ export interface Toaster {
52
+ toast(toast: ToastData): void;
53
+ }
54
+
55
+ export interface FilePickerRes {
56
+ path: string;
57
+ realpath: string;
58
+ }
59
+
60
+ export const enum FileSelectionType {
61
+ FILE,
62
+ FOLDER,
63
+ }
64
+
65
+ export interface ServerAPI {
66
+ routerHook: RouterHook;
67
+ toaster: Toaster;
68
+ openFilePicker(startPath: string, includeFiles?: boolean, regex?: RegExp): Promise<FilePickerRes>;
69
+ openFilePickerV2(
70
+ select: FileSelectionType,
71
+ startPath: string,
72
+ includeFiles?: boolean,
73
+ includeFolders?: boolean,
74
+ filter?: RegExp | ((file: File) => boolean),
75
+ extensions?: string[],
76
+ showHiddenFiles?: boolean,
77
+ allowAllFiles?: boolean,
78
+ max?: number,
79
+ ): Promise<FilePickerRes>;
80
+ callPluginMethod<TArgs = {}, TRes = {}>(methodName: string, args: TArgs): Promise<ServerResponse<TRes>>;
81
+ callServerMethod<TArgs = {}, TRes = {}>(methodName: string, args: TArgs): Promise<ServerResponse<TRes>>;
82
+ fetchNoCors<TRes = {}>(url: RequestInfo, request?: RequestInit): Promise<ServerResponse<TRes>>;
83
+ executeInTab(tab: string, runAsync: boolean, code: string): Promise<unknown>;
84
+ injectCssIntoTab<TRes = string>(tab: string, style: string): Promise<ServerResponse<TRes>>;
85
+ removeCssFromTab(tab: string, cssId: string): Promise<unknown>;
86
+ }
87
+
88
+ type DefinePluginFn = (serverAPI: ServerAPI) => Plugin;
89
+
90
+ // TypeScript helper function
91
+ export const definePlugin = (fn: DefinePluginFn): DefinePluginFn => {
92
+ return (...args) => {
93
+ // TODO: Maybe wrap this
94
+ return fn(...args);
95
+ };
96
+ };
@@ -0,0 +1,43 @@
1
+ export * from './patcher';
2
+ export * from './react';
3
+ export * from './static-classes';
4
+
5
+ declare global {
6
+ var FocusNavController: any;
7
+ var GamepadNavTree: any;
8
+ }
9
+
10
+ export function joinClassNames(...classes: string[]): string {
11
+ return classes.join(' ');
12
+ }
13
+
14
+ export function sleep(ms: number) {
15
+ return new Promise((res) => setTimeout(res, ms));
16
+ }
17
+
18
+ /**
19
+ * Finds the SP window, since it is a render target as of 10-19-2022's beta
20
+ */
21
+ export function findSP(): Window {
22
+ // old (SP as host)
23
+ if (document.title == 'SP') return window;
24
+ // new (SP as popup)
25
+ const navTrees = getGamepadNavigationTrees();
26
+ return navTrees?.find((x: any) => x.m_ID == 'root_1_').Root.Element.ownerDocument.defaultView;
27
+ }
28
+
29
+ /**
30
+ * Gets the correct FocusNavController, as the Feb 22 2023 beta has two for some reason.
31
+ */
32
+ export function getFocusNavController(): any {
33
+ return window.GamepadNavTree?.m_context?.m_controller || window.FocusNavController;
34
+ }
35
+
36
+ /**
37
+ * Gets the gamepad navigation trees as Valve seems to be moving them.
38
+ */
39
+ export function getGamepadNavigationTrees(): any {
40
+ const focusNav = getFocusNavController();
41
+ const context = focusNav.m_ActiveContext || focusNav.m_LastActiveContext;
42
+ return context?.m_rgGamepadNavigationTrees;
43
+ }
@@ -0,0 +1,160 @@
1
+ // TODO: implement storing patches as an option so we can offer unpatchAll selectively
2
+ // Return this in a replacePatch to call the original method (can still modify args).
3
+ export let callOriginal = Symbol('DECKY_CALL_ORIGINAL');
4
+
5
+ export interface PatchOptions {
6
+ singleShot?: boolean;
7
+ }
8
+
9
+ type GenericPatchHandler = (args: any[], ret?: any) => any;
10
+
11
+ export interface Patch {
12
+ original: Function;
13
+ property: string;
14
+ object: any;
15
+ patchedFunction: any;
16
+ hasUnpatched: boolean;
17
+ handler: GenericPatchHandler;
18
+
19
+ unpatch: () => void;
20
+ }
21
+
22
+ // let patches = new Set<Patch>();
23
+
24
+ export function beforePatch(
25
+ object: any,
26
+ property: string,
27
+ handler: (args: any[]) => any,
28
+ options: PatchOptions = {},
29
+ ): Patch {
30
+ const orig = object[property];
31
+ object[property] = function (...args: any[]) {
32
+ handler.call(this, args);
33
+ const ret = patch.original.call(this, ...args);
34
+ if (options.singleShot) {
35
+ patch.unpatch();
36
+ }
37
+ return ret;
38
+ };
39
+ const patch = processPatch(object, property, handler, object[property], orig);
40
+ return patch;
41
+ }
42
+
43
+ export function afterPatch(
44
+ object: any,
45
+ property: string,
46
+ handler: (args: any[], ret: any) => any,
47
+ options: PatchOptions = {},
48
+ ): Patch {
49
+ const orig = object[property];
50
+ object[property] = function (...args: any[]) {
51
+ let ret = patch.original.call(this, ...args);
52
+ ret = handler.call(this, args, ret);
53
+ if (options.singleShot) {
54
+ patch.unpatch();
55
+ }
56
+ return ret;
57
+ };
58
+ const patch = processPatch(object, property, handler, object[property], orig);
59
+ return patch;
60
+ }
61
+
62
+ export function replacePatch(
63
+ object: any,
64
+ property: string,
65
+ handler: (args: any[]) => any,
66
+ options: PatchOptions = {},
67
+ ): Patch {
68
+ const orig = object[property];
69
+ object[property] = function (...args: any[]) {
70
+ const ret = handler.call(this, args);
71
+ if (ret == callOriginal) return patch.original.call(this, ...args);
72
+ if (options.singleShot) {
73
+ patch.unpatch();
74
+ }
75
+ return ret;
76
+ };
77
+ const patch = processPatch(object, property, handler, object[property], orig);
78
+ return patch;
79
+ }
80
+
81
+ function processPatch(
82
+ object: any,
83
+ property: any,
84
+ handler: GenericPatchHandler,
85
+ patchedFunction: any,
86
+ original: any,
87
+ ): Patch {
88
+ // Assign all props of original function to new one
89
+ Object.assign(object[property], original);
90
+ // Allow toString webpack filters to continue to work
91
+ object[property].toString = () => original.toString();
92
+
93
+ // HACK: for compatibility, remove when all plugins are using new patcher
94
+ Object.defineProperty(object[property], '__deckyOrig', {
95
+ get: () => patch.original,
96
+ set: (val: any) => (patch.original = val),
97
+ });
98
+
99
+ // Build a Patch object of this patch
100
+ const patch: Patch = {
101
+ object,
102
+ property,
103
+ handler,
104
+ patchedFunction,
105
+ original,
106
+ hasUnpatched: false,
107
+ unpatch: () => unpatch(patch),
108
+ };
109
+
110
+ object[property].__deckyPatch = patch;
111
+
112
+ return patch;
113
+ }
114
+
115
+ function unpatch(patch: Patch): void {
116
+ const { object, property, handler, patchedFunction, original } = patch;
117
+ if (patch.hasUnpatched) throw new Error('Function is already unpatched.');
118
+ let realProp = property;
119
+ let realObject = object;
120
+ console.debug('[Patcher] unpatching', {
121
+ realObject,
122
+ realProp,
123
+ object,
124
+ property,
125
+ handler,
126
+ patchedFunction,
127
+ original,
128
+ isEqual: realObject[realProp] === patchedFunction,
129
+ });
130
+
131
+ // If another patch has been applied to this function after this one, move down until we find the correct patch
132
+ while (realObject[realProp] && realObject[realProp] !== patchedFunction) {
133
+ realObject = realObject[realProp].__deckyPatch;
134
+ realProp = 'original';
135
+ console.debug('[Patcher] moved to next', {
136
+ realObject,
137
+ realProp,
138
+ object,
139
+ property,
140
+ handler,
141
+ patchedFunction,
142
+ original,
143
+ isEqual: realObject[realProp] === patchedFunction,
144
+ });
145
+ }
146
+
147
+ realObject[realProp] = realObject[realProp].__deckyPatch.original;
148
+
149
+ patch.hasUnpatched = true;
150
+ console.debug('[Patcher] unpatched', {
151
+ realObject,
152
+ realProp,
153
+ object,
154
+ property,
155
+ handler,
156
+ patchedFunction,
157
+ original,
158
+ isEqual: realObject[realProp] === patchedFunction,
159
+ });
160
+ }
@@ -0,0 +1,119 @@
1
+ import * as React from 'react';
2
+
3
+ // this shouldn't need to be redeclared but it does for some reason
4
+
5
+ declare global {
6
+ interface Window {
7
+ SP_REACT: typeof React;
8
+ }
9
+ }
10
+
11
+ export function fakeRenderComponent(fun: Function, customHooks: any = {}): any {
12
+ const hooks = (window.SP_REACT as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher
13
+ .current;
14
+
15
+ // TODO: add more hooks
16
+
17
+ let oldHooks = {
18
+ useContext: hooks.useContext,
19
+ useCallback: hooks.useCallback,
20
+ useLayoutEffect: hooks.useLayoutEffect,
21
+ useEffect: hooks.useEffect,
22
+ useMemo: hooks.useMemo,
23
+ useRef: hooks.useRef,
24
+ useState: hooks.useState,
25
+ };
26
+
27
+ hooks.useCallback = (cb: Function) => cb;
28
+ hooks.useContext = (cb: any) => cb._currentValue;
29
+ hooks.useLayoutEffect = (_: Function) => {}; //cb();
30
+ hooks.useMemo = (cb: Function, _: any[]) => cb;
31
+ hooks.useEffect = (_: Function) => {}; //cb();
32
+ hooks.useRef = (val: any) => ({ current: val || {} });
33
+ hooks.useState = (v: any) => {
34
+ let val = v;
35
+
36
+ return [val, (n: any) => (val = n)];
37
+ };
38
+
39
+ Object.assign(hooks, customHooks);
40
+
41
+ const res = fun(hooks);
42
+
43
+ Object.assign(hooks, oldHooks);
44
+
45
+ return res;
46
+ }
47
+
48
+ export function wrapReactType(node: any, prop: any = 'type') {
49
+ if (node[prop]?.__DECKY_WRAPPED) {
50
+ return node[prop];
51
+ } else {
52
+ return (node[prop] = { ...node[prop], __DECKY_WRAPPED: true });
53
+ }
54
+ }
55
+
56
+ export function wrapReactClass(node: any, prop: any = 'type') {
57
+ if (node[prop]?.__DECKY_WRAPPED) {
58
+ return node[prop];
59
+ } else {
60
+ const cls = node[prop];
61
+ const wrappedCls = class extends cls {
62
+ static __DECKY_WRAPPED = true;
63
+ };
64
+ return (node[prop] = wrappedCls);
65
+ }
66
+ }
67
+
68
+ export function getReactRoot(o: HTMLElement | Element | Node) {
69
+ return (
70
+ // @ts-expect-error 7053
71
+ o[Object.keys(o).find((k) => k.startsWith('__reactContainer$')) as string] ||
72
+ // @ts-expect-error 7053
73
+ o['_reactRootContainer']?._internalRoot?.current
74
+ );
75
+ }
76
+
77
+ export function getReactInstance(o: HTMLElement | Element | Node) {
78
+ return (
79
+ // @ts-expect-error 7053
80
+ o[Object.keys(o).find((k) => k.startsWith('__reactFiber')) as string] ||
81
+ // @ts-expect-error 7053
82
+ o[Object.keys(o).find((k) => k.startsWith('__reactInternalInstance')) as string]
83
+ );
84
+ }
85
+
86
+ // Based on https://github.com/GooseMod/GooseMod/blob/9ef146515a9e59ed4e25665ed365fd72fc0dcf23/src/util/react.js#L20
87
+ export interface findInTreeOpts {
88
+ walkable?: string[];
89
+ ignore?: string[];
90
+ }
91
+
92
+ export declare type findInTreeFilter = (element: any) => boolean;
93
+
94
+ export const findInTree = (parent: any, filter: findInTreeFilter, opts: findInTreeOpts): any => {
95
+ const { walkable = null, ignore = [] } = opts ?? {};
96
+
97
+ if (!parent || typeof parent !== 'object') {
98
+ // Parent is invalid to search through
99
+ return null;
100
+ }
101
+
102
+ if (filter(parent)) return parent; // Parent matches, just return
103
+
104
+ if (Array.isArray(parent)) {
105
+ // Parent is an array, go through values
106
+ return parent.map((x) => findInTree(x, filter, opts)).find((x) => x);
107
+ }
108
+
109
+ // Parent is an object, go through values (or option to only use certain keys)
110
+ return (walkable || Object.keys(parent))
111
+ .map((x) => !ignore.includes(x) && findInTree(parent[x], filter, opts))
112
+ .find((x: any) => x);
113
+ };
114
+
115
+ export const findInReactTree = (node: any, filter: findInTreeFilter) =>
116
+ findInTree(node, filter, {
117
+ // Specialised findInTree for React nodes
118
+ walkable: ['props', 'children', 'child', 'sibling'],
119
+ });