Compare commits

..

278 Commits

Author SHA1 Message Date
semantic-release-bot
93c59d8026 chore(release): 4.7.3 [CI SKIP] 2024-10-03 20:50:33 +00:00
AAGaming
0f9fb5a3b8 fix(components): fix missing components on oct 2 2024 beta 2024-10-03 16:46:50 -04:00
semantic-release-bot
8c6043b5a7 chore(release): 4.7.2 [CI SKIP] 2024-09-16 23:30:08 +00:00
AAGaming
3aa07dc9ce fix(utils): fix potential race condition in findSP 2024-09-16 19:29:38 -04:00
semantic-release-bot
3f335b3583 chore(release): 4.7.1 [CI SKIP] 2024-08-08 18:18:52 +00:00
AAGaming
4c97097757 fix(utils/react): fix potential race condition in injectFCTrampoline 2024-08-08 14:18:19 -04:00
AAGaming
1ee43e86d6 chore(utils/react): get rid of ts-expect-error 2024-08-03 13:50:33 -04:00
AAGaming
db6ab9c448 chore(docs): add note to CloseSideMenus 2024-07-28 18:51:08 -04:00
semantic-release-bot
4646f22b0c chore(release): 4.7.0 [CI SKIP] 2024-07-28 22:18:51 +00:00
AAGaming
7eb484d55c feat(router): support desktop bpm overlay 2024-07-28 18:17:52 -04:00
AAGaming
5164f980b3 chore(stores): add SteamUIStore, securitystore 2024-07-28 18:15:51 -04:00
AAGaming
0457feec95 chore(tabs): port back to normal find 2024-07-28 18:15:34 -04:00
semantic-release-bot
73b8d52c7f chore(release): 4.6.0 [CI SKIP] 2024-07-26 18:33:10 +00:00
AAGaming
2b8d2ae4db feat(classMapper): add findClassByName back 2024-07-26 14:32:34 -04:00
AAGaming
48de8928e4 chore(deprecation): deprecate useQuickAccessVisible as it has been moved to @decky/api 2024-07-26 14:07:58 -04:00
semantic-release-bot
d60715b755 chore(release): 4.5.0 [CI SKIP] 2024-07-24 04:59:59 +00:00
AAGaming
a370c1f7d3 feat(classMapper): add classModuleMap, make findClass require ID 2024-07-24 00:59:28 -04:00
AAGaming
d83bada4af feat(webpack): refactor to prepare for classMapper changes 2024-07-24 00:59:04 -04:00
semantic-release-bot
1f7178f175 chore(release): 4.4.0 [CI SKIP] 2024-07-18 04:55:06 +00:00
AAGaming
44fdf9ed3b feat(utils/react): add injectFCTrampoline 2024-07-18 00:54:29 -04:00
semantic-release-bot
1b5fd78bc2 chore(release): 4.3.1 [CI SKIP] 2024-07-17 23:30:02 +00:00
AAGaming
d64c42ac31 fix(Menu/MenuGroup): rewrite filter to work on beta and prevent future errors 2024-07-17 19:29:25 -04:00
semantic-release-bot
0ec60edf4d chore(release): 4.3.0 [CI SKIP] 2024-07-09 06:38:22 +00:00
AAGaming
2c3a9f81de feat(utils): react tree patching api 2024-07-09 02:37:46 -04:00
semantic-release-bot
00079a71cc chore(release): 4.2.2 [CI SKIP] 2024-07-04 05:18:57 +00:00
AAGaming
c04f024b34 fix(SteamSpinner): add background option 2024-07-04 01:18:27 -04:00
AAGaming
5dae77b003 chore(ci): update actions to remove deprecation warnings 2024-06-29 17:53:00 -04:00
semantic-release-bot
6b8c28e69c chore(release): 4.2.1 [CI SKIP] 2024-06-27 04:17:41 +00:00
AAGaming
3ef9648355 fix(errorboundary): work around broken react types
for the billionth time
2024-06-27 00:16:14 -04:00
semantic-release-bot
0da85355c2 chore(release): 4.2.0 [CI SKIP] 2024-06-27 04:07:06 +00:00
AAGaming
dcdbb2d6c7 feat(components): add ErrorBoundary 2024-06-27 00:06:32 -04:00
semantic-release-bot
58e3d35e1e chore(release): 4.1.1 [CI SKIP] 2024-06-27 03:19:49 +00:00
Ava Johnson
bd1dc85b92 fix(ReorderableList): avoid mutating props (#109)
Co-authored-by: AAGaming <aagaming@riseup.net>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-26 23:19:18 -04:00
semantic-release-bot
aa78f4c0a2 chore(release): 4.1.0 [CI SKIP] 2024-06-27 03:11:23 +00:00
AAGaming
b04044451a feat(release): release v4.1.0 2024-06-26 23:10:47 -04:00
semantic-release-bot
5d8f141114 chore(release): 4.0.1 [CI SKIP] 2024-06-27 03:10:04 +00:00
AAGaming
2bfe62409f fix(release): empty commit to bump to v4.0.1 2024-06-26 23:09:36 -04:00
semantic-release-bot
f5cefa9eea chore(release): 4.0.0 [CI SKIP] 2024-06-27 03:08:08 +00:00
AAGaming
abfd2c0105 fix(ci): temp remove npm publisher
BREAKING CHANGE: v4 release
2024-06-26 23:07:29 -04:00
AAGaming
8cb7c273eb fix(ci): empty commit so semantic-release shuts up
BREAKING CHANGE: v4 release
2024-06-26 23:03:39 -04:00
AAGaming
c440fa76b1 chore(ci): empty commit so semantic-release shuts up 2024-06-26 23:00:30 -04:00
AAGaming
4e4859deb6 chore(ci): empty commit so semantic-release shuts up 2024-06-26 22:56:50 -04:00
AAGaming
9579c41c6d chore(ci): empty commit so semantic-release shuts up 2024-06-26 22:53:34 -04:00
AAGaming
4dd6afd91b chore(ci): add v4-dev to release branches 2024-06-26 22:41:52 -04:00
AAGaming
b092d49bf8 chore(ci): return of the CI 2024-06-26 22:39:30 -04:00
AAGaming
7e0cb153b1 fix(*): fixes for jun 26 beta 2024-06-26 22:28:58 -04:00
AAGaming
b1e503853f fix(package): unbreak react 2024-06-13 18:26:38 -04:00
AAGaming
832ad697c7 chore(package): v4.0.3 2024-06-13 18:23:06 -04:00
AAGaming
62b454f712 fix(package): pin react properly 2024-06-13 18:22:18 -04:00
AAGaming
ea19d62dab fix(package): fix ts memes 2024-06-13 17:00:37 -04:00
AAGaming
9f262097b9 fix(Menu): work around ConfigContext error 2024-06-13 15:53:06 -04:00
AAGaming
e6e8c91ec7 fix(components): forgot one 2024-05-27 13:20:03 -04:00
AAGaming
688c7471cd fix(components): fix missing children prop
also remove unnessecary use of tsx
2024-05-27 13:19:11 -04:00
AAGaming
6c1b12b95f chore(ci): disable on v4 for now 2024-05-27 13:15:49 -04:00
semantic-release-bot
0dfb2cedb6 chore(release): 3.26.1 [CI SKIP] 2024-05-25 23:15:00 +00:00
AAGaming
2a78cc0116 fix(Field): add children prop 2024-05-25 19:14:30 -04:00
AAGaming
1716782183 chore(package): ver bump [skip ci] 2024-05-24 18:04:31 -04:00
AAGaming
0cd498beac fix(plugin): shim definePlugin for now [ci skip] 2024-05-24 18:04:05 -04:00
AAGaming
b728d90263 chore(plugin): delete 2024-05-24 17:10:51 -04:00
AAGaming
3384d2910d chore(*): fix version lol 2024-05-24 16:35:31 -04:00
semantic-release-bot
78c4cdb9e9 chore(release): 3.26.0 [CI SKIP] 2024-05-24 20:24:30 +00:00
AAGaming
d20659e073 chore(releaserc): add v4-dev branch 2024-05-24 16:23:59 -04:00
AAGaming
d91f49e728 fix(utils/react): shut it ts 2024-05-24 16:22:51 -04:00
AAGaming
735080fee3 chore(*): init v4 package.json and release.yaml 2024-05-24 16:21:58 -04:00
jbofill
86e6e4c0f5 chore(static-classes): add/fix/update css classes (#102) 2024-05-24 16:17:48 -04:00
shadow
46b1d6e8ea Dropdown contextMenuPositionOptions prop definition (#104) 2024-05-24 16:17:12 -04:00
AAGaming
a7635b6305 chore(*): run prettier 2024-05-12 15:48:13 -04:00
AAGaming
bffd530bda feat(*): add v4 webpack api and port everything to it
also restructures a bunch
2024-05-12 15:45:26 -04:00
AAGaming
9c79187d37 chore(package): init v4 2024-05-12 12:43:26 -04:00
semantic-release-bot
29e6439115 chore(release): 3.25.0 [CI SKIP] 2024-03-09 22:17:49 +00:00
AAGaming
a8eeb917e2 feat(classMapper): add class mapper
thanks valve
2024-03-09 17:13:39 -05:00
AAGaming
17b99dfed8 fix(staticclasses): unbreak on latest beta
unsure if this works for all of them but it works Enough
2024-03-09 17:13:05 -05:00
AAGaming
9ef419cee1 chore(modal): remove useless comments 2024-02-03 00:49:11 -05:00
semantic-release-bot
5aad952936 chore(release): 3.24.5 [CI SKIP] 2024-02-03 05:32:01 +00:00
AAGaming
c2b0fad298 fix(finds): make modal and scroll components work on latest betaa
THEY INCREASED THE MINIFIER PRESET AAAAAAAAAAAAAAAAAAAAAAAAAA
2024-02-03 00:31:29 -05:00
semantic-release-bot
4d4cfedfe0 chore(release): 3.24.4 [CI SKIP] 2024-01-22 19:56:22 +00:00
AAGaming
bb12921863 fix(types): fix incorrect as on many components leading to any types 2024-01-22 14:55:52 -05:00
semantic-release-bot
223739af25 chore(release): 3.24.3 [CI SKIP] 2024-01-20 03:44:07 +00:00
AAGaming
95d977df45 fix(router): wait 2s if internal navigators init fails 2024-01-19 22:43:38 -05:00
semantic-release-bot
0f2692a3f2 chore(release): 3.24.2 [CI SKIP] 2024-01-20 01:40:41 +00:00
Beebles
ebf496bf61 fix(navigation): Fix on chromium 109 (#100) 2024-01-19 18:40:11 -07:00
semantic-release-bot
0a5170e412 chore(release): 3.24.1 [CI SKIP] 2023-12-13 03:08:36 +00:00
AAGaming
11dd82bbb1 fix(utils/react): support react 18, add getReactRoot 2023-12-12 22:07:59 -05:00
semantic-release-bot
153bb209d1 chore(release): 3.24.0 [CI SKIP] 2023-12-03 22:29:55 +00:00
Lukas Senionis
e27b638d26 feat(static-classes): add BasicAppDetailsSectionStylerClasses (#99) 2023-12-03 17:29:30 -05:00
AAGaming
7d287f10d6 chore(releaserc): fix typo 2023-11-26 15:01:54 -05:00
AAGaming
9925bc8cfb chore(releaserc): add docs(steamclient) release patch target 2023-11-26 15:01:33 -05:00
semantic-release-bot
179a93d5e1 chore(release): 3.23.1 [CI SKIP] 2023-11-09 19:45:32 +00:00
AAGaming
5203ce348a fix(webpack): don't break if a module fails to load
doesn't matter now but could in the future so might as well fix it now
2023-11-09 14:44:41 -05:00
dependabot[bot]
3f47b5ccce chore(deps-dev): bump @babel/traverse from 7.21.4 to 7.23.2 (#98) 2023-10-20 13:58:41 +00:00
semantic-release-bot
d22d32677b chore(release): 3.23.0 [CI SKIP] 2023-10-11 16:06:34 +00:00
jbofill
30e319425b feat(static-classes): add more css classes (#94) 2023-10-11 12:06:01 -04:00
semantic-release-bot
aebdfaa089 chore(release): 3.22.1 [CI SKIP] 2023-10-10 13:38:32 +00:00
TrainDoctor
de914b1a35 Merge pull request #97 from FrogTheFrog/patch-29
fix(useQuickAccessVisible): use the "Page Visibility API" instead of focus/blur
2023-10-10 06:37:52 -07:00
Lukas Senionis
4c4fda47e3 fix(useQuickAccessVisible): use the "Page Visibility API" instead of focus/blur 2023-10-09 21:38:34 +03:00
semantic-release-bot
678084e4fd chore(release): 3.22.0 [CI SKIP] 2023-08-09 22:23:32 +00:00
TrainDoctor
4f8f65d429 feat: add components found while working on tabmaster 2023-08-09 15:22:56 -07:00
Tormak
8a352d288d chore: adjusted per aa's request 2023-08-09 13:17:52 -05:00
Tormak
fd0d011cbf feat: add components found while working on tabmaster 2023-07-10 07:47:03 -05:00
Travis Lane
70219d90bc Merge branch 'SteamDeckHomebrew:main' into main 2023-07-10 07:32:04 -05:00
semantic-release-bot
1ff9351f96 chore(release): 3.21.8 [CI SKIP] 2023-06-27 13:38:46 +00:00
Marco Rodolfi
c256d341c8 Merge pull request #89 from SteamDeckHomebrew/RodoMa92-patch-1
fix: the typescript compiler was eating the enum
2023-06-27 15:38:13 +02:00
Marco Rodolfi
c9b583945c fix: the typescript compiler was eating the enum
If not defined with a const keyword
2023-06-27 15:38:04 +02:00
semantic-release-bot
c213204ff4 chore(release): 3.21.7 [CI SKIP] 2023-06-26 16:30:09 +00:00
Travis Lane
9128c1e7da fix: add patch indicator to prevent crashes (#88) 2023-06-26 12:29:21 -04:00
Travis Lane
a06650cf09 Update react.ts with requested changes 2023-06-26 11:26:45 -05:00
Tormak
3170779c6b fix: add patch indicator to prevent crashes 2023-06-25 17:23:45 -05:00
semantic-release-bot
5ffa14bec8 chore(release): 3.21.6 [CI SKIP] 2023-06-22 15:27:59 +00:00
Marco Rodolfi
ee51dc5fc0 fix: reposition parameter for file picker V2 2023-06-22 17:27:14 +02:00
semantic-release-bot
b38ec17d8f chore(release): 3.21.5 [CI SKIP] 2023-06-22 10:16:29 +00:00
Marco Rodolfi
8b54ee990e fix: missing parameter 2023-06-22 12:15:08 +02:00
Marco Rodolfi
7e01781d40 chore: better order for file picker v2 call 2023-06-22 11:59:32 +02:00
semantic-release-bot
2b31473614 chore(release): 3.21.4 [CI SKIP] 2023-06-22 09:39:28 +00:00
Marco Rodolfi
8bfeae4b35 fix: move the new file picker api as v2 2023-06-22 11:38:45 +02:00
semantic-release-bot
a7761321bc chore(release): 3.21.3 [CI SKIP] 2023-06-22 08:41:58 +00:00
Marco Rodolfi
979a630f2b fix: total is not needed as a return value to plugins 2023-06-22 10:41:09 +02:00
Marco Rodolfi
a4d1fcf086 Merge pull request #86 from SteamDeckHomebrew/file_picker_call
chore: add new interface for openfilepicker
2023-06-22 10:13:39 +02:00
Marco Rodolfi
e9e7f5c026 chore: add new interface for openfilepicker 2023-06-22 10:09:01 +02:00
semantic-release-bot
9a691cea09 chore(release): 3.21.2 [CI SKIP] 2023-06-18 17:15:58 +00:00
AAGaming
44d9b90cdc fix(SidebarNavigation): allow ReactNode for page title 2023-06-18 13:15:21 -04:00
TrainDoctor
585c3b8348 Merge pull request #85 from Woovie/patch-1
Update Discord link
2023-06-07 17:35:53 -07:00
Jordan Banasik
1287493b63 Update Discord link 2023-06-07 17:15:40 -07:00
semantic-release-bot
bf01a0184b chore(release): 3.21.1 [CI SKIP] 2023-05-28 23:16:04 +00:00
Jonas Dellinger
fb5f043ba9 fix(reorderable-list): open label type from string to ReactNode 2023-05-29 01:14:59 +02:00
semantic-release-bot
26f2c92dce chore(release): 3.21.0 [CI SKIP] 2023-05-19 14:04:38 +00:00
TrainDoctor
01e53d4c13 Merge pull request #84 from Maclay74/feature/gamepad-ui-classes
Gamepad UI classes, fix findSP to return null when not found
2023-05-19 07:03:59 -07:00
Mikhail Kozlov
18d341f82a fix(Utils): return null when SP not found 2023-05-18 11:41:45 -07:00
Mikhail Kozlov
3ca8c43a54 feat(StaticClasses): add new GamepadUI classes 2023-05-18 11:40:41 -07:00
semantic-release-bot
99353467ad chore(release): 3.20.7 [CI SKIP] 2023-05-10 20:47:47 +00:00
Party Wumpus
b093ce93ba Merge pull request #83 from RodoMa92/main
fix(decky): fix decky on latest beta
2023-05-10 21:46:57 +01:00
Marco Rodolfi
74a7cbaf94 fix(decky): fix decky on latest beta
Thanks to AAGaming and McDjuady for fixing this stuff.
2023-05-10 20:38:14 +02:00
semantic-release-bot
8fb35e3c09 chore(release): 3.20.6 [CI SKIP] 2023-04-29 02:26:37 +00:00
AAGaming
5d5cb31638 fix(QuickAccessTab): set decky tab ID to 999 2023-04-28 22:25:56 -04:00
semantic-release-bot
21f1e5f0af chore(release): 3.20.5 [CI SKIP] 2023-04-04 21:27:14 +00:00
TrainDoctor
0fa43701a9 Merge pull request #82 from Tormak9970/main
fix: reorderable list no longer toggles on backout
2023-04-04 14:26:37 -07:00
Tormak
4d52eaea12 Merge branch 'main' of https://github.com/Tormak9970/decky-frontend-lib 2023-04-04 17:04:22 -05:00
Tormak
7e1182a83f fix: reorderable list no longer toggles on backout 2023-04-04 17:04:18 -05:00
semantic-release-bot
167ded103a chore(release): 3.20.4 [CI SKIP] 2023-04-04 03:16:30 +00:00
AAGaming
b8ddf3d927 fix(SteamSpinner): oh apparently the class was moved outside the component for some reason?????? 2023-04-03 23:15:51 -04:00
semantic-release-bot
62f4b354a8 chore(release): 3.20.3 [CI SKIP] 2023-04-04 02:53:53 +00:00
AAGaming
79d229be50 fix(SteamSpinner): fix the fix 2023-04-03 22:53:10 -04:00
semantic-release-bot
f43157fde5 chore(release): 3.20.2 [CI SKIP] 2023-04-04 02:50:17 +00:00
AAGaming
19819b7a5b fix(SteamSpinner): dont error on latest desktop beta 2023-04-03 22:49:31 -04:00
semantic-release-bot
8752f576a7 chore(release): 3.20.1 [CI SKIP] 2023-04-03 22:18:43 +00:00
TrainDoctor
102a441124 Merge pull request #79 from FrogTheFrog/main
fix(useQuickAccessVisible): make it work again
2023-04-03 15:18:07 -07:00
semantic-release-bot
62f40bd48f chore(release): 3.20.0 [CI SKIP] 2023-04-03 20:49:24 +00:00
TrainDoctor
a7acc9ae45 Merge pull request #80 from Tormak9970/main
Add save on backout to reorderable list component.
2023-04-03 13:48:35 -07:00
Travis Lane
be7f17a3c3 Merge pull request #2 from Tormak9970/reorderable-list
chore: bumped pnpm lockfile version
2023-04-03 16:47:18 -04:00
Tormak
5d097b6108 chore: bumped pnpm lockfile version 2023-04-03 16:44:13 -04:00
Travis Lane
ae98930eeb Merge pull request #1 from Tormak9970/reorderable-list
Reorderable list now saves on backout
2023-03-29 10:18:32 -04:00
Travis Lane
6ce0da996d Merge branch 'main' into reorderable-list 2023-03-29 10:18:23 -04:00
Tormak
b1591f86bb feat: reorderable list now saves on backout 2023-03-29 07:10:55 -05:00
TrainDoctor
f7318f0210 Merge pull request #78 from Tormak9970/patch-1 2023-03-23 07:40:28 -07:00
Travis Lane
1a34501868 updated to use 'separator' instead of string 2023-03-22 20:17:58 -05:00
FrogTheFrog
7dacb23e8b fix(useQuickAccessVisible): make it work again 2023-03-19 22:25:53 +02:00
semantic-release-bot
7e5c7b5ac3 chore(release): 3.19.2 [CI SKIP] 2023-03-07 23:48:45 +00:00
Beebles
f71e4dedc8 Merge pull request #71 from doZennn/patch-1
fix(Item): add highlightOnFocus prop
2023-03-07 16:48:10 -07:00
Travis Lane
419835204e Add support for SidebarNavigation separator
Looked into it and in order to render a separator you need to pass `"separator"`
2023-03-06 11:15:36 -06:00
semantic-release-bot
86f33de2c0 chore(release): 3.19.1 [CI SKIP] 2023-02-23 02:57:40 +00:00
AAGaming
0b6dc24c0d fix(*): refactoring to fix for feb 22 2023 beta 2023-02-22 21:56:46 -05:00
semantic-release-bot
18ce1ad790 chore(release): 3.19.0 [CI SKIP] 2023-02-22 03:38:02 +00:00
Travis Lane
5a074b5bb6 feat: added reorderable list and updated fieldProps (#57) 2023-02-22 03:37:26 +00:00
AAGaming
26fae13c8e feat(ReorderableList): add animations, clean up 2023-02-21 22:36:56 -05:00
AAGaming
53faf55df1 fix(Field): remove style 2023-02-21 22:36:35 -05:00
Tormak
b480d397c4 fix: fixed missing export 2023-02-21 19:06:08 -06:00
Tormak
dfcb3bec19 feat: added doc comments 2023-02-21 12:22:21 -06:00
Travis Lane
cf7dc26a0c Merge branch 'SteamDeckHomebrew:main' into main 2023-02-21 12:22:12 -05:00
semantic-release-bot
d3b87b26c5 chore(release): 3.18.11 [CI SKIP] 2023-02-18 23:24:21 +00:00
TrainDoctor
b64dd9f723 Merge pull request #75 from beebls/main
fix(Navigation): fix NavigateToExternalWeb and ModalRoot
2023-02-18 15:23:49 -08:00
beebls
82214fef4c fix(Navigation): fix NavigateToExternalWeb 2023-02-18 15:51:31 -07:00
Jozen Blue Martinez
e559a43af9 Add missing types (#76)
* feat(SteamAppOverview): Add types

* feat(SliderFieldProps): Add className type

* feat(ToggleFieldProps): Add highlightOnFocus type

* feat(AppDetails): Add types for libraryAssets.logoPosition

* feat(types): Add types to some globals
2023-02-18 19:58:50 +00:00
beebls
c53d7f8448 fix(Navigation): fix NavigateToExternalWeb 2023-02-17 18:21:48 -07:00
AAGaming
68a46263a4 chore(docs): update wikijs typedoc plugin 2023-02-04 19:45:51 -05:00
AAGaming
56e7b2c492 chore(docs): fix workflow oops 2023-02-04 19:39:32 -05:00
AAGaming
1282636b0b chore(docs): revert workflow changes 2023-02-04 19:38:47 -05:00
AAGaming
2343274a5b chore(docs): update wikijs typedoc plugin 2023-02-04 19:36:38 -05:00
AAGaming
f965c05c27 chore(docs): fix path 2023-02-04 17:20:34 -05:00
AAGaming
f8c8ae7c29 chore(docs): update typedoc-wikijs-theme to fix bug 2023-02-04 17:18:52 -05:00
AAGaming
3085efd439 chore(docs): fix mv 2023-02-04 17:17:05 -05:00
AAGaming
db8d91a7ff chore(docs): bump typedoc-wikijs-theme and update docs workflow 2023-02-04 17:14:27 -05:00
AAGaming
2091daaeac chore(docs): update doc generation to use typedoc-wikijs-theme, also bump typescript 2023-02-04 16:55:52 -05:00
Tormak
b1b2f4fa2d feat: made requested changes and ran prettier 2023-02-04 11:39:08 -06:00
Tormak
b146eab8d7 fix: list didn't update on prop change 2023-02-03 15:33:03 -06:00
Tormak
667933bd7c refactor: addressed change reqs 2023-02-01 17:42:03 -06:00
Tormak
dbd01b11ca feat: support for non-interactable reordering 2023-02-01 17:24:17 -06:00
Tormak
47a6fddc89 feat: support for user specified icon 2023-02-01 15:33:27 -06:00
Tormak
c57e0eed34 refactor: changes to improve deck cohesion 2023-02-01 15:25:23 -06:00
Tormak
5adc5e14ed refactor: updated to simpler reorderable list 2023-02-01 08:18:55 -06:00
Tormak
cea315a52c feat: refactoring mostly complete 2023-01-28 17:19:24 -06:00
Tormak
5b166d6db8 feat: changed ReorderableList to working version 2023-01-28 16:53:22 -06:00
semantic-release-bot
edcc43a6ee chore(release): 3.18.10 [CI SKIP] 2023-01-17 00:35:19 +00:00
AAGaming
9723854ddc fix(SuspensefulImage): fix changing src 2023-01-16 19:34:44 -05:00
semantic-release-bot
7501817b76 chore(release): 3.18.9 [CI SKIP] 2023-01-16 14:13:05 +00:00
AAGaming
4affd4aaec fix(Navigation): fix on stable 2023-01-16 09:12:16 -05:00
semantic-release-bot
6b3db72a14 chore(release): 3.18.8 [CI SKIP] 2023-01-16 01:23:38 +00:00
AAGaming
58b69f0d6c fix(Navigation): fix timing issue in decky-loader 2023-01-15 20:22:54 -05:00
semantic-release-bot
c62102e993 chore(release): 3.18.7 [CI SKIP] 2023-01-16 00:19:36 +00:00
AAGaming
2e66e5a555 fix: un-break navigation on stable 2023-01-15 19:18:47 -05:00
semantic-release-bot
ce525318d8 chore(release): 3.18.6 [CI SKIP] 2023-01-13 02:55:51 +00:00
AAGaming
aac2d520a6 fix(Router): fix Navigation for the millionth time 2023-01-12 21:55:08 -05:00
Travis Lane
e761ee02ba Merge branch 'SteamDeckHomebrew:main' into main 2023-01-07 20:14:33 -05:00
Jozen Blue Martinez
c53f87b4a9 fix(Item): add highlightOnFocus prop 2022-12-29 15:36:08 +08:00
semantic-release-bot
a656f4e57f chore(release): 3.18.5 [CI SKIP] 2022-12-21 15:57:16 +00:00
noot
0b50f2cf0b fix: fixed prop interfaces (#70) 2022-12-21 10:56:39 -05:00
semantic-release-bot
e48c7bbadd chore(release): 3.18.4 [CI SKIP] 2022-12-16 02:04:28 +00:00
TrainDoctor
727fcc8186 Merge pull request #67 from SteamDeckHomebrew/aa/fix-modals-dec2022
fix(modals): fix ModalRoot again
2022-12-15 18:03:27 -08:00
semantic-release-bot
dc196d53f5 chore(release): 3.18.3 [CI SKIP] 2022-12-12 23:49:39 +00:00
Beebles
f0379e5d19 fix(Router): update Router interface to SteamOS3.4 and add Navigation (#52) 2022-12-12 18:48:52 -05:00
AAGaming
fd94842647 fix(modals): fix ModalRoot again 2022-12-11 19:52:20 -05:00
semantic-release-bot
ef6be8c6ec chore(release): 3.18.2 [CI SKIP] 2022-12-11 20:12:52 +00:00
Lukas Senionis
767dc2fcee fix(useQuickAccessVisible): remove invalid prop access (#66) 2022-12-11 15:12:22 -05:00
semantic-release-bot
52305987c5 chore(release): 3.18.1 [CI SKIP] 2022-12-11 20:02:11 +00:00
Lukas Senionis
6f14da152a fix(findSP): fallback to last active context (#53) 2022-12-11 15:01:41 -05:00
semantic-release-bot
bb291b211c chore(release): 3.18.0 [CI SKIP] 2022-12-11 15:10:33 +00:00
Jozen Blue Martinez
88f245d476 feat(DialogCheckbox): Add DialogCheckbox component (#58)
* feat(DialogCheckbox): Add DialogCheckbox component

* fix(DialogCheckbox): Better sibling match

* fix(DialogCheckbox): Extend FocusableProps

* fix(DialogCheckbox): Extend FooterLegendProps

i should have probably tested that

* feat(DialogCheckbox): add onClick() prop

* fix(DialogCheckbox): replace default export with named export
2022-12-11 16:10:01 +01:00
semantic-release-bot
5bc78df918 chore(release): 3.17.0 [CI SKIP] 2022-12-11 15:08:02 +00:00
Jozen Blue Martinez
c586afb97d feat(ControlsList): Add ControlsList component (#61)
* feat(ControlsList): Add ControlsList component

* fix(ControlsList): replace default export with named export
2022-12-11 16:07:31 +01:00
semantic-release-bot
d9150c2556 chore(release): 3.16.2 [CI SKIP] 2022-12-11 14:48:15 +00:00
Jonas Dellinger
cd0635e94f fix(Marquee): replace default export with named export 2022-12-11 15:47:39 +01:00
semantic-release-bot
443c7850d7 chore(release): 3.16.1 [CI SKIP] 2022-12-11 14:46:16 +00:00
Jozen Blue Martinez
d24136ecb6 fix(FooterLegend): change description types to ReactNode (#62) 2022-12-11 15:45:40 +01:00
semantic-release-bot
55507446cc chore(release): 3.16.0 [CI SKIP] 2022-12-11 14:45:21 +00:00
Jozen Blue Martinez
925ea8c3ce feat(Marquee): Add Marquee component (#63) 2022-12-11 15:44:50 +01:00
semantic-release-bot
14c5210931 chore(release): 3.15.0 [CI SKIP] 2022-12-11 14:18:49 +00:00
Jozen Blue Martinez
cc29ddaf57 feat(Focusable): add noFocusRing prop type (#65) 2022-12-11 15:18:07 +01:00
semantic-release-bot
1e8979b641 chore(release): 3.14.0 [CI SKIP] 2022-12-10 00:14:13 +00:00
jurassicplayer
7ba1229a4e feat(toast): add showToast/playSound to ToastData (#64) 2022-12-09 19:13:37 -05:00
semantic-release-bot
4c2a715324 chore(release): 3.13.0 [CI SKIP] 2022-11-29 20:01:20 +00:00
Jozen Blue Martinez
678ba216f1 feat(Menu): add more missing props (#60) [CI SKIP]
* feat(Menu): extend FooterLegendProps

* feat(MenuItem): add more missing props
2022-11-29 21:00:45 +01:00
semantic-release-bot
07d15f5dca chore(release): 3.12.0 [CI SKIP] 2022-11-28 12:03:43 +00:00
Jozen Blue Martinez
c84a091469 feat(MenuItem): add missing props (#59) 2022-11-28 07:03:07 -05:00
Tormak
3c171cfb8f feat: added reorderable list and updated fieldProps 2022-11-25 18:32:55 -05:00
semantic-release-bot
47fd13692f chore(release): 3.11.1 [CI SKIP] 2022-11-20 01:13:29 +00:00
AAGaming
2ec9519b7d fix(Footer): add types for ActionDescriptionMap 2022-11-19 20:12:45 -05:00
semantic-release-bot
24606190e0 chore(release): 3.11.0 [CI SKIP] 2022-11-18 18:00:30 +00:00
Lukas Senionis
ed98d14b37 feat(classes): add "appDetailsClasses" (#55) 2022-11-18 12:59:57 -05:00
semantic-release-bot
b882612dfa chore(release): 3.10.0 [CI SKIP] 2022-11-18 17:37:50 +00:00
Lukas Senionis
32291620b4 feat(classes): add appDetailsHeaderClasses (#54) 2022-11-18 12:37:16 -05:00
semantic-release-bot
9b368c5f11 chore(release): 3.9.0 [CI SKIP] 2022-11-16 20:45:13 +00:00
Lukas Senionis
e167ef5a13 feat(Dialog): add "focusable" button prop (#51) 2022-11-16 15:44:36 -05:00
semantic-release-bot
2f3df00967 chore(release): 3.8.0 [CI SKIP] 2022-11-11 21:05:54 +00:00
AAGaming
215156d316 feat(routerhook): add global components support 2022-11-11 16:05:05 -05:00
semantic-release-bot
47d75db690 chore(release): 3.7.14 [CI SKIP] 2022-11-05 01:47:25 +00:00
AAGaming
82768e0415 fix(Menu): fix on Steam beta 2022-11-04 21:46:39 -04:00
AAGaming
e44187fe4b fix(Modal): fix on Steam beta 2022-11-04 21:46:22 -04:00
semantic-release-bot
72af32436e chore(release): 3.7.13 [CI SKIP] 2022-11-02 16:56:44 +00:00
Lukas Senionis
e1f64a38de fix(useQuickAccessVisible): make it work in beta (#49)
* fix(useQuickAccessVisible): make it work in beta

* fix(useQuickAccessVisible): rename for consistency

* fix(useQuickAccessVisible): update the exports

* fix(useQuickAccessVisible): shut up typescript
2022-11-02 12:56:11 -04:00
TrainDoctor
82ed48761d Merge pull request #38 from Tormak9970/main
Added some of the SteamClient types
2022-10-30 19:36:12 -07:00
TrainDoctor
a81c342d2a Update src/deck-components/index.ts 2022-10-30 17:35:48 -07:00
TrainDoctor
68d630262d Merge pull request #45 from FrogTheFrog/patch-19
Extend CI for PRs
2022-10-29 15:16:22 -07:00
semantic-release-bot
0bb8c67cfa chore(release): 3.7.12 [CI SKIP] 2022-10-29 19:44:41 +00:00
Jonas Dellinger
edd29e6c5a Merge pull request #46 from FrogTheFrog/patch-20
fix(Item): change title and description types to ReactNode
2022-10-29 21:44:13 +02:00
Lukas Senionis
0ed054fae9 fix(Item): change title and description types to ReactNode 2022-10-29 13:25:30 +03:00
semantic-release-bot
0d912eac88 chore(release): 3.7.11 [CI SKIP] 2022-10-28 23:04:53 +00:00
AAGaming
789e16380f fix(package.json): train wtf 2022-10-28 19:04:25 -04:00
semantic-release-bot
88b50bbc1e chore(release): 3.7.10 [CI SKIP] 2022-10-28 22:56:58 +00:00
AAGaming
75f35882f2 fix(tabs): shut up typescript 2022-10-28 18:56:30 -04:00
semantic-release-bot
23af4c0bb4 chore(release): 3.7.9 [CI SKIP] 2022-10-28 22:43:16 +00:00
AAGaming
a074277bb5 fix(tabs): fix on stable for real this time i think 2022-10-28 18:42:28 -04:00
Lukas Senionis
9c72a55aff Remove prepack hook 2022-10-28 22:00:33 +03:00
Lukas Senionis
92ffc76075 Extend CI for PRs 2022-10-27 11:48:44 +03:00
semantic-release-bot
fb49d64fd3 chore(release): 3.7.8 [CI SKIP] 2022-10-26 21:54:08 +00:00
Lukas Senionis
cfef1dc320 fix(Field): fix this time for real (#44) 2022-10-26 17:53:41 -04:00
semantic-release-bot
f6b4d6b254 chore(release): 3.7.7 [CI SKIP] 2022-10-26 21:49:50 +00:00
Lukas Senionis
0010a1fcee fix(Field): remove incompatible properties (#42)
* fix(Field): remove incompatible properties

* fix(Field): remove the override
2022-10-26 17:49:14 -04:00
Tormak
ce3860f73b added jsdoc and SteamClient global declaration 2022-10-17 08:39:37 -04:00
Tormak
d8b10a2133 ran prettier 2022-10-15 22:46:41 -05:00
Tormak
1581304dcb restyled 2022-10-15 22:44:58 -05:00
Travis Lane
60ddf474e0 Merge branch 'SteamDeckHomebrew:main' into main 2022-10-15 13:46:50 -05:00
Tormak
52ae328e2e added some of the SteamClient types 2022-10-15 14:35:57 -05:00
Tormak
aa0fad2ecf fixed typo 2022-09-29 13:24:15 -05:00
Tormak
dab9071d1e found name of static classes 2022-09-29 13:01:06 -05:00
Tormak
189a90ba31 Updated ScrollPanelClasses
marked the old variable as depreciated and moved the implementation to the new variable
2022-09-25 09:06:14 -05:00
Tormak
8509ae8f9a Merge branch 'main' of https://github.com/Tormak9970/decky-frontend-lib 2022-09-24 12:05:06 -05:00
Tormak
a6ebfdcd7d fixed includes 2022-09-24 12:05:04 -05:00
Tormak
5f7655baaf fixed bugs with static-classes and future proofing 2022-09-24 11:51:19 -05:00
Travis Lane
bca2dcc9bd Merge branch 'SteamDeckHomebrew:main' into main 2022-09-24 10:54:09 -05:00
Tormak
546a4da043 updated DialogButton props and added nav pref enum 2022-09-22 17:33:08 -05:00
75 changed files with 8928 additions and 5226 deletions

View File

@@ -1,9 +1,10 @@
name: Generate docs
on:
push:
branches:
- main
workflow_dispatch:
# push:
# branches:
# - main
jobs:
release:
@@ -11,19 +12,19 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
path: lib
- name: Setup | Checkout wiki
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: SteamDeckHomebrew/wiki
path: wiki
ssh-key: ${{ secrets.SSH_DEPLOY_KEY }}
persist-credentials: true
- name: Setup | Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
- name: Setup | Dependencies
@@ -51,4 +52,4 @@ jobs:
ssh: true
directory: ./wiki
repository: SteamDeckHomebrew/wiki
branch: main
branch: main

View File

@@ -1,6 +1,7 @@
name: Release
on:
pull_request:
push:
branches:
- main
@@ -11,19 +12,22 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup | Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 16
node-version: 20
- name: Setup | Dependencies
run: npm i -g pnpm && pnpm i --frozen-lockfile
- name: Build
run: pnpm run build
- name: Test
run: pnpm run test
- name: Release
if: github.event_name != 'pull_request'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN_DECKY_ORG }}
run: pnpm exec semantic-release

View File

@@ -1,13 +1,14 @@
{
"branches": ["main", "dev"],
"branches": ["main", "v4-dev"],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "angular",
"releaseRules": [
{"type": "chore", "scope": "classes", "release": "patch"},
{"type": "*", "scope": "docs", "release": false}
{ "type": "chore", "scope": "classes", "release": "patch" },
{ "type": "docs", "scope": "steamclient", "release": "patch" },
{ "type": "*", "scope": "docs", "release": false }
]
}
],

26
.vscode/tasks.json vendored
View File

@@ -1,15 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "dev",
"problemMatcher": [
"$tsc-watch"
],
"label": "npm: dev",
"detail": "tsc -b -w",
"isBackground": true
}
]
}
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "dev",
"problemMatcher": ["$tsc-watch"],
"label": "npm: dev",
"detail": "tsc -b -w",
"isBackground": true
}
]
}

View File

@@ -1,3 +1,639 @@
## [4.7.3](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.7.2...v4.7.3) (2024-10-03)
### Bug Fixes
* **components:** fix missing components on oct 2 2024 beta ([0f9fb5a](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/0f9fb5a3b8ef4f9978025a28323ab080fb0e7a4c))
## [4.7.2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.7.1...v4.7.2) (2024-09-16)
### Bug Fixes
* **utils:** fix potential race condition in findSP ([3aa07dc](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/3aa07dc9ce798ff8d1148424ee9e8a8bf2ba78c6))
## [4.7.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.7.0...v4.7.1) (2024-08-08)
### Bug Fixes
* **utils/react:** fix potential race condition in injectFCTrampoline ([4c97097](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/4c97097757919580a380b70785e6c161de6b03cc))
# [4.7.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.6.0...v4.7.0) (2024-07-28)
### Features
* **router:** support desktop bpm overlay ([7eb484d](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/7eb484d55c6be6e7844878eb47eda55591a6cf51))
# [4.6.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.5.0...v4.6.0) (2024-07-26)
### Features
* **classMapper:** add findClassByName back ([2b8d2ae](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/2b8d2ae4dbd9a0c4a59a43be0101a0a8fe1c518f))
# [4.5.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.4.0...v4.5.0) (2024-07-24)
### Features
* **classMapper:** add classModuleMap, make findClass require ID ([a370c1f](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/a370c1f7d3dca0db56a346c98c28ed9681a415e0))
* **webpack:** refactor to prepare for classMapper changes ([d83bada](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/d83bada4af2d16c750955de9a52f94a0080a2c14))
# [4.4.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.3.1...v4.4.0) (2024-07-18)
### Features
* **utils/react:** add injectFCTrampoline ([44fdf9e](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/44fdf9ed3b9a676a88b0ddc6a1c2c89d46ff7651))
## [4.3.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.3.0...v4.3.1) (2024-07-17)
### Bug Fixes
* **Menu/MenuGroup:** rewrite filter to work on beta and prevent future errors ([d64c42a](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/d64c42ac310d3c3266c4ff610d9ec5ab6c7707b6))
# [4.3.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.2.2...v4.3.0) (2024-07-09)
### Features
* **utils:** react tree patching api ([2c3a9f8](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/2c3a9f81de0b63364bb31f4a4dd8e559784ece16))
## [4.2.2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.2.1...v4.2.2) (2024-07-04)
### Bug Fixes
* **SteamSpinner:** add background option ([c04f024](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/c04f024b34b1148c965850965127f9fd44204157))
## [4.2.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.2.0...v4.2.1) (2024-06-27)
### Bug Fixes
* **errorboundary:** work around broken react types ([3ef9648](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/3ef96483550020cecf656b94a73d2bb9313bda07))
# [4.2.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.1.1...v4.2.0) (2024-06-27)
### Features
* **components:** add ErrorBoundary ([dcdbb2d](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/dcdbb2d6c7c0b72197f04153d7c3e73e9e71ea5c))
## [4.1.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.1.0...v4.1.1) (2024-06-27)
### Bug Fixes
* **ReorderableList:** avoid mutating props ([#109](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/109)) ([bd1dc85](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/bd1dc85b9202c8ec6ca994177417574fdd71cbd7))
# [4.1.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.0.1...v4.1.0) (2024-06-27)
### Features
* **release:** release v4.1.0 ([b040444](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/b04044451a9dc3633fe624e47dd58c7ea206d0a3))
## [4.0.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.0.0...v4.0.1) (2024-06-27)
### Bug Fixes
* **release:** empty commit to bump to v4.0.1 ([2bfe624](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/2bfe62409f775a69124e0f2e853ae0b1668a9c36))
# [4.0.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.25.0...v4.0.0) (2024-06-27)
### Bug Fixes
* **ci:** empty commit so semantic-release shuts up ([8cb7c27](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/8cb7c273eb61c1f949844291b2864c11bf746058))
* **ci:** temp remove npm publisher ([abfd2c0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/abfd2c010508ddf6e18149374dc52b50402ffb4c))
* **components:** fix missing children prop ([688c747](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/688c7471cde96bf9e9a71c47d19bd63cff7a66b3))
* **components:** forgot one ([e6e8c91](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/e6e8c91ec7ea711f6c147f28e300f745dcbd24f4))
* **Field:** add children prop ([2a78cc0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/2a78cc011671ab123fb6356f54102e531a7953a8))
* fixes for jun 26 beta ([7e0cb15](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/7e0cb153b197267f49b5e1f513b54a880bf3710f))
* **Menu:** work around ConfigContext error ([9f26209](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/9f262097b9808a0366bb52cd24844371bbe65316))
* **package:** fix ts memes ([ea19d62](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/ea19d62dabfbecad97c1ab9384c676f8a018f96b))
* **package:** pin react properly ([62b454f](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/62b454f71255d5e1898677909a63612615e09083))
* **package:** unbreak react ([b1e5038](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/b1e503853ff6d39f1e86a6180f355e73b9fd6925))
* **plugin:** shim definePlugin for now [ci skip] ([0cd498b](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/0cd498beacbd23efa3b771880ff0a4df3f636836))
* **utils/react:** shut it ts ([d91f49e](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/d91f49e728b613e24b40d7e81d47fd1f3bd92372))
### Features
* add v4 webpack api and port everything to it ([bffd530](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/bffd530bda9a49aae603c323c0a2b4328eee968d))
### BREAKING CHANGES
* **ci:** v4 release
* **ci:** v4 release
## [3.26.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.26.0...v3.26.1) (2024-05-25)
### Bug Fixes
* **Field:** add children prop ([2a78cc0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/2a78cc011671ab123fb6356f54102e531a7953a8))
* **plugin:** shim definePlugin for now [ci skip] ([0cd498b](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/0cd498beacbd23efa3b771880ff0a4df3f636836))
# [3.26.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.25.0...v3.26.0) (2024-05-24)
### Bug Fixes
* **utils/react:** shut it ts ([d91f49e](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/d91f49e728b613e24b40d7e81d47fd1f3bd92372))
### Features
* add v4 webpack api and port everything to it ([bffd530](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/bffd530bda9a49aae603c323c0a2b4328eee968d))
# [3.25.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.24.5...v3.25.0) (2024-03-09)
### Bug Fixes
* **staticclasses:** unbreak on latest beta ([17b99df](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/17b99dfed8e4e146d0f4f5e78a950db5b10ae2b4))
### Features
* **classMapper:** add class mapper ([a8eeb91](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/a8eeb917e22ef72905d448e159d70375ebf77ba6))
## [3.24.5](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.24.4...v3.24.5) (2024-02-03)
### Bug Fixes
* **finds:** make modal and scroll components work on latest betaa ([c2b0fad](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/c2b0fad298512aa8778c677275bd497bd8f7b00e))
## [3.24.4](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.24.3...v3.24.4) (2024-01-22)
### Bug Fixes
* **types:** fix incorrect `as` on many components leading to any types ([bb12921](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/bb129218634b77ddb1d73b0fe38a91898073707c))
## [3.24.3](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.24.2...v3.24.3) (2024-01-20)
### Bug Fixes
* **router:** wait 2s if internal navigators init fails ([95d977d](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/95d977df452d3b73b007c98854deab1842fa6fbf))
## [3.24.2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.24.1...v3.24.2) (2024-01-20)
### Bug Fixes
* **navigation:** Fix on chromium 109 ([#100](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/100)) ([ebf496b](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/ebf496bf61cffa1a5205b4a094fd2279011bffa9))
## [3.24.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.24.0...v3.24.1) (2023-12-13)
### Bug Fixes
* **utils/react:** support react 18, add getReactRoot ([11dd82b](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/11dd82bbb1814ac4d2fa9d381372e325daba2558))
# [3.24.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.23.1...v3.24.0) (2023-12-03)
### Features
* **static-classes:** add BasicAppDetailsSectionStylerClasses ([#99](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/99)) ([e27b638](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/e27b638d26e41332b1554dbd55ca0c55a1821138))
## [3.23.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.23.0...v3.23.1) (2023-11-09)
### Bug Fixes
* **webpack:** don't break if a module fails to load ([5203ce3](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/5203ce348afd727da0c8c52f6d8f8a16712f88d2))
# [3.23.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.22.1...v3.23.0) (2023-10-11)
### Features
* **static-classes:** add more css classes ([#94](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/94)) ([30e3194](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/30e319425bd4b0ee481dd7bd3245dacd90806afb))
## [3.22.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.22.0...v3.22.1) (2023-10-10)
### Bug Fixes
* **useQuickAccessVisible:** use the "Page Visibility API" instead of focus/blur ([4c4fda4](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/4c4fda47e3d9fd936b493c5965634a0ff443014f))
# [3.22.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.21.8...v3.22.0) (2023-08-09)
### Bug Fixes
* add patch indicator to prevent crashes ([3170779](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/3170779c6b3d02ea17f7b6c1fbd57e00498ffe4f))
### Features
* add components found while working on tabmaster ([4f8f65d](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/4f8f65d42979149cc80b4a86545d3d0d9bf14bf3))
* add components found while working on tabmaster ([fd0d011](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/fd0d011cbf05790c5a1078970b5be72f9267402c))
## [3.21.8](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.21.7...v3.21.8) (2023-06-27)
### Bug Fixes
* the typescript compiler was eating the enum ([c9b5839](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/c9b583945c1cb5267b41a821743590a841572abe))
## [3.21.7](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.21.6...v3.21.7) (2023-06-26)
### Bug Fixes
* add patch indicator to prevent crashes ([#88](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/88)) ([9128c1e](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/9128c1e7dadb98a8926d3dba9907a01cc78d90cf))
## [3.21.6](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.21.5...v3.21.6) (2023-06-22)
### Bug Fixes
* reposition parameter for file picker V2 ([ee51dc5](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/ee51dc5fc0dd5bdc2b0b9e10aa27607fbe51f491))
## [3.21.5](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.21.4...v3.21.5) (2023-06-22)
### Bug Fixes
* missing parameter ([8b54ee9](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/8b54ee990ee4d9b51174737979c35ab7ad92ed7a))
## [3.21.4](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.21.3...v3.21.4) (2023-06-22)
### Bug Fixes
* move the new file picker api as v2 ([8bfeae4](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/8bfeae4b3593b2efa0aa075a0d9e0b5926cdf169))
## [3.21.3](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.21.2...v3.21.3) (2023-06-22)
### Bug Fixes
* total is not needed as a return value to plugins ([979a630](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/979a630f2b02ac4a1ac19e38002244f9dfe97177))
## [3.21.2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.21.1...v3.21.2) (2023-06-18)
### Bug Fixes
* **SidebarNavigation:** allow ReactNode for page title ([44d9b90](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/44d9b90cdcfb9e6441fdb1e4b21fe844f1f29fd5))
## [3.21.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.21.0...v3.21.1) (2023-05-28)
### Bug Fixes
* **reorderable-list:** open label type from string to ReactNode ([fb5f043](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/fb5f043ba9eeed0209960c8ae4ae597c2831b8c8))
# [3.21.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.20.7...v3.21.0) (2023-05-19)
### Bug Fixes
* **Utils:** return null when SP not found ([18d341f](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/18d341f82acf843ff830d1bf3d44678f3f2eda52))
### Features
* **StaticClasses:** add new GamepadUI classes ([3ca8c43](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/3ca8c43a542b1049c4e203ad186e555401fbfbfe))
## [3.20.7](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.20.6...v3.20.7) (2023-05-10)
### Bug Fixes
* **decky:** fix decky on latest beta ([74a7cba](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/74a7cbaf94538c68a01f5fa707935c4d21570c5f))
## [3.20.6](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.20.5...v3.20.6) (2023-04-29)
### Bug Fixes
* **QuickAccessTab:** set decky tab ID to 999 ([5d5cb31](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/5d5cb31638070deae9970a93c587b447d5e56559))
## [3.20.5](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.20.4...v3.20.5) (2023-04-04)
### Bug Fixes
* reorderable list no longer toggles on backout ([7e1182a](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/7e1182a83f7fafbec6fe115a72f8b64b71c119a4))
## [3.20.4](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.20.3...v3.20.4) (2023-04-04)
### Bug Fixes
* **SteamSpinner:** oh apparently the class was moved outside the component for some reason?????? ([b8ddf3d](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/b8ddf3d927401d04c0fde8ebc8960639369b8ad3))
## [3.20.3](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.20.2...v3.20.3) (2023-04-04)
### Bug Fixes
* **SteamSpinner:** fix the fix ([79d229b](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/79d229be50d26e2510af0ea16cdf6644371a5967))
## [3.20.2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.20.1...v3.20.2) (2023-04-04)
### Bug Fixes
* **SteamSpinner:** dont error on latest desktop beta ([19819b7](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/19819b7a5bc9434fa802f4e8dca4f4cb6921df07))
## [3.20.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.20.0...v3.20.1) (2023-04-03)
### Bug Fixes
* **useQuickAccessVisible:** make it work again ([7dacb23](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/7dacb23e8be7b1f076cdd0869a4e3a3902b07ec5))
# [3.20.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.19.2...v3.20.0) (2023-04-03)
### Bug Fixes
* **Field:** remove style ([53faf55](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/53faf55df1484204e276cd21a32703c2d7809332))
* fixed missing export ([b480d39](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/b480d397c4251f42c1f24ff5e74322d22e313f05))
* list didn't update on prop change ([b146eab](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/b146eab8d7e1338afa54a168a2fd895e716c2bb2))
### Features
* added doc comments ([dfcb3be](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/dfcb3bec19900e099ae3766771e120dbd4f229f5))
* added reorderable list and updated fieldProps ([3c171cf](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/3c171cfb8ff18ed02eeb569a183c9d43fd0b4f57))
* changed ReorderableList to working version ([5b166d6](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/5b166d6db879200b049e872cba327957ba5fb705))
* made requested changes and ran prettier ([b1b2f4f](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/b1b2f4fa2da755efd65b82b15b52196f89fb09c0))
* refactoring mostly complete ([cea315a](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/cea315a52c285b31ad4e5d0a03104c674ae4f7f5))
* reorderable list now saves on backout ([b1591f8](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/b1591f86bbd36e160818626760e0717ee50878e0))
* **ReorderableList:** add animations, clean up ([26fae13](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/26fae13c8ebd3f11f134c3bc0edfc971afd42fff))
* support for non-interactable reordering ([dbd01b1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/dbd01b11cafe9b102cc371b9812f99aec718d106))
* support for user specified icon ([47a6fdd](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/47a6fddc89b8f9110252c5e19a6e95152c367dbf))
## [3.19.2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.19.1...v3.19.2) (2023-03-07)
### Bug Fixes
* **Item:** add highlightOnFocus prop ([c53f87b](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/c53f87b4a9273b377853bfff1d27474ebd6e564a))
## [3.19.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.19.0...v3.19.1) (2023-02-23)
### Bug Fixes
* refactoring to fix for feb 22 2023 beta ([0b6dc24](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/0b6dc24c0da2d7644e185425e975787657f8bba1))
# [3.19.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.11...v3.19.0) (2023-02-22)
### Features
* added reorderable list and updated fieldProps ([#57](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/57)) ([5a074b5](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/5a074b5bb68c675c484a7b693f67a67488be9bcf))
## [3.18.11](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.10...v3.18.11) (2023-02-18)
### Bug Fixes
* **Navigation:** fix NavigateToExternalWeb ([82214fe](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/82214fef4c0a383776631fbb754550fe69f9000d))
* **Navigation:** fix NavigateToExternalWeb ([c53d7f8](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/c53d7f8448d8aad76dc699f1f309bdd547ee14df))
## [3.18.10](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.9...v3.18.10) (2023-01-17)
### Bug Fixes
* **SuspensefulImage:** fix changing src ([9723854](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/9723854ddca53d7708e1effbddec9e5ead22d5de))
## [3.18.9](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.8...v3.18.9) (2023-01-16)
### Bug Fixes
* **Navigation:** fix on stable ([4affd4a](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/4affd4aaec088f01d0f30af48cb4daa34acf26b1))
## [3.18.8](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.7...v3.18.8) (2023-01-16)
### Bug Fixes
* **Navigation:** fix timing issue in decky-loader ([58b69f0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/58b69f0d6c43356c4f0ed183802d5bf7fb80e978))
## [3.18.7](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.6...v3.18.7) (2023-01-16)
### Bug Fixes
* un-break navigation on stable ([2e66e5a](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/2e66e5a555f44009d24e332eca82453ba930baf7))
## [3.18.6](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.5...v3.18.6) (2023-01-13)
### Bug Fixes
* **Router:** fix Navigation for the millionth time ([aac2d52](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/aac2d520a68b1074ba1ae988d6c92f7881a296d7))
## [3.18.5](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.4...v3.18.5) (2022-12-21)
### Bug Fixes
* fixed prop interfaces ([#70](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/70)) ([0b50f2c](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/0b50f2cf0baa76fc00aa0a41a8435d7a512bff19))
## [3.18.4](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.3...v3.18.4) (2022-12-16)
### Bug Fixes
* **modals:** fix ModalRoot again ([fd94842](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/fd94842647e51dd9a104e170e0c5ee2bebce12d6))
## [3.18.3](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.2...v3.18.3) (2022-12-12)
### Bug Fixes
* **Router:** update Router interface to SteamOS3.4 and add Navigation ([#52](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/52)) ([f0379e5](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/f0379e5d19279863b571e66918bc9107efedb612))
## [3.18.2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.1...v3.18.2) (2022-12-11)
### Bug Fixes
* **useQuickAccessVisible:** remove invalid prop access ([#66](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/66)) ([767dc2f](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/767dc2fcee97d8b6c2d331ae29704d9b469de51a))
## [3.18.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.0...v3.18.1) (2022-12-11)
### Bug Fixes
* **findSP:** fallback to last active context ([#53](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/53)) ([6f14da1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/6f14da152acc4757b814844f1b77bf83dd98d77e))
# [3.18.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.17.0...v3.18.0) (2022-12-11)
### Features
* **DialogCheckbox:** Add DialogCheckbox component ([#58](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/58)) ([88f245d](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/88f245d476a6477e9fc0cd35e9b675961ecbc26c))
# [3.17.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.16.2...v3.17.0) (2022-12-11)
### Features
* **ControlsList:** Add ControlsList component ([#61](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/61)) ([c586afb](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/c586afb97d59928ecb703b5a254ed1b9405e2c7e))
## [3.16.2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.16.1...v3.16.2) (2022-12-11)
### Bug Fixes
* **Marquee:** replace default export with named export ([cd0635e](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/cd0635e94f98499f9f5fc24a7fd4b93efe7dfc38))
## [3.16.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.16.0...v3.16.1) (2022-12-11)
### Bug Fixes
* **FooterLegend:** change description types to ReactNode ([#62](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/62)) ([d24136e](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/d24136ecb6b0c5239b68723e8f92a4822aa7b590))
# [3.16.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.15.0...v3.16.0) (2022-12-11)
### Features
* **Marquee:** Add Marquee component ([#63](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/63)) ([925ea8c](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/925ea8c3ceaaf6ff2f79b8808908a9b144a4fcff))
# [3.15.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.14.0...v3.15.0) (2022-12-11)
### Features
* **Focusable:** add noFocusRing prop type ([#65](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/65)) ([cc29dda](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/cc29ddaf578e21ab2abe1cd266f1d15debee0637))
# [3.14.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.13.0...v3.14.0) (2022-12-10)
### Features
* **toast:** add showToast/playSound to ToastData ([#64](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/64)) ([7ba1229](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/7ba1229a4e24fea587b96dc8b078200faf45ddee))
# [3.13.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.12.0...v3.13.0) (2022-11-29)
### Features
* **Menu:** add more missing props ([#60](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/60)) [CI SKIP] ([678ba21](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/678ba216f1e194986b0c391398e6f73536cd0102))
# [3.12.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.11.1...v3.12.0) (2022-11-28)
### Features
* **MenuItem:** add missing props ([#59](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/59)) ([c84a091](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/c84a09146935f0942265b7a1e4aadc40e8cf22dc))
## [3.11.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.11.0...v3.11.1) (2022-11-20)
### Bug Fixes
* **Footer:** add types for ActionDescriptionMap ([2ec9519](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/2ec9519b7d6d1cc0d232853ce05a773953b37c5a))
# [3.11.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.10.0...v3.11.0) (2022-11-18)
### Features
* **classes:** add "appDetailsClasses" ([#55](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/55)) ([ed98d14](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/ed98d14b37cf09500afd88e7c8e9c03749119b38))
# [3.10.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.9.0...v3.10.0) (2022-11-18)
### Features
* **classes:** add appDetailsHeaderClasses ([#54](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/54)) ([3229162](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/32291620b403f8b65cf378343454a3f2668fb6ee))
# [3.9.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.8.0...v3.9.0) (2022-11-16)
### Features
* **Dialog:** add "focusable" button prop ([#51](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/51)) ([e167ef5](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/e167ef5a138a3edc004db2365334f8455c177132))
# [3.8.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.14...v3.8.0) (2022-11-11)
### Features
* **routerhook:** add global components support ([215156d](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/215156d31688faac9028627379e5a3ac4d64ec46))
## [3.7.14](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.13...v3.7.14) (2022-11-05)
### Bug Fixes
* **Menu:** fix on Steam beta ([82768e0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/82768e0415d084deb2af39beb3f9273a83e819de))
* **Modal:** fix on Steam beta ([e44187f](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/e44187fe4b9d3e3c9e94490669591599dc5246ba))
## [3.7.13](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.12...v3.7.13) (2022-11-02)
### Bug Fixes
* **useQuickAccessVisible:** make it work in beta ([#49](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/49)) ([e1f64a3](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/e1f64a38de85073e5cea74ecea4b9cde9a783ecc))
## [3.7.12](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.11...v3.7.12) (2022-10-29)
### Bug Fixes
* **Item:** change title and description types to ReactNode ([0ed054f](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/0ed054fae972ffd36299b142bd693f80388480a6))
## [3.7.11](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.10...v3.7.11) (2022-10-28)
### Bug Fixes
* **package.json:** train wtf ([789e163](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/789e16380fd01a6b46188c7a1174a55c18c8dead))
## [3.7.10](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.9...v3.7.10) (2022-10-28)
### Bug Fixes
* **tabs:** shut up typescript ([75f3588](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/75f35882f27252e1255208953a6e801c68d5dcec))
## [3.7.9](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.8...v3.7.9) (2022-10-28)
### Bug Fixes
* **tabs:** fix on stable for real this time i think ([a074277](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/a074277bb58428a64295154ebf96aceb96e654a7))
## [3.7.8](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.7...v3.7.8) (2022-10-26)
### Bug Fixes
* **Field:** fix this time for real ([#44](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/44)) ([cfef1dc](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/cfef1dc320a5f649d66c3af365cd6aa2d88e46ea))
## [3.7.7](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.6...v3.7.7) (2022-10-26)
### Bug Fixes
* **Field:** remove incompatible properties ([#42](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/42)) ([0010a1f](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/0010a1fceedc417aa25b709d066341da97d42444))
## [3.7.6](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.5...v3.7.6) (2022-10-26)

View File

@@ -2,9 +2,9 @@
Library used to develop plugins used for use with [decky-loader](https://github.com/SteamDeckHomebrew/decky-loader).
## Decky Loader Discord [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/ZU74G2NJzk)
## Decky Loader Discord [![Chat](https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord)](https://deckbrew.xyz/discord)
Please contact the developers here for questions and support that cannot be addressed via a Github issue.
Please [contact the developers here](https://deckbrew.xyz/discord) for questions and support that cannot be addressed via a Github issue.
## Developers and Contributors
@@ -20,8 +20,11 @@ This library can also theoretically be used to extend existing UI elements of th
### Getting Started (Developers)
If you would like a feature added to decky-frontend-lib, please request it via a Github issue.
If you would like a feature added to decky-frontend-lib, please request it via a Github issue.
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.
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.
Tips for fixing failing module finds after Steam updates:
- `Object.entries(DFL)` can point out any undefined exports

View File

@@ -1,3 +1,3 @@
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript',],
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
};

5
global.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare global {
interface Window {
SP_REACT: typeof React;
}
}

5
globals.d.ts vendored
View File

@@ -1,5 +0,0 @@
declare global {
interface Window {
SP_REACT: typeof React;
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "decky-frontend-lib",
"version": "3.7.6",
"description": "A library for building decky plugins",
"name": "@decky/ui",
"version": "4.7.3",
"description": "A library for interacting with the Steam frontend in Decky plugins and elsewhere.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"module": "dist/index.js",
@@ -9,8 +9,7 @@
"scripts": {
"build": "shx rm -rf dist && tsc -b",
"dev": "tsc -b -w",
"docs": "typedoc --tsconfig ./tsconfig.json src/**/*",
"prepack": "npm run build",
"docs": "typedoc --theme wiki-js --tsconfig ./tsconfig.json src/**/*",
"test": "echo 'No tests for now!'",
"prepare": "husky install",
"commit": "git-cz"
@@ -30,7 +29,7 @@
"steam",
"components"
],
"author": "Jonas Dellinger <jonas@dellinger.dev>",
"author": "SteamDeckHomebrew Team",
"license": "LGPL-2.1",
"bugs": {
"url": "https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues"
@@ -42,28 +41,30 @@
}
},
"devDependencies": {
"@commitlint/cli": "^17.0.2",
"@commitlint/config-conventional": "^17.0.2",
"@commitlint/cz-commitlint": "^17.0.0",
"@semantic-release/changelog": "^6.0.1",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@commitlint/cz-commitlint": "^19.2.0",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@types/jest": "^27.4.1",
"@types/react": "16.14.0",
"@types/react-router": "5.1.18",
"commitizen": "^4.2.4",
"husky": "^8.0.1",
"@types/jest": "^29.5.12",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/react-router": "5.1.20",
"commitizen": "^4.3.0",
"husky": "^9.0.11",
"import-sort-style-module": "^6.0.0",
"jest": "^27.5.1",
"prettier": "^2.7.1",
"jest": "^29.7.0",
"minimist": "^1.2.8",
"prettier": "^3.3.2",
"prettier-plugin-import-sort": "^0.0.7",
"semantic-release": "^19.0.3",
"semantic-release": "^24.0.0",
"shx": "^0.3.4",
"ts-jest": "^27.1.4",
"typedoc": "^0.23.15",
"typedoc-plugin-markdown": "^3.13.6",
"typedoc-plugin-mdn-links": "^2.0.0",
"typedoc-plugin-missing-exports": "^1.0.0",
"typescript": "^4.6.3"
"ts-jest": "^29.1.4",
"typedoc": "^0.25.13",
"typedoc-plugin-mdn-links": "^3.1.29",
"typedoc-plugin-missing-exports": "^2.3.0",
"typedoc-wikijs-theme": "^1.0.5",
"typescript": "^5.4.5"
},
"pnpm": {
"peerDependencyRules": {
@@ -71,6 +72,12 @@
"react",
"react-dom"
]
},
"updateConfig": {
"ignoreDependencies": [
"react",
"react-dom"
]
}
},
"importSort": {
@@ -79,7 +86,8 @@
"parser": "typescript"
}
},
"dependencies": {
"minimist": "^1.2.6"
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"tag": "latest"
}
}

9232
pnpm-lock.yaml generated Executable file → Normal file

File diff suppressed because it is too large Load Diff

40
src/class-mapper.ts Normal file
View File

@@ -0,0 +1,40 @@
import { Module, ModuleID, createModuleMapping } from './webpack';
export interface ClassModule {
[name: string]: string;
}
export const classModuleMap: Map<ModuleID, ClassModule> = createModuleMapping((m: Module) => {
if (typeof m == 'object' && !m.__esModule) {
const keys = Object.keys(m);
// special case some libraries
if (keys.length == 1 && m.version) return false;
// special case localization
if (keys.length > 1000 && m.AboutSettings) return false;
return keys.length > 0 && keys.every((k) => !Object.getOwnPropertyDescriptor(m, k)?.get && typeof m[k] == 'string');
}
return false;
});
export const classMap = [...classModuleMap.values()];
export function findClass(id: string, name: string): string | void {
return classModuleMap.get(id)?.[name];
}
export function findClassByName(name: string): string | void {
return classMap.find((m) => m[name])?.[name];
}
export function findClassModule(filter: (module: any) => boolean): ClassModule | void {
return classMap.find((m) => filter(m));
}
export function unminifyClass(minifiedClass: string): string | void {
for (let m of classModuleMap.values()) {
for (let className of Object.keys(m)) {
if (m[className] == minifiedClass) return className;
}
}
}

View File

@@ -2,14 +2,15 @@ import { FC } from 'react';
import { CommonUIModule } from '../webpack';
import { ItemProps } from './Item';
import { createPropListRegex } from '../utils';
export interface ButtonItemProps extends ItemProps {
onClick?(e: MouseEvent): void;
disabled?: boolean;
}
const buttonItemRegex = createPropListRegex(["highlightOnFocus", "childrenContainerWidth"], false);
export const ButtonItem = Object.values(CommonUIModule).find(
(mod: any) =>
mod?.render?.toString()?.includes('"highlightOnFocus","childrenContainerWidth"') ||
mod?.render?.toString()?.includes('childrenContainerWidth:"min"'),
(mod?.render?.toString && buttonItemRegex.test(mod.render.toString())) ||
mod?.render?.toString?.().includes('childrenContainerWidth:"min"'),
) as FC<ButtonItemProps>;

View File

@@ -1,6 +1,6 @@
import { HTMLAttributes, ReactNode, RefAttributes, VFC } from 'react';
import { HTMLAttributes, ReactNode, RefAttributes, FC } from 'react';
import { findModuleChild } from '../webpack';
import { Export, findModuleExport } from '../webpack';
export interface CarouselProps extends HTMLAttributes<HTMLDivElement> {
autoFocus?: boolean;
@@ -20,9 +20,6 @@ export interface CarouselProps extends HTMLAttributes<HTMLDivElement> {
scrollToAlignment?: 'center';
}
export const Carousel = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.render?.toString().includes('setFocusedColumn:')) return m[prop];
}
}) as VFC<CarouselProps & RefAttributes<HTMLDivElement>>;
export const Carousel = findModuleExport((e: Export) => e.render?.toString().includes('setFocusedColumn:')) as FC<
CarouselProps & RefAttributes<HTMLDivElement>
>;

View File

@@ -0,0 +1,14 @@
import { FC, ReactNode } from 'react';
import { Export, findModuleExport } from '../webpack';
export interface ControlsListProps {
alignItems?: 'left' | 'right' | 'center';
spacing?: 'standard' | 'extra';
children?: ReactNode;
}
export const ControlsList: FC<ControlsListProps> = findModuleExport(
(e: Export) =>
e?.toString && e.toString().includes('().ControlsListChild') && e.toString().includes('().ControlsListOuterPanel'),
);

View File

@@ -1,4 +1,4 @@
import { CSSProperties, FC, RefAttributes } from 'react';
import { CSSProperties, FC, ReactNode, RefAttributes } from 'react';
import { CommonUIModule } from '../webpack';
import { FooterLegendProps } from './FooterLegend';
@@ -6,11 +6,39 @@ import { FooterLegendProps } from './FooterLegend';
export interface DialogCommonProps extends RefAttributes<HTMLDivElement> {
style?: CSSProperties;
className?: string;
children?: ReactNode;
}
export interface DialogButtonProps extends DialogCommonProps, FooterLegendProps {
/**
* Enables/disables the focus around the button.
*
* @note
* Default value depends on context, so setting it to `false` will enable it.
*/
noFocusRing?: boolean;
/**
* Disables the button - assigned `on*` methods will not be invoked if clicked.
*
* @note
* Depending on where it is, it might still get focus. In such case it can be
* partially disabled separately.
*
* @see focusable.
*/
disabled?: boolean;
/**
* Enables/disables the navigation based focus on button - you won't be able to navigate to
* it via the gamepad or keyboard.
*
* @note
* If set to `false`, it still can be clicked and **WILL** become focused until navigated away.
* Depending on the context of where the button is, even a disabled button can focused.
*/
focusable?: boolean;
onClick?(e: MouseEvent): void;
onPointerDown?(e: PointerEvent): void;
onPointerUp?(e: PointerEvent): void;
@@ -24,13 +52,19 @@ export interface DialogButtonProps extends DialogCommonProps, FooterLegendProps
}
const CommonDialogDivs = Object.values(CommonUIModule).filter(
(m: any) => typeof m === 'object' && m?.render?.toString().includes('"div",Object.assign({},'),
(m: any) => typeof m === 'object' && m?.render?.toString().includes('createElement("div",{...') ||
m?.render?.toString().includes('createElement("div",Object.assign({},'),
);
const MappedDialogDivs = new Map(
Object.values(CommonDialogDivs).map((m: any) => {
const renderedDiv = m.render({});
// Take only the first class name segment as it identifies the element we want
return [renderedDiv.props.className.split(' ')[0], m];
try {
const renderedDiv = m.render({});
// Take only the first class name segment as it identifies the element we want
return [renderedDiv.props.className.split(' ')[0], m];
} catch (e) {
console.error("[DFL:Dialog]: failed to render common dialog component", e);
return [null, null];
}
}),
);
@@ -44,14 +78,11 @@ export const DialogControlsSection = MappedDialogDivs.get('DialogControlsSection
export const DialogControlsSectionHeader = MappedDialogDivs.get('DialogControlsSectionHeader') as FC<DialogCommonProps>;
export const DialogButtonPrimary = Object.values(CommonUIModule).find(
(mod: any) => mod?.render?.toString()?.includes('DialogButton') && mod?.render?.toString()?.includes('Primary'),
(mod: any) => mod?.render?.toString()?.includes('"DialogButton","_DialogLayout","Primary"'),
) as FC<DialogButtonProps>;
export const DialogButtonSecondary = Object.values(CommonUIModule).find(
(mod: any) =>
mod?.render?.toString()?.includes('Object.assign({type:"button"') &&
mod?.render?.toString()?.includes('DialogButton') &&
mod?.render?.toString()?.includes('Secondary'),
(mod: any) => mod?.render?.toString()?.includes('"DialogButton","_DialogLayout","Secondary"')
) as FC<DialogButtonProps>;
// This is the "main" button. The Primary can act as a submit button,

View File

@@ -0,0 +1,39 @@
import { FC, ReactNode } from 'react';
import { findModule } from '../webpack';
import { DialogCommonProps } from './Dialog';
import { FooterLegendProps } from './FooterLegend';
export interface DialogCheckboxProps extends DialogCommonProps, FooterLegendProps {
onChange?(checked: boolean): void;
label?: ReactNode;
description?: ReactNode;
disabled?: boolean;
tooltip?: string;
color?: string;
highlightColor?: string;
bottomSeparator?: 'standard' | 'thick' | 'none';
controlled?: boolean;
checked?: boolean;
onClick?(evt: Event): void;
}
export const DialogCheckbox = Object.values(
findModule((m: any) => {
if (typeof m !== 'object') return false;
for (const prop in m) {
if (m[prop]?.prototype?.GetPanelElementProps) return true;
}
return false;
}),
).find(
(m: any) =>
m?.prototype?.SetChecked &&
m?.prototype?.Toggle &&
m?.prototype?.GetPanelElementProps &&
// beta || stable as of oct 2 2024
(m?.prototype?.render?.toString().includes('="DialogCheckbox"') || (
m.contextType &&
m.prototype?.render.toString().includes('fallback:')
))
) as FC<DialogCheckboxProps>;

View File

@@ -1,7 +1,8 @@
import { ReactNode, VFC } from 'react';
import { ReactNode, FC } from 'react';
import { CommonUIModule } from '../webpack';
import { ItemProps } from './Item';
import { createPropListRegex } from '../utils';
export interface SingleDropdownOption {
data: any;
@@ -19,6 +20,11 @@ export interface MultiDropdownOption {
export type DropdownOption = SingleDropdownOption | MultiDropdownOption;
export interface DropdownMenuPositionOptions {
[_: string]: unknown
bMatchWidth?: boolean
}
export interface DropdownProps {
rgOptions: DropdownOption[];
selectedOption: any;
@@ -26,7 +32,7 @@ export interface DropdownProps {
onMenuWillOpen?(showMenu: () => void): void;
onMenuOpened?(): void;
onChange?(data: SingleDropdownOption): void;
contextMenuPositionOptions?: any;
contextMenuPositionOptions?: DropdownMenuPositionOptions;
menuLabel?: string;
strDefaultLabel?: string;
renderButtonValue?(element: ReactNode): ReactNode;
@@ -35,10 +41,11 @@ export interface DropdownProps {
export const Dropdown = Object.values(CommonUIModule).find(
(mod: any) => mod?.prototype?.SetSelectedOption && mod?.prototype?.BuildMenu,
) as VFC<DropdownProps>;
) as FC<DropdownProps>;
export interface DropdownItemProps extends DropdownProps, ItemProps {}
const dropdownItemRegex = createPropListRegex(["dropDownControlRef", "description"], false);
export const DropdownItem = Object.values(CommonUIModule).find((mod: any) =>
mod?.toString()?.includes('"dropDownControlRef","description"'),
) as VFC<DropdownItemProps>;
mod?.toString && dropdownItemRegex.test(mod.toString()),
) as FC<DropdownItemProps>;

View File

@@ -0,0 +1,6 @@
import { FC, PropsWithChildren } from "react";
import { findModuleExport } from "../webpack";
export const ErrorBoundary = findModuleExport(
(e) => e.InstallErrorReportingStore && e?.prototype?.Reset && e?.prototype?.componentDidCatch,
) as FC<PropsWithChildren>; // Actually a class but @types/react is broken lol

View File

@@ -1,9 +1,10 @@
import { FC, HTMLAttributes, ReactNode, RefAttributes } from 'react';
import { FC, ReactNode, RefAttributes } from 'react';
import { findModuleChild } from '../webpack';
import { Export, findModuleExport } from '../webpack';
import { FooterLegendProps } from './FooterLegend';
export interface FieldProps extends HTMLAttributes<HTMLDivElement>, FooterLegendProps {
export interface FieldProps extends FooterLegendProps {
children?: ReactNode;
label?: ReactNode;
bottomSeparator?: 'standard' | 'thick' | 'none';
description?: ReactNode;
@@ -20,12 +21,9 @@ export interface FieldProps extends HTMLAttributes<HTMLDivElement>, FooterLegend
verticalAlignment?: 'center' | 'none'; // Alligns inline label with children
focusable?: boolean; // Allows to get focus without any focusable children or on* callbacks
onActivate?: (e: CustomEvent | MouseEvent) => void;
override onClick?: (e: CustomEvent | MouseEvent) => void;
onClick?: (e: CustomEvent | MouseEvent) => void;
}
export const Field = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.render?.toString().includes('"shift-children-below"')) return m[prop];
}
}) as FC<FieldProps & RefAttributes<HTMLDivElement>>;
export const Field = findModuleExport((e: Export) => e?.render?.toString().includes('"shift-children-below"')) as FC<
FieldProps & RefAttributes<HTMLDivElement>
>;

View File

@@ -0,0 +1,15 @@
import { ElementType, FC, ReactNode } from 'react';
import { Export, findModuleExport } from '../webpack';
export interface FocusRingProps {
className?: string;
rootClassName?: string;
render?: ElementType;
children?: ReactNode;
NavigationManager?: any;
}
export const FocusRing = findModuleExport((e: Export) =>
e?.toString()?.includes('.GetShowDebugFocusRing())'),
) as FC<FocusRingProps>;

View File

@@ -0,0 +1,21 @@
import { HTMLAttributes, ReactNode, RefAttributes, FC } from 'react';
import { Export, findModuleExport } from '../webpack';
import { FooterLegendProps } from './FooterLegend';
import { createPropListRegex } from '../utils';
export interface FocusableProps extends HTMLAttributes<HTMLDivElement>, FooterLegendProps {
children: ReactNode;
'flow-children'?: string;
focusClassName?: string;
focusWithinClassName?: string;
noFocusRing?: boolean;
onActivate?: (e: CustomEvent) => void;
onCancel?: (e: CustomEvent) => void;
}
const focusableRegex = createPropListRegex(["flow-children", "onActivate", "onCancel", "focusClassName", "focusWithinClassName"]);
export const Focusable = findModuleExport((e: Export) =>
e?.render?.toString && focusableRegex.test(e.render.toString())
) as FC<FocusableProps & RefAttributes<HTMLDivElement>>;

View File

@@ -1,3 +1,5 @@
import { ReactNode } from 'react';
export enum GamepadButton {
INVALID,
OK,
@@ -29,30 +31,29 @@ export enum GamepadButton {
STEAM_GUIDE,
STEAM_QUICK_MENU,
}
export enum NavEntryPositionPreferences {
export declare enum NavEntryPositionPreferences {
FIRST,
LAST,
MAINTAIN_X,
MAINTAIN_Y,
PREFERRED_CHILD,
}
export interface GamepadEventDetail {
button: number;
is_repeat?: boolean;
source: number;
}
export type GamepadEvent = CustomEvent<GamepadEventDetail>;
export declare type ActionDescriptionMap = {
[key in GamepadButton]?: ReactNode;
};
export declare type GamepadEvent = CustomEvent<GamepadEventDetail>;
export interface FooterLegendProps {
actionDescriptionMap?: unknown;
onOKActionDescription?: string;
onCancelActionDescription?: string;
onSecondaryActionDescription?: string;
onOptionsActionDescription?: string;
onMenuActionDescription?: string;
actionDescriptionMap?: ActionDescriptionMap;
onOKActionDescription?: ReactNode;
onCancelActionDescription?: ReactNode;
onSecondaryActionDescription?: ReactNode;
onOptionsActionDescription?: ReactNode;
onMenuActionDescription?: ReactNode;
onButtonDown?: (evt: GamepadEvent) => void;
onButtonUp?: (evt: GamepadEvent) => void;
onOKButton?: (evt: GamepadEvent) => void;

View File

@@ -1,11 +1,13 @@
import { ReactNode } from 'react';
export interface ItemProps {
label?: string;
description?: string;
label?: ReactNode;
description?: ReactNode;
children?: ReactNode;
layout?: 'below' | 'inline';
icon?: ReactNode;
bottomSeparator?: 'standard' | 'thick' | 'none';
indentLevel?: number;
tooltip?: string;
highlightOnFocus?: boolean;
}

20
src/components/Marquee.ts Normal file
View File

@@ -0,0 +1,20 @@
import { CSSProperties, FC } from 'react';
import { Export, findModuleExport } from '../webpack';
export interface MarqueeProps {
play?: boolean;
direction?: 'left' | 'right';
speed?: number;
delay?: number;
fadeLength?: number;
center?: boolean;
resetOnPause?: boolean;
style?: CSSProperties;
className?: string;
children: React.ReactNode;
}
export const Marquee: FC<MarqueeProps> = findModuleExport(
(e: Export) => e?.toString && e.toString().includes('.Marquee') && e.toString().includes('--fade-length'),
);

53
src/components/Menu.ts Executable file
View File

@@ -0,0 +1,53 @@
import { FC, ReactNode } from 'react';
import { Export, findModuleByExport, findModuleExport } from '../webpack';
import { FooterLegendProps } from './FooterLegend';
export const showContextMenu: (children: ReactNode, parent?: EventTarget) => void = findModuleExport(
(e: Export) => typeof e === 'function' && e.toString().includes('GetContextMenuManagerFromWindow(')
&& e.toString().includes('.CreateContextMenuInstance('),
);
export interface MenuProps extends FooterLegendProps {
label: string;
onCancel?(): void;
cancelText?: string;
children?: ReactNode;
}
export const Menu: FC<MenuProps> = findModuleExport(
(e: Export) => e?.prototype?.HideIfSubmenu && e?.prototype?.HideMenu,
);
export interface MenuGroupProps {
label: string;
disabled?: boolean;
children?: ReactNode;
}
const MenuGoupModule = findModuleByExport(e => e?.prototype?.Focus && e?.prototype?.OnOKButton && e?.prototype?.render?.toString().includes?.(`"emphasis"==this.props.tone`));
export const MenuGroup: FC<MenuGroupProps> = MenuGoupModule && Object.values(MenuGoupModule).find((e: Export) => typeof e == "function" && e?.toString()?.includes("bInGamepadUI:"));
export interface MenuItemProps extends FooterLegendProps {
bInteractableItem?: boolean;
onClick?(evt: Event): void;
onSelected?(evt: Event): void;
onMouseEnter?(evt: MouseEvent): void;
onMoveRight?(): void;
selected?: boolean;
disabled?: boolean;
bPlayAudio?: boolean;
tone?: 'positive' | 'emphasis' | 'destructive';
children?: ReactNode;
}
export const MenuItem: FC<MenuItemProps> = findModuleExport(
(e: Export) =>
e?.render?.toString()?.includes('bPlayAudio:') || (e?.prototype?.OnOKButton && e?.prototype?.OnMouseEnter),
);
/*
all().map(m => {
if (typeof m !== "object") return undefined;
for (let prop in m) { if (m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) return m[prop]}
}).find(x => x)
*/

View File

@@ -1,11 +1,11 @@
import { FC, ReactNode } from 'react';
import { findSP } from '../utils';
import { findModuleChild } from '../webpack';
import { Export, findModule, findModuleDetailsByExport, findModuleExport } from '../webpack';
// All of the popout options + strTitle are related. Proper usage is not yet known...
export interface ShowModalProps {
browserContext?: unknown; // This is another Deck Object that is yet to be found
browserContext?: unknown;
bForcePopOut?: boolean;
bHideActionIcons?: boolean;
bHideMainWindowForPopouts?: boolean;
@@ -29,18 +29,30 @@ export interface ShowModalResult {
Update: (modal: ReactNode) => void;
}
const showModalRaw: (modal: ReactNode, parent?: EventTarget, props?: ShowModalProps) => Promise<ShowModalResult> =
findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (typeof m[prop] === 'function' && m[prop].toString().includes('bHideMainWindowForPopouts:!0')) {
return m[prop];
}
}
});
const showModalRaw: (
modal: ReactNode,
parent?: EventTarget,
title?: string,
props?: ShowModalProps,
unknown1?: unknown,
hideActions?: { bHideActions?: boolean },
modalManager?: unknown,
) => ShowModalResult = findModuleExport(
(e: Export) =>
typeof e === 'function' && e.toString().includes('props.bDisableBackgroundDismiss') && !e?.prototype?.Cancel,
);
export const showModal = (modal: ReactNode, parent?: EventTarget, props?: ShowModalProps): Promise<ShowModalResult> => {
return showModalRaw(modal, parent || findSP(), props);
export const showModal = (
modal: ReactNode,
parent?: EventTarget,
props: ShowModalProps = {
strTitle: 'Decky Dialog',
bHideMainWindowForPopouts: false,
},
): ShowModalResult => {
return showModalRaw(modal, parent || findSP(), props.strTitle, props, undefined, {
bHideActions: props.bHideActionIcons,
});
};
export interface ModalRootProps {
@@ -70,20 +82,36 @@ export interface ConfirmModalProps extends ModalRootProps {
bMiddleDisabled?: boolean;
}
export const ConfirmModal = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (!m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) {
return m[prop];
}
}
}) as FC<ConfirmModalProps>;
export const ConfirmModal = findModuleExport(
(e: Export) => !e?.prototype?.OK && e?.prototype?.Cancel && e?.prototype?.render,
) as FC<ConfirmModalProps>;
export const ModalRoot = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) {
return m[prop];
export const ModalRoot = Object.values(
findModule((m: any) => {
if (typeof m !== 'object') return false;
for (let prop in m) {
if (m[prop]?.m_mapModalManager && Object.values(m)?.find((x: any) => x?.type)) {
return true;
}
}
}
}) as FC<ModalRootProps>;
return false;
}) || {},
)?.find((x: any) => x?.type?.toString()?.includes('((function(){')) as FC<ModalRootProps>;
interface SimpleModalProps {
active?: boolean;
children: ReactNode;
}
const [ModalModule, _ModalPosition] = findModuleDetailsByExport((e: Export) => e?.toString().includes('.ModalPosition'), 5)
const ModalModuleProps = ModalModule ? Object.values(ModalModule) : [];
export const SimpleModal = ModalModuleProps.find((prop) => {
const string = prop?.toString();
return string?.includes('.ShowPortalModal()') && string?.includes('.OnElementReadyCallbacks.Register(');
}) as FC<SimpleModalProps>;
export const ModalPosition = _ModalPosition as FC<SimpleModalProps>;

26
src/components/Panel.ts Normal file
View File

@@ -0,0 +1,26 @@
import { FC, ReactNode } from 'react';
import { Export, findModuleDetailsByExport } from '../webpack';
// TODO where did this go?
// export const Panel: FC<{ children?: ReactNode; }> = findModuleExport((e: Export) => {
// if (typeof mod !== 'object' || !mod.__esModule) return undefined;
// return mod.Panel;
// });
export interface PanelSectionProps {
title?: string;
spinner?: boolean;
children?: ReactNode;
}
const [mod, panelSection] = findModuleDetailsByExport((e: Export) => e.toString()?.includes('.PanelSection'));
export const PanelSection = panelSection as FC<PanelSectionProps>;
export interface PanelSectionRowProps {
children?: ReactNode;
}
export const PanelSectionRow = Object.values(mod).filter(
(exp: any) => !exp?.toString()?.includes('.PanelSection'),
)[0] as FC<PanelSectionRowProps>;

View File

@@ -0,0 +1,37 @@
import { ReactNode, FC } from 'react';
import { Export, findModuleExport } from '../webpack';
import { ItemProps } from './Item';
import { createPropListRegex } from '../utils';
export interface ProgressBarItemProps extends ItemProps {
indeterminate?: boolean;
nTransitionSec?: number;
nProgress?: number;
focusable?: boolean;
}
export interface ProgressBarProps {
indeterminate?: boolean;
nTransitionSec?: number;
nProgress?: number;
focusable?: boolean;
}
export interface ProgressBarWithInfoProps extends ProgressBarItemProps {
sTimeRemaining?: ReactNode;
sOperationText?: ReactNode;
}
export const ProgressBar = findModuleExport((e: Export) =>
e?.toString()?.includes('.ProgressBar,"standard"=='),
) as FC<ProgressBarProps>;
export const ProgressBarWithInfo = findModuleExport((e: Export) =>
e?.toString()?.includes('.ProgressBarFieldStatus},'),
) as FC<ProgressBarWithInfoProps>;
const progressBarItemRegex = createPropListRegex(["indeterminate", "nTransitionSec", "nProgress"]);
export const ProgressBarItem = findModuleExport((e: Export) =>
e?.toString && progressBarItemRegex.test(e.toString()),
) as FC<ProgressBarItemProps>;

15
src/components/Scroll.ts Normal file
View File

@@ -0,0 +1,15 @@
import { FC, ReactNode } from 'react';
import { Export, findModuleByExport, findModuleExport } from '../webpack';
const ScrollingModule = findModuleByExport((e: Export) => e?.render?.toString?.().includes('{case"x":'));
const ScrollingModuleProps = ScrollingModule ? Object.values(ScrollingModule) : [];
export const ScrollPanel = ScrollingModuleProps.find((prop: any) =>
prop?.render?.toString?.().includes('{case"x":'),
) as FC<{ children?: ReactNode }>;
export const ScrollPanelGroup: FC<{ children?: ReactNode }> = findModuleExport((e: Export) =>
e?.render?.toString().includes('.FocusVisibleChild()),[])'),
);

View File

@@ -0,0 +1,30 @@
import { ReactNode, FC } from 'react';
import { Export, findModuleExport } from '../webpack';
import { createPropListRegex } from '../utils';
export interface SidebarNavigationPage {
title: ReactNode;
content: ReactNode;
icon?: ReactNode;
visible?: boolean;
hideTitle?: boolean;
identifier?: string;
route?: string;
link?: string;
padding?: 'none' | 'compact';
}
export interface SidebarNavigationProps {
title?: string;
pages: (SidebarNavigationPage | 'separator')[];
showTitle?: boolean;
disableRouteReporting?: boolean;
page?: string;
onPageRequested?: (page: string) => void;
}
const sidebarNavigationRegex = createPropListRegex(["pages", "fnSetNavigateToPage", "disableRouteReporting"]);
export const SidebarNavigation = findModuleExport((e: Export) =>
e?.toString && sidebarNavigationRegex.test(e.toString()),
) as FC<SidebarNavigationProps>;

View File

@@ -25,8 +25,10 @@ export interface SliderFieldProps extends ItemProps {
valueSuffix?: string;
minimumDpadGranularity?: number;
onChange?(value: number): void;
className?: string;
}
export const SliderField = Object.values(CommonUIModule).find((mod: any) =>
mod?.toString()?.includes('SliderField,fallback'),
// stable || beta as of oct 2 2024
mod?.toString()?.includes('SliderField,fallback') || mod?.toString()?.includes("SliderField\",")
) as FC<SliderFieldProps>;

8
src/components/Spinner.ts Executable file
View File

@@ -0,0 +1,8 @@
import { FC, SVGAttributes } from 'react';
import { IconsModule } from '../webpack';
// TODO type this and other icons?
export const Spinner = Object.values(IconsModule)?.find(
(mod: any) => mod?.toString && /Spinner\)}\)?,.\.createElement\(\"path\",{d:\"M18 /.test(mod.toString()),
) as FC<SVGAttributes<SVGElement>>;

12
src/components/SteamSpinner.ts Executable file
View File

@@ -0,0 +1,12 @@
import { FC, ReactNode, SVGAttributes } from 'react';
import { Export, findModuleExport } from '../webpack';
interface SteamSpinnerProps {
children?: ReactNode;
background?: "transparent"; // defaults to black seemingly, but only "transparent" is checked against
}
export const SteamSpinner = findModuleExport(
(e: Export) => e?.toString?.()?.includes('Steam Spinner') && e?.toString?.()?.includes('src'),
) as FC<SVGAttributes<SVGElement> & SteamSpinnerProps>;

70
src/components/Tabs.tsx Normal file
View File

@@ -0,0 +1,70 @@
import { FC, ReactNode } from 'react';
import { findModuleByExport } from '../webpack';
import { FooterLegendProps } from './FooterLegend';
/**
* Individual tab objects for the Tabs component
*
* `id` ID of this tab, can be used with activeTab to auto-focus a given tab
* `title` Title shown in the header bar
* `renderTabAddon` Return a {@link ReactNode} to render it next to the tab title, i.e. the counts for each tab on the Media page
* `content` Content of the tab
* `footer` Sets up button handlers and labels
*/
export interface Tab {
id: string;
title: string;
renderTabAddon?: () => ReactNode;
content: ReactNode;
footer?: FooterLegendProps;
}
/**
* Props for the {@link Tabs}
*
* `tabs` array of {@link Tab}
* `activeTab` tab currently active, needs to be one of the tabs {@link Tab.id}, must be set using a `useState` in the `onShowTab` handler
* `onShowTab` Called when the active tab should change, needs to set `activeTab`. See example.
* `autoFocusContents` Whether to automatically focus the tab contents or not.
* `footer` Sets up button handlers and labels
*
* @example
* const Component: FC = () => {
* const [currentTab, setCurrentTab] = useState<string>("Tab1");
*
* return (
* <Tabs
* title="Theme Manager"
* activeTab={currentTabRoute}
* onShowTab={(tabID: string) => {
* setCurrentTabRoute(tabID);
* }}
* tabs={[
* {
* title: "Tab 1",
* content: <Tab1Component />,
* id: "Tab1",
* },
* {
* title: "Tab 2",
* content: <Tab2Component />,
* id: "Tab2",
* },
* ]}
* />
* );
* };
*/
export interface TabsProps {
tabs: Tab[];
activeTab: string;
onShowTab: (tab: string) => void;
autoFocusContents?: boolean;
}
const tabsModule = findModuleByExport(e => e?.toString()?.includes(".TabRowTabs") && e?.toString()?.includes("activeTab:"));
/**
* Tabs component as used in the library and media tabs. See {@link TabsProps}.
*/
export const Tabs = tabsModule && Object.values(tabsModule).find((e: any) => e?.type?.toString()?.includes("((function()")) as FC<TabsProps>;

View File

@@ -1,4 +1,4 @@
import { ChangeEventHandler, HTMLAttributes, ReactNode, VFC } from 'react';
import { ChangeEventHandler, HTMLAttributes, ReactNode, FC } from 'react';
import { CommonUIModule, Module } from '../webpack';
@@ -25,4 +25,4 @@ export interface TextFieldProps extends HTMLAttributes<HTMLInputElement> {
export const TextField = Object.values(CommonUIModule).find(
(mod: Module) => mod?.validateUrl && mod?.validateEmail,
) as VFC<TextFieldProps>;
) as FC<TextFieldProps>;

View File

@@ -4,11 +4,13 @@ import { CommonUIModule } from '../webpack';
import { ItemProps } from './Item';
export interface ToggleFieldProps extends ItemProps {
highlightOnFocus?: boolean;
checked: boolean;
disabled?: boolean;
onChange?(checked: boolean): void;
}
export const ToggleField = Object.values(CommonUIModule).find((mod: any) =>
mod?.render?.toString()?.includes('ToggleField,fallback'),
// stable || beta as of oct 2 2024
mod?.render?.toString()?.includes('ToggleField,fallback') || mod?.render?.toString()?.includes("ToggleField\",")
) as FC<ToggleFieldProps>;

View File

@@ -1,23 +1,26 @@
export * from './Button';
export * from './ButtonItem';
export * from './Carousel';
export * from './ControlsList';
export * from './Dialog';
export * from './DialogCheckbox';
export * from './Dropdown';
export * from './ErrorBoundary';
export * from './Field';
export * from './Focusable';
export * from './FocusRing';
export * from './FooterLegend';
export * from './Marquee';
export * from './Menu';
export * from './Modal';
export * from './Panel';
export * from './ProgressBar';
export * from './Router';
export * from './SidebarNavigation';
export * from './SliderField';
export * from './Spinner';
export * from './static-classes';
export * from './SteamSpinner';
export * from './Tabs';
export * from './TextField';
export * from './Toggle';
export * from './ToggleField';
export * from './Scroll';

View File

@@ -1,6 +1,7 @@
import { CSSProperties, FC, useState } from 'react';
import { ConfirmModal, SliderField, gamepadSliderClasses } from '../deck-components';
import { ConfirmModal, SliderField } from '../components';
import { gamepadSliderClasses } from '../utils/static-classes';
interface ColorPickerModalProps {
closeModal: () => void;

View File

@@ -0,0 +1,183 @@
import { Fragment, JSXElementConstructor, ReactElement, ReactNode, useEffect, useState } from 'react';
import { Field, FieldProps, Focusable, GamepadButton } from '../components';
/**
* A ReorderableList entry of type <T>.
* @param label The name of this entry in the list.
* @param data Optional data to connect to this entry.
* @param position The position of this entry in the list.
*/
export type ReorderableEntry<T> = {
label: ReactNode;
data?: T;
position: number;
};
/**
* Properties for a ReorderableList component of type <T>.
*
* @param animate If the list should animate. @default true
*/
export type ReorderableListProps<T> = {
entries: ReorderableEntry<T>[];
onSave: (entries: ReorderableEntry<T>[]) => void;
interactables?: JSXElementConstructor<{ entry: ReorderableEntry<T> }>;
fieldProps?: FieldProps;
animate?: boolean;
};
/**
* A component for creating reorderable lists.
*
* See an example implementation {@linkplain https://github.com/Tormak9970/Component-Testing-Plugin/blob/main/src/testing-window/ReorderableListTest.tsx here}.
*/
export function ReorderableList<T>(props: ReorderableListProps<T>) {
if (props.animate === undefined) props.animate = true;
const [entryList, setEntryList] = useState<ReorderableEntry<T>[]>(
[...props.entries].sort((a: ReorderableEntry<T>, b: ReorderableEntry<T>) => a.position - b.position),
);
const [reorderEnabled, setReorderEnabled] = useState<boolean>(false);
useEffect(() => {
setEntryList([...props.entries].sort((a: ReorderableEntry<T>, b: ReorderableEntry<T>) => a.position - b.position));
}, [props.entries]);
function toggleReorderEnabled(): void {
let newReorderValue = !reorderEnabled;
setReorderEnabled(newReorderValue);
if (!newReorderValue) {
props.onSave(entryList);
}
}
function saveOnBackout(e: Event) {
const event = e as CustomEvent;
if (event.detail.button == GamepadButton.CANCEL && reorderEnabled) {
setReorderEnabled(!reorderEnabled);
props.onSave(entryList);
}
}
return (
<Fragment>
<div
style={{
width: 'inherit',
height: 'inherit',
flex: '1 1 1px',
scrollPadding: '48px 0px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-start',
alignContent: 'stretch',
}}
>
<Focusable
onSecondaryButton={toggleReorderEnabled}
onSecondaryActionDescription={reorderEnabled ? 'Save Order' : 'Reorder'}
onClick={toggleReorderEnabled}
onButtonDown={saveOnBackout}
>
{entryList.map((entry: ReorderableEntry<T>) => (
<ReorderableItem
animate={props.animate!}
listData={entryList}
entryData={entry}
reorderEntryFunc={setEntryList}
reorderEnabled={reorderEnabled}
fieldProps={props.fieldProps}
>
{props.interactables ? <props.interactables entry={entry} /> : null}
</ReorderableItem>
))}
</Focusable>
</div>
</Fragment>
);
}
/**
* Properties for a ReorderableItem component of type <T>
*/
export type ReorderableListEntryProps<T> = {
fieldProps?: FieldProps;
listData: ReorderableEntry<T>[];
entryData: ReorderableEntry<T>;
reorderEntryFunc: CallableFunction;
reorderEnabled: boolean;
animate: boolean;
children: ReactElement | null;
};
function ReorderableItem<T>(props: ReorderableListEntryProps<T>) {
const [isSelected, _setIsSelected] = useState<boolean>(false);
const [isSelectedLastFrame, setIsSelectedLastFrame] = useState<boolean>(false);
const listEntries = props.listData;
function onReorder(e: Event): void {
if (!props.reorderEnabled) return;
const event = e as CustomEvent;
const currentIdx = listEntries.findIndex((entryData: ReorderableEntry<T>) => entryData === props.entryData);
const currentIdxValue = listEntries[currentIdx];
if (currentIdx < 0) return;
let targetPosition: number = -1;
if (event.detail.button == GamepadButton.DIR_DOWN) {
targetPosition = currentIdxValue.position + 1;
} else if (event.detail.button == GamepadButton.DIR_UP) {
targetPosition = currentIdxValue.position - 1;
}
if (targetPosition >= listEntries.length || targetPosition < 0) return;
let otherToUpdate = listEntries.find((entryData: ReorderableEntry<T>) => entryData.position === targetPosition);
if (!otherToUpdate) return;
let currentPosition = currentIdxValue.position;
currentIdxValue.position = otherToUpdate.position;
otherToUpdate.position = currentPosition;
props.reorderEntryFunc(
[...listEntries].sort((a: ReorderableEntry<T>, b: ReorderableEntry<T>) => a.position - b.position),
);
}
async function setIsSelected(val: boolean) {
_setIsSelected(val);
// Wait 3 frames, then set. I have no idea why, but if you dont wait long enough it doesn't work.
for (let i = 0; i < 3; i++) await new Promise((res) => requestAnimationFrame(res));
setIsSelectedLastFrame(val);
}
return (
<div
style={
props.animate
? {
transition:
isSelected || isSelectedLastFrame
? ''
: 'transform 0.3s cubic-bezier(0.25, 1, 0.5, 1), opacity 0.3s cubic-bezier(0.25, 1, 0.5, 1)', // easeOutQuart https://easings.net/#easeOutQuart
transform: !props.reorderEnabled || isSelected ? 'scale(1)' : 'scale(0.9)',
opacity: !props.reorderEnabled || isSelected ? 1 : 0.7,
}
: {}
}
>
<Field
label={props.entryData.label}
{...props.fieldProps}
focusable={!props.children}
onButtonDown={onReorder}
onGamepadBlur={() => setIsSelected(false)}
onGamepadFocus={() => setIsSelected(true)}
>
<Focusable style={{ display: 'flex', width: '100%', position: 'relative' }}>{props.children}</Focusable>
</Field>
</div>
);
}

View File

@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { FC, ImgHTMLAttributes, useState } from 'react';
import { Spinner } from '../deck-components';
import { Spinner } from '../components';
interface SuspensefulImageProps extends ImgHTMLAttributes<HTMLImageElement> {
suspenseWidth?: string | number;
@@ -13,6 +13,8 @@ export const SuspensefulImage: FC<SuspensefulImageProps> = (props) => {
const [error, setError] = useState(false);
useEffect(() => {
setLoading(true);
setError(false);
const img = new Image();
img.src = props.src || '';
img.addEventListener('load', () => {
@@ -21,7 +23,7 @@ export const SuspensefulImage: FC<SuspensefulImageProps> = (props) => {
img.addEventListener('error', () => {
setError(true);
});
}, []);
}, [props.src]);
return loading ? (
<div

View File

@@ -1,2 +1,3 @@
export * from './SuspensefulImage';
export * from './ColorPickerModal';
export * from './ReorderableList';

View File

@@ -1 +1 @@
export * from './usequickaccessvisible';
export * from './useQuickAccessVisible';

View File

@@ -0,0 +1,65 @@
import { useEffect, useState } from 'react';
import { getGamepadNavigationTrees } from '../utils';
function getQuickAccessWindow(): Window | null {
const navTrees = getGamepadNavigationTrees();
return (
navTrees.find((tree: any) => tree?.id === 'QuickAccess-NA')?.m_Root?.m_element?.ownerDocument.defaultView ?? null
);
}
/**
* Returns state indicating the visibility of quick access menu.
*
* @deprecated moved to @decky/api
*
* @returns `true` if quick access menu is visible and `false` otherwise.
*
* @example
* import { FC, useEffect } from "react";
* import { useQuickAccessVisible } from "@decky/ui";
*
* export const PluginPanelView: FC<{}> = ({ }) => {
* const isVisible = useQuickAccessVisible();
*
* useEffect(() => {
* if (!isVisible) {
* return;
* }
*
* const interval = setInterval(() => console.log("Hello world!"), 1000);
* return () => {
* clearInterval(interval);
* }
* }, [isVisible])
*
* return (
* <div>
* {isVisible ? "VISIBLE" : "INVISIBLE"}
* </div>
* );
* };
*/
export function useQuickAccessVisible(): boolean {
// By default we say that document is not hidden, unless we know otherwise.
// This would cover the cases when Valve breaks something and the quick access window
// cannot be accessed anymore - the plugins that use this would continue working somewhat.
const [isHidden, setIsHidden] = useState(getQuickAccessWindow()?.document.hidden ?? false);
useEffect(() => {
const quickAccessWindow = getQuickAccessWindow();
if (quickAccessWindow === null) {
console.error('Could not get window of QuickAccess menu!');
return;
}
const onVisibilityChange = () => setIsHidden(quickAccessWindow.document.hidden);
quickAccessWindow.addEventListener('visibilitychange', onVisibilityChange);
return () => {
quickAccessWindow.removeEventListener('visibilitychange', onVisibilityChange);
};
}, []);
return !isHidden;
}

View File

@@ -1,68 +0,0 @@
import { useEffect, useState } from 'react';
declare global {
var FocusNavController: any;
}
/**
* Returns state indicating the visibility of quick access menu.
*
* @remarks
* During development it is possible to open the quick access menu without giving it
* focus in some cases. In such cases, the quick access menu state is invisible.
*
* This seems to be impossible to replicate when running the deck normally. Even in
* the edge cases it always seems to have a focus.
*
* @returns `true` if quick access menu is visible (focused) and `false` otherwise.
*
* @example
* import { VFC, useEffect } from "react";
* import { useQuickAccessVisible } from "decky-frontend-lib";
*
* export const PluginPanelView: VFC<{}> = ({ }) => {
* const isVisible = useQuickAccessVisible();
*
* useEffect(() => {
* if (!isVisible) {
* return;
* }
*
* const interval = setInterval(() => console.log("Hello world!"), 1000);
* return () => {
* clearInterval(interval);
* }
* }, [isVisible])
*
* return (
* <div>
* {isVisible ? "VISIBLE" : "INVISIBLE"}
* </div>
* );
* };
*/
export function useQuickAccessVisible(): boolean {
// Assuming that the component is rendered in QAM already, so true by default...
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
const quickAccessWindow: Window | null =
FocusNavController?.GetGamepadNavTreeByID('QuickAccess-NA')?.m_Root?.m_element?.ownerDocument.defaultView ?? null;
if (quickAccessWindow === null) {
console.error('Could not get window of QuickAccess menu!');
return;
}
const onBlur = () => setIsVisible(false);
const onFocus = () => setIsVisible(true);
quickAccessWindow.addEventListener('blur', onBlur);
quickAccessWindow.addEventListener('focus', onFocus);
return () => {
quickAccessWindow.removeEventListener('blur', onBlur);
quickAccessWindow.removeEventListener('focus', onFocus);
};
}, []);
return isVisible;
}

View File

@@ -1,19 +0,0 @@
import { ElementType, FC, ReactNode } from 'react';
import { findModuleChild } from '../webpack';
export interface FocusRingProps {
className?: string;
rootClassName?: string;
render?: ElementType;
children?: ReactNode;
NavigationManager?: any;
}
export const FocusRing = findModuleChild((m: any) => {
if (typeof m !== 'object') return false;
for (let prop in m) {
if (m[prop]?.toString()?.includes('.GetShowDebugFocusRing())')) return m[prop];
}
return false;
}) as FC<FocusRingProps>;

View File

@@ -1,21 +0,0 @@
import { HTMLAttributes, ReactNode, RefAttributes, VFC } from 'react';
import { findModuleChild } from '../webpack';
import { FooterLegendProps } from './FooterLegend';
export interface FocusableProps extends HTMLAttributes<HTMLDivElement>, FooterLegendProps {
children: ReactNode;
'flow-children'?: string;
focusClassName?: string;
focusWithinClassName?: string;
onActivate?: (e: CustomEvent) => void;
onCancel?: (e: CustomEvent) => void;
}
export const Focusable = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.render?.toString()?.includes('["flow-children","onActivate","onCancel","focusClassName",'))
return m[prop];
}
}) as VFC<FocusableProps & RefAttributes<HTMLDivElement>>;

View File

@@ -1,68 +0,0 @@
import { FC, ReactNode } from 'react';
import { findModuleChild } from '../webpack';
export const showContextMenu: (children: ReactNode, parent?: EventTarget) => void = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (typeof m[prop] === 'function' && m[prop].toString().includes('stopPropagation))')) {
return m[prop];
}
}
});
export interface MenuProps {
label: string;
onCancel?(): void;
cancelText?: string;
children?: ReactNode;
}
export const Menu: FC<MenuProps> = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.prototype?.HideIfSubmenu && m[prop]?.prototype?.HideMenu) {
return m[prop];
}
}
});
export interface MenuGroupProps {
label: string;
disabled?: boolean;
children?: ReactNode;
}
export const MenuGroup: FC<MenuGroupProps> = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.prototype?.RenderSubMenu && m[prop]?.prototype?.ShowSubMenu) {
return m[prop];
}
}
});
export interface MenuItemProps {
onSelected?(): void;
disabled?: boolean;
children?: ReactNode;
}
export const MenuItem: FC<MenuItemProps> = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.prototype?.OnOKButton && m[prop]?.prototype?.OnMouseEnter) {
return m[prop];
}
}
});
/*
all().map(m => {
if (typeof m !== "object") return undefined;
for (let prop in m) { if (m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) return m[prop]}
}).find(x => x)
*/

View File

@@ -1,23 +0,0 @@
import { FC } from 'react';
import { findModuleChild } from '../webpack';
export interface PanelSectionProps {
title?: string;
spinner?: boolean;
}
const [panelSection, mod] = findModuleChild((mod: any) => {
for (let prop in mod) {
if (mod[prop]?.toString()?.includes('.PanelSection')) {
return [mod[prop], mod];
}
}
return null;
});
export const PanelSection = panelSection as FC<PanelSectionProps>;
export const PanelSectionRow = Object.values(mod).filter(
(exp: any) => !exp?.toString()?.includes('.PanelSection'),
)[0] as FC;

View File

@@ -1,44 +0,0 @@
import { ReactNode, VFC } from 'react';
import { findModuleChild } from '../webpack';
import { ItemProps } from './Item';
export interface ProgressBarItemProps extends ItemProps {
indeterminate?: boolean;
nTransitionSec?: number;
nProgress?: number;
focusable?: boolean;
}
export interface ProgressBarProps {
indeterminate?: boolean;
nTransitionSec?: number;
nProgress?: number;
focusable?: boolean;
}
export interface ProgressBarWithInfoProps extends ProgressBarItemProps {
sTimeRemaining?: ReactNode;
sOperationText?: ReactNode;
}
export const ProgressBar = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.toString()?.includes('.ProgressBar,"standard"==')) return m[prop];
}
}) as VFC<ProgressBarProps>;
export const ProgressBarWithInfo = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.toString()?.includes('.ProgressBarFieldStatus},')) return m[prop];
}
}) as VFC<ProgressBarWithInfoProps>;
export const ProgressBarItem = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.toString()?.includes('"indeterminate","nTransitionSec"')) return m[prop];
}
}) as VFC<ProgressBarItemProps>;

View File

@@ -1,98 +0,0 @@
import { Module, findModuleChild } from '../webpack';
export enum SideMenu {
None,
Main,
QuickAccess,
}
export enum QuickAccessTab {
Notifications,
RemotePlayTogetherControls,
VoiceChat,
Friends,
Settings,
Perf,
Help,
Decky,
}
export enum DisplayStatus {
Invalid = 0,
Launching = 1,
Uninstalling = 2,
Installing = 3,
Running = 4,
Validating = 5,
Updating = 6,
Downloading = 7,
Synchronizing = 8,
ReadyToInstall = 9,
ReadyToPreload = 10,
ReadyToLaunch = 11,
RegionRestricted = 12,
PresaleOnly = 13,
InvalidPlatform = 14,
PreloadComplete = 16,
BorrowerLocked = 17,
UpdatePaused = 18,
UpdateQueued = 19,
UpdateRequired = 20,
UpdateDisabled = 21,
DownloadPaused = 22,
DownloadQueued = 23,
DownloadRequired = 24,
DownloadDisabled = 25,
LicensePending = 26,
LicenseExpired = 27,
AvailForFree = 28,
AvailToBorrow = 29,
AvailGuestPass = 30,
Purchase = 31,
Unavailable = 32,
NotLaunchable = 33,
CloudError = 34,
CloudOutOfDate = 35,
Terminating = 36,
}
export type AppOverview = {
appid: string;
display_name: string;
display_status: DisplayStatus;
sort_as: string;
};
export interface Router {
CloseSideMenus(): void;
OpenQuickAccessMenu(quickAccessTab?: QuickAccessTab): void;
GetQuickAccessTab(): QuickAccessTab;
Navigate(path: string): void;
NavigateBackOrOpenMenu(): void;
NavigateToAppProperties(): void;
NavigateToBugForum(): void;
NavigateToExternalWeb(url: string): void;
NavigateToHelp(): void;
NavigateToInvites(): void;
NavigateToRunningApp(replace?: boolean): void;
NavigateToStorage(): void;
NavigateToStore(): void;
NavigateToStoreApp(appId: number | string): void;
NavigateToStoreFreeToPlay(): void;
NavigateToStoreManual(): void;
NavigateToStoreNewReleases(): void;
NavigateToStoreOnSale(): void;
ToggleSideMenu(sideMenu: SideMenu): void;
CloseSideMenus(): void;
OpenSideMenu(sideMenu: SideMenu): void;
OpenPowerMenu(unknown?: any): void;
get RunningApps(): AppOverview[];
get MainRunningApp(): AppOverview | undefined;
}
export const Router = findModuleChild((m: Module) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.Navigate && m[prop]?.NavigationManager) return m[prop];
}
}) as Router;

View File

@@ -1,33 +0,0 @@
import { ReactNode, VFC } from 'react';
import { Module, findModuleChild } from '../webpack';
export interface SidebarNavigationPage {
title: string;
content: ReactNode;
icon?: ReactNode;
visible?: boolean;
hideTitle?: boolean;
identifier?: string;
route?: string;
link?: string;
padding?: 'none' | 'compact';
}
export interface SidebarNavigationProps {
title?: string;
pages: SidebarNavigationPage[];
showTitle?: boolean;
disableRouteReporting?: boolean;
page?: string;
onPageRequested?: (page: string) => void;
}
export const SidebarNavigation = findModuleChild((mod: Module) => {
for (let prop in mod) {
if (mod[prop]?.toString()?.includes('"disableRouteReporting"')) {
return mod[prop];
}
}
return null;
}) as VFC<SidebarNavigationProps>;

View File

@@ -1,8 +0,0 @@
import { FC, SVGAttributes } from 'react';
import { IconsModule } from '../webpack';
// TODO type this and other icons?
export const Spinner = Object.values(IconsModule).find(
(mod: any) => mod?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(mod.toString()),
) as FC<SVGAttributes<SVGElement>>;

View File

@@ -1,11 +0,0 @@
import { FC, SVGAttributes } from 'react';
import { findModuleChild } from '../webpack';
export const SteamSpinner = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.toString()?.includes('Steam Spinner') && m[prop].toString().includes('PreloadThrobber'))
return m[prop];
}
}) as FC<SVGAttributes<SVGElement>>;

View File

@@ -1,135 +0,0 @@
import { FC, ReactNode, createElement, useEffect, useState } from 'react';
import { fakeRenderComponent, findInReactTree, sleep } from '../utils';
import { findModule } from '../webpack';
import { FooterLegendProps } from './FooterLegend';
import { SteamSpinner } from './SteamSpinner';
/**
* Individual tab objects for the Tabs component
*
* `id` ID of this tab, can be used with activeTab to auto-focus a given tab
* `title` Title shown in the header bar
* `renderTabAddon` Return a {@link ReactNode} to render it next to the tab title, i.e. the counts for each tab on the Media page
* `content` Content of the tab
* `footer` Sets up button handlers and labels
*/
export interface Tab {
id: string;
title: string;
renderTabAddon?: () => ReactNode;
content: ReactNode;
footer?: FooterLegendProps;
}
/**
* Props for the {@link Tabs}
*
* `tabs` array of {@link Tab}
* `activeTab` tab currently active, needs to be one of the tabs {@link Tab.id}, must be set using a `useState` in the `onShowTab` handler
* `onShowTab` Called when the active tab should change, needs to set `activeTab`. See example.
* `autoFocusContents` Whether to automatically focus the tab contents or not.
* `footer` Sets up button handlers and labels
*
* @example
* const Component: FC = () => {
* const [currentTab, setCurrentTab] = useState<string>("Tab1");
*
* return (
* <Tabs
* title="Theme Manager"
* activeTab={currentTabRoute}
* onShowTab={(tabID: string) => {
* setCurrentTabRoute(tabID);
* }}
* tabs={[
* {
* title: "Tab 1",
* content: <Tab1Component />,
* id: "Tab1",
* },
* {
* title: "Tab 2",
* content: <Tab2Component />,
* id: "Tab2",
* },
* ]}
* />
* );
* };
*/
export interface TabsProps {
tabs: Tab[];
activeTab: string;
onShowTab: (tab: string) => void;
autoFocusContents?: boolean;
}
declare global {
interface Window {
DeckyPluginLoader: any;
}
}
let tabsComponent: any;
const getTabs = async () => {
if (tabsComponent) return tabsComponent;
while (!window?.DeckyPluginLoader?.routerHook?.routes) {
console.debug('[DFL:Tabs]: Waiting for Decky router...');
await sleep(500);
}
return (tabsComponent = fakeRenderComponent(
() => {
return findInReactTree(
findInReactTree(
window.DeckyPluginLoader.routerHook.routes
.find((x: any) => x.props.path == '/library/app/:appid/achievements')
.props.children.type(),
(x) => x?.props?.scrollTabsTop,
).type({ appid: 1 }),
(x) => x?.props?.tabs,
).type;
},
{
useRef: () => ({ current: { reaction: { track: () => {} } } }),
useContext: () => ({ match: { params: { appid: 1 } } }),
useMemo: () => ({ data: {} }),
},
));
};
let oldTabs: any;
try {
const oldTabsModule = findModule((m: any) => {
if (typeof m !== 'object') return false;
for (let prop in m) {
if (m[prop]?.Unbleed) return true;
}
return false;
});
if (oldTabsModule) oldTabs = Object.values(oldTabsModule).find((x: any) => x?.type?.toString()?.includes("((function(") && x?.type?.toString()?.includes("[\"tabs\""));
} catch (e) {
console.error("Error finding oldTabs:", e)
}
/**
* Tabs component as used in the library and media tabs. See {@link TabsProps}
* Unlike other components in `decky-frontend-lib`, this requires Decky Loader to be running.
*/
export const Tabs = oldTabs || ((props: TabsProps) => {
const found = tabsComponent;
const [tc, setTC] = useState<FC<TabsProps>>(found);
useEffect(() => {
if (found) return;
(async () => {
console.debug('[DFL:Tabs]: Finding component...');
const t = await getTabs();
console.debug('[DFL:Tabs]: Found!');
setTC(t);
})();
}, []);
console.log('tc', tc);
return tc ? createElement(tc, props) : <SteamSpinner />;
}) as FC<TabsProps>;

View File

@@ -1,380 +0,0 @@
import { findModule } from '../webpack';
type QuickAccessMenuClasses = Record<
| 'ActiveTab'
| 'AllTabContents'
| 'BatteryDetailsLabels'
| 'BatteryIcon'
| 'BatteryPercentageLabel'
| 'BatteryProjectedLabel'
| 'BatteryProjectedValue'
| 'BatterySectionContainer'
| 'Blocked'
| 'ComingSoon'
| 'Container'
| 'ContentTransition'
| 'Down'
| 'EmptyNotifications'
| 'Enter'
| 'EnterActive'
| 'Exit'
| 'ExitActive'
| 'FooterBoxShadow'
| 'FriendsListTabPanel'
| 'FriendsTitle'
| 'FullHeight'
| 'HeaderAndFooterVisible'
| 'HeaderContainer'
| 'ItemFocusAnim-darkGrey'
| 'ItemFocusAnim-darkerGrey'
| 'ItemFocusAnim-darkerGrey-nocolor'
| 'ItemFocusAnim-green'
| 'ItemFocusAnim-grey'
| 'ItemFocusAnimBorder-darkGrey'
| 'KeyboardButton'
| 'Label'
| 'LowBattery'
| 'LowBatteryGauge'
| 'Menu'
| 'Open'
| 'PanelExitAnchor'
| 'PanelOuterNav'
| 'PanelSection'
| 'PanelSectionRow'
| 'PanelSectionTitle'
| 'QuickAccessMenu'
| 'ReallyLow'
| 'Remaining'
| 'Selected'
| 'Tab'
| 'TabContentColumn'
| 'TabGroupPanel'
| 'TabPanelHidden'
| 'Tabs'
| 'Text'
| 'Title'
| 'TransitionMenuDelay'
| 'Up'
| 'ViewPlaceholder'
| 'VoiceTab'
| 'duration-app-launch'
| 'focusAnimation'
| 'hoverAnimation',
string
>;
type ScrollPanelClasses = Record<'ScrollBoth' | 'ScrollPanel' | 'ScrollX' | 'ScrollY', string>;
type GamepadDialogClasses = Record<
| 'duration-app-launch'
| 'GamepadDialogContent'
| 'GamepadDialogContent_InnerWidth'
| 'Field'
| 'Button'
| 'NoMinWidth'
| 'ActiveAndUnfocused'
| 'StandaloneFieldSeparator'
| 'StandardPadding'
| 'CompactPadding'
| 'WithDescription'
| 'WithBottomSeparatorStandard'
| 'WithBottomSeparatorThick'
| 'HighlightOnFocus'
| 'ItemFocusAnim-darkerGrey'
| 'ItemFocusAnim-darkGrey'
| 'WithBottomSeparator'
| 'Disabled'
| 'Clickable'
| 'FieldClickTarget'
| 'FieldChildren'
| 'FieldLeadIcon'
| 'FieldLabelRow'
| 'VerticalAlignCenter'
| 'InlineWrapShiftsChildrenBelow'
| 'ExtraPaddingOnChildrenBelow'
| 'ChildrenWidthFixed'
| 'ChildrenWidthGrow'
| 'WithFirstRow'
| 'WithChildrenBelow'
| 'FieldLabel'
| 'FieldLabelValue'
| 'FieldDescription'
| 'ModalPosition'
| 'WithStandardPadding'
| 'slideInAnimation'
| 'BasicTextInput'
| 'Toggle'
| 'ToggleRail'
| 'On'
| 'ToggleSwitch'
| 'LabelFieldValue'
| 'DropDownControlButtonContents'
| 'Spacer'
| 'ControlsListOuterPanel'
| 'StandardSpacing'
| 'ExtraSpacing'
| 'AlignRight'
| 'AlignLeft'
| 'AlignCenter'
| 'ControlsListChild'
| 'QuickAccess-Menu'
| 'BigButtons'
| 'BottomButtons'
| 'ItemFocusAnim-darkerGrey-nocolor'
| 'ItemFocusAnim-grey'
| 'ItemFocusAnimBorder-darkGrey'
| 'ItemFocusAnim-green'
| 'focusAnimation'
| 'hoverAnimation',
string
>;
type QuickAccessControlsClasses = Record<
| 'duration-app-launch'
| 'PanelSection'
| 'PanelSectionTitle'
| 'Text'
| 'PanelSectionRow'
| 'Label'
| 'ComingSoon'
| 'LowBattery'
| 'ReallyLow'
| 'LowBatteryGauge'
| 'Remaining'
| 'EmptyNotifications'
| 'BatterySectionContainer'
| 'BatteryIcon'
| 'BatteryPercentageLabel'
| 'BatteryDetailsLabels'
| 'BatteryProjectedValue'
| 'BatteryProjectedLabel'
| 'ItemFocusAnim-darkerGrey-nocolor'
| 'ItemFocusAnim-darkerGrey'
| 'ItemFocusAnim-darkGrey'
| 'ItemFocusAnim-grey'
| 'ItemFocusAnimBorder-darkGrey'
| 'ItemFocusAnim-green'
| 'focusAnimation'
| 'hoverAnimation',
string
>;
type UpdaterFieldClasses = Record<
| 'duration-app-launch'
| 'OOBEUpdateStatusContainer'
| 'UpdateScreen'
| 'UpdatePanel'
| 'CurrentStatus'
| 'TotalUpdateSize'
| 'ProgressInfoContainer'
| 'TimeRemaining'
| 'BatteryLowWarning'
| 'fadeInAnimation'
| 'ProgressStatus'
| 'UpdateStatusContainer'
| 'UpdaterFieldStatusSuccess'
| 'UpdaterFieldStatusApplying'
| 'TextContainer'
| 'ApplyingText'
| 'UpdateBytesRemaining'
| 'Label'
| 'Numerator'
| 'Separator'
| 'Denominator'
| 'PatchNotes'
| 'PostedTime'
| 'EventDetailTitle'
| 'EventDetailsSubTitle'
| 'EventDetailsBody'
| 'InsufficientBatteryText'
| 'UnsupportedHardwareWarning'
| 'Title'
| 'Text'
| 'Body'
| 'ItemFocusAnim-darkerGrey-nocolor'
| 'ItemFocusAnim-darkerGrey'
| 'ItemFocusAnim-darkGrey'
| 'ItemFocusAnim-grey'
| 'ItemFocusAnimBorder-darkGrey'
| 'ItemFocusAnim-green'
| 'focusAnimation'
| 'hoverAnimation',
string
>;
type PlaySectionClasses = Record<
| 'AchievementCountLabel'
| 'AchievementProgressRow'
| 'ActionSection'
| 'AppButtonsContainer'
| 'Arrow'
| 'AvatarAndPersona'
| 'BreakNarrow'
| 'BreakShort'
| 'BreakTall'
| 'BreakUltraWide'
| 'BreakWide'
| 'ClickablePlayBarItem'
| 'CloudStatusIcon'
| 'CloudStatusLabel'
| 'CloudStatusRow'
| 'CloudSyncProblem'
| 'CloudSynching'
| 'ComingSoon'
| 'Container'
| 'DetailsProgressBar'
| 'DetailsProgressContainer'
| 'DetailsSection'
| 'DetailsSectionExtra'
| 'DetailsSectionStatus'
| 'DotDotDot'
| 'DownloadPaused'
| 'DownloadProgressBar'
| 'Downloading'
| 'FavoriteButton'
| 'Favorited'
| 'GameInfoButton'
| 'GameStat'
| 'GameStatIcon'
| 'GameStatIconForced'
| 'GameStatRight'
| 'GameStatsSection'
| 'GamepadUIBreakNarrow'
| 'GamepadUIBreakShort'
| 'GamepadUIBreakWide'
| 'Glassy'
| 'HideWhenNarrow'
| 'Icon'
| 'Icons'
| 'InPage'
| 'InnerContainer'
| 'InvalidPlatform'
| 'ItemFocusAnim-darkGrey'
| 'ItemFocusAnim-darkerGrey'
| 'ItemFocusAnim-darkerGrey-nocolor'
| 'ItemFocusAnim-green'
| 'ItemFocusAnim-grey'
| 'ItemFocusAnimBorder-darkGrey'
| 'Label'
| 'LastPlayed'
| 'LastPlayedInfo'
| 'MenuActive'
| 'MenuButton'
| 'MiniAchievements'
| 'OfflineMode'
| 'OnlyDownloadBar'
| 'PermanentlyUnavailable'
| 'PlayBar'
| 'PlayBarCloudStatusContainer'
| 'PlayBarDetailLabel'
| 'PlayBarGameIcon'
| 'PlayBarGameName'
| 'PlayBarIconAndGame'
| 'PlayBarLabel'
| 'Playtime'
| 'PlaytimeIcon'
| 'PlaytimeIconForced'
| 'PortraitBar'
| 'Presale'
| 'RecentlyUpdated'
| 'RecentlyUpdatedIcon'
| 'RecentlyUpdatedLink'
| 'RecentlyUpdatedText'
| 'RightBreakNarrow'
| 'RightBreakUltraNarrow'
| 'RightBreakUltraWide'
| 'RightBreakWide'
| 'RightControls'
| 'Row'
| 'SharedLibrary'
| 'StatusAndStats'
| 'StatusNameContainer'
| 'StickyHeader'
| 'StickyHeaderShadow'
| 'SuperimposedGridItems'
| 'SyncAnim'
| 'Visible'
| 'duration-app-launch'
| 'favorited'
| 'focusAnimation'
| 'hoverAnimation',
string
>;
type GamepadSliderClasses = Record<
| 'error-shake-duration'
| 'SliderControlPanelGroup'
| 'SliderControlAndNotches'
| 'WithDefaultValue'
| 'SliderControl'
| 'Disabled'
| 'SliderTrack'
| 'SliderHasNotches'
| 'SliderTrackDark'
| 'SliderHandleContainer'
| 'VerticalLineSliderHandleContainer'
| 'ParenSliderHandleContainer'
| 'SliderHandle'
| 'SliderHandleFocusPop'
| 'VerticalLineSliderHandle'
| 'ParenSliderHandle'
| 'Left'
| 'SliderControlWithIcon'
| 'Icon'
| 'SliderNotchContainer'
| 'SliderNotch'
| 'AlignToEnds'
| 'SliderNotchLabel'
| 'AlignToLeft'
| 'AlignToRight'
| 'SliderNotchTick'
| 'TickActive'
| 'LabelText'
| 'DescriptionValue'
| 'EditableValue'
| 'FakeEditableValue'
| 'RedBorder'
| 'EditableValueSuffix'
| 'ErrorShake'
| 'error-shake'
| 'CompoundSlider'
| 'CompoundSliderSubSlider'
| 'Right'
| 'CompoundSliderSubSliderLabelContainer'
| 'CompoundSliderSubSliderLabelPositioner'
| 'CompoundSliderSubSliderLabel'
| 'CompoundSliderSubSliderLabelInternal'
| 'DefaultValueTickContainer'
| 'DefaultValueTick',
string
>;
export const quickAccessMenuClasses: QuickAccessMenuClasses = findModule(
(mod) => typeof mod === 'object' && mod?.Title?.includes('quickaccessmenu'),
);
/**
* @depreciated please use quickAccessMenuClasses instead
*/
export const staticClasses = quickAccessMenuClasses;
export const scrollPanelClasses: ScrollPanelClasses = findModule(
(mod) => typeof mod === 'object' && mod?.ScrollPanel?.includes('scrollpanel'),
);
/**
* @depreciated please use scrollPanelClasses instead
*/
export const scrollClasses = scrollPanelClasses;
export const gamepadDialogClasses: GamepadDialogClasses = findModule(
(mod) => typeof mod === 'object' && mod?.GamepadDialogContent?.includes('gamepaddialog'),
);
export const quickAccessControlsClasses: QuickAccessControlsClasses = findModule(
(mod) => typeof mod === 'object' && mod?.PanelSection?.includes('quickaccesscontrols'),
);
export const updaterFieldClasses: UpdaterFieldClasses = findModule(
(mod) => typeof mod === 'object' && mod?.OOBEUpdateStatusContainer?.includes('updaterfield'),
);
export const playSectionClasses: PlaySectionClasses = findModule(
(mod) => typeof mod === 'object' && mod?.Container?.includes('appdetailsplaysection'),
);
export const gamepadSliderClasses: GamepadSliderClasses = findModule(
(mod) => typeof mod === 'object' && mod?.SliderControlPanelGroup?.includes('gamepadslider'),
);

321
src/globals/SteamClient.ts Normal file
View File

@@ -0,0 +1,321 @@
declare global {
var SteamClient: SteamClient;
}
export interface Apps {
RegisterForAppOverviewChanges: any;
RegisterForAppDetails: any;
RegisterForLocalizationChanges: any;
RegisterForWorkshopChanges: any;
RegisterForWorkshopItemDownloads: any;
GetLibraryBootstrapData: any;
RegisterForAchievementChanges: any;
GetFriendAchievementsForApp: any;
GetMyAchievementsForApp: any;
AddUserTagToApps: any;
RemoveUserTagFromApps: any;
ClearUserTagsOnApps: any;
ClearAndSetUserTagsOnApp: any;
SetAppHidden: any;
ResetHiddenState: any;
SetAppLaunchOptions: any;
SetAppResolutionOverride: any;
SetAppCurrentLanguage: any;
SetAppAutoUpdateBehavior: any;
SetAppBackgroundDownloadsBehavior: any;
ToggleAppFamilyBlockedState: any;
ToggleAppSteamCloudEnabled: any;
ToggleAppSteamCloudSyncOnSuspendEnabled: any;
ToggleOverrideResolutionForInternalDisplay: any;
ToggleEnableSteamOverlayForApp: any;
ToggleEnableDesktopTheatreForApp: any;
BrowseLocalFilesForApp: any;
BrowseScreenshotsForApp: any;
BrowseScreenshotForApp: any;
BackupFilesForApp: any;
VerifyFilesForApp: any;
CreateDesktopShortcutForApp: any;
JoinAppContentBeta: any;
JoinAppContentBetaByPassword: any;
GetAchievementsInTimeRange: any;
GetSubscribedWorkshopItems: any;
SubscribeWorkshopItem: any;
GetDownloadedWorkshopItems: any;
DownloadWorkshopItem: any;
SetLocalScreenshotCaption: any;
SetLocalScreenshotSpoiler: any;
GetDetailsForScreenshotUpload: any;
UploadLocalScreenshot: any;
DeleteLocalScreenshot: any;
GetScreenshotsInTimeRange: any;
GetFriendsWhoPlay: any;
RequestLegacyCDKeysForApp: any;
GetSoundtrackDetails: any;
GetStoreTagLocalization: any;
GetLaunchOptionsForApp: any;
GetResolutionOverrideForApp: any;
ScanForShortcuts: any;
GetAllShortcuts: any;
GetShortcutData: any;
AddShortcut: any;
RemoveShortcut: any;
InstallFlatpakAppAndCreateShortcut: any;
ListFlatpakApps: any;
UninstallFlatpakApp: any;
ShowControllerConfigurator: any;
SetThirdPartyControllerConfiguration: any;
ToggleAllowDesktopConfiguration: any;
SetControllerRumblePreference: any;
GetCachedAppDetails: any;
SetCachedAppDetails: any;
ReportLibraryAssetCacheMiss: any;
SaveAchievementProgressCache: any;
SetStreamingClientForApp: any;
SetCustomArtworkForApp: any;
ClearCustomArtworkForApp: any;
SetCustomLogoPositionForApp: any;
ClearCustomLogoPositionForApp: any;
RequestIconDataForApp: any;
SpecifyCompatTool: any;
GetAvailableCompatTools: any;
SetShortcutName: any;
SetShortcutExe: any;
SetShortcutStartDir: any;
SetShortcutLaunchOptions: any;
SetShortcutIsVR: any;
PromptToChangeShortcut: any;
PromptToSelectShortcutIcon: any;
InstallApp: any;
RunGame: any;
VerifyApp: any;
StreamGame: any;
CancelLaunch: any;
TerminateApp: any;
UninstallApps: any;
ShowStore: any;
SetDLCEnabled: any;
ContinueGameAction: any;
CancelGameAction: any;
GetActiveGameActions: any;
GetGameActionDetails: any;
GetGameActionForApp: any;
SkipShaderProcessing: any;
MarkEulaAccepted: any;
MarkEulaRejected: any;
LoadEula: any;
GetConflictingFileTimestamps: any;
GetCloudPendingRemoteOperations: any;
ClearProton: any;
RegisterForMarketingMessages: any;
FetchMarketingMessages: any;
MarkMarketingMessageSeen: any;
ReportMarketingMessageSeen: any;
RegisterForGameActionStart: any;
RegisterForGameActionEnd: any;
RegisterForGameActionTaskChange: any;
RegisterForGameActionUserRequest: any;
RegisterForGameActionShowError: any;
RegisterForGameActionShowUI: any;
OpenAppSettingsDialog: any;
}
export interface Window {
RegisterForExternalDisplayChanged: any;
SetManualDisplayScaleFactor: any;
SetAutoDisplayScale: any;
Minimize: any;
ProcessShuttingDown: any;
ToggleMaximize: any;
MoveTo: any;
ResizeTo: any;
SetMinSize: any;
SetResizeGrip: any;
SetComposition: any;
GamescopeBlur: any;
BringToFront: any;
SetForegroundWindow: any;
SetKeyFocus: any;
FlashWindow: any;
StopFlashWindow: any;
ShowWindow: any;
HideWindow: any;
SetWindowIcon: any;
GetWindowDimensions: any;
GetWindowRestoreDetails: any;
PositionWindowRelative: any;
GetMousePositionDetails: any;
IsWindowMinimized: any;
GetBrowserID: any;
}
export interface SteamClient {
Apps: Apps;
Browser: any;
BrowserView: any;
ClientNotifications: any;
Cloud: any;
Console: any;
Downloads: any;
FamilySharing: any;
FriendSettings: any;
Friends: any;
GameSessions: any;
Input: any;
InstallFolder: any;
Installs: any;
MachineStorage: any;
Messaging: any;
Notifications: any;
OpenVR: any;
Overlay: any;
Parental: any;
RegisterIFrameNavigatedCallback: any;
RemotePlay: any;
RoamingStorage: any;
Screenshots: any;
Settings: any;
SharedConnection: any;
Stats: any;
Storage: any;
Streaming: any;
System: any;
UI: any;
URL: any;
Updates: any;
User: any;
WebChat: any;
Window: Window;
}
export interface SteamShortcut {
appid: number;
data: {
bIsApplication: boolean;
strAppName: string;
strExePath: string;
strArguments: string;
strShortcutPath: string;
strSortAs: string;
};
}
/**
* @prop unAppID is not properly set by Steam for non-steam game shortcuts, so it defaults to 0 for them
*/
export interface LifetimeNotification {
unAppID: number;
nInstanceID: number;
bRunning: boolean;
}
export type AppAchievements = {
nAchieved: number;
nTotal: number;
vecAchievedHidden: any[];
vecHighlight: any[];
vecUnachieved: any[];
};
export type AppLanguages = {
strDisplayName: string;
strShortName: string;
};
export type LogoPinPositions = 'BottomLeft' | 'UpperLeft' | 'CenterCenter' | 'UpperCenter' | 'BottomCenter';
export interface LogoPosition {
pinnedPosition: LogoPinPositions;
nWidthPct: number;
nHeightPct: number;
}
export interface AppDetails {
achievements: AppAchievements;
bCanMoveInstallFolder: boolean;
bCloudAvailable: boolean;
bCloudEnabledForAccount: boolean;
bCloudEnabledForApp: boolean;
bCloudSyncOnSuspendAvailable: boolean;
bCloudSyncOnSuspendEnabled: boolean;
bCommunityMarketPresence: boolean;
bEnableAllowDesktopConfiguration: boolean;
bFreeRemovableLicense: boolean;
bHasAllLegacyCDKeys: boolean;
bHasAnyLocalContent: boolean;
bHasLockedPrivateBetas: boolean;
bIsExcludedFromSharing: boolean;
bIsSubscribedTo: boolean;
bOverlayEnabled: boolean;
bOverrideInternalResolution: boolean;
bRequiresLegacyCDKey: boolean;
bShortcutIsVR: boolean;
bShowCDKeyInMenus: boolean;
bShowControllerConfig: boolean;
bSupportsCDKeyCopyToClipboard: boolean;
bVRGameTheatreEnabled: boolean;
bWorkshopVisible: boolean;
eAppOwnershipFlags: number;
eAutoUpdateValue: number;
eBackgroundDownloads: number;
eCloudSync: number;
eControllerRumblePreference: number;
eDisplayStatus: number;
eEnableThirdPartyControllerConfiguration: number;
eSteamInputControllerMask: number;
iInstallFolder: number;
lDiskUsageBytes: number;
lDlcUsageBytes: number;
nBuildID: number;
nCompatToolPriority: number;
nPlaytimeForever: number;
nScreenshots: number;
rtLastTimePlayed: number;
rtLastUpdated: number;
rtPurchased: number;
selectedLanguage: {
strDisplayName: string;
strShortName: string;
};
strCloudBytesAvailable: string;
strCloudBytesUsed: string;
strCompatToolDisplayName: string;
strCompatToolName: string;
strDeveloperName: string;
strDeveloperURL: string;
strDisplayName: string;
strExternalSubscriptionURL: string;
strFlatpakAppID: string;
strHomepageURL: string;
strLaunchOptions: string;
strManualURL: string;
strOwnerSteamID: string;
strResolutionOverride: string;
strSelectedBeta: string;
strShortcutExe: string;
strShortcutLaunchOptions: string;
strShortcutStartDir: string;
strSteamDeckBlogURL: string;
unAppID: number;
vecBetas: any[];
vecDLC: any[];
vecDeckCompatTestResults: any[];
vecLanguages: AppLanguages[];
vecLegacyCDKeys: any[];
vecMusicAlbums: any[];
vecPlatforms: string[];
vecScreenShots: any[];
libraryAssets?: {
logoPosition?: LogoPosition;
};
}
export interface SteamAppOverview {
display_name: string;
gameid: string;
appid: number;
icon_hash: string;
third_party_mod?: boolean;
selected_clientid?: string;
BIsModOrShortcut: () => boolean;
BIsShortcut: () => boolean;
}

2
src/globals/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './SteamClient';
export * from './stores';

57
src/globals/stores.ts Normal file
View File

@@ -0,0 +1,57 @@
import { WindowRouter } from '../modules/Router';
import { AppDetails, LogoPosition, SteamAppOverview } from './SteamClient';
declare global {
interface Window {
LocalizationManager: {
m_mapTokens: Map<string, string>;
m_mapFallbackTokens: Map<string, string>;
m_rgLocalesToUse: string[];
};
App: {
m_CurrentUser: {
bIsLimited: boolean;
bIsOfflineMode: boolean;
bSupportAlertActive: boolean;
bCanInviteFriends: boolean;
NotificationCounts: {
comments: number;
inventory_items: number;
invites: number;
gifts: number;
offline_messages: number;
trade_offers: number;
async_game_updates: number;
moderator_messages: number;
help_request_replies: number;
};
strAccountBalance: string;
strAccountName: string;
strSteamID: string;
};
};
appStore: {
GetAppOverviewByAppID: (appId: number) => SteamAppOverview | null;
GetCustomVerticalCapsuleURLs: (app: SteamAppOverview) => string[];
GetCustomLandcapeImageURLs: (app: SteamAppOverview) => string[];
GetCustomHeroImageURLs: (app: SteamAppOverview) => string[];
GetCustomLogoImageURLs: (app: SteamAppOverview) => string[];
GetLandscapeImageURLForApp: (app: SteamAppOverview) => string;
GetVerticalCapsuleURLForApp: (app: SteamAppOverview) => string;
GetCachedLandscapeImageURLForApp: (app: SteamAppOverview) => string;
GetCachedVerticalImageURLForApp: (app: SteamAppOverview) => string;
GetPregeneratedVerticalCapsuleForApp: (app: SteamAppOverview) => string;
GetIconURLForApp: (app: SteamAppOverview) => string;
};
appDetailsStore: {
GetAppDetails: (appId: number) => AppDetails | null;
GetCustomLogoPosition: (app: SteamAppOverview) => LogoPosition | null;
SaveCustomLogoPosition: (app: SteamAppOverview, logoPositions: LogoPosition) => any;
};
SteamUIStore: {
GetFocusedWindowInstance: () => WindowRouter;
};
securitystore: {
IsLockScreenActive: () => boolean;
};
}
}

View File

@@ -1,8 +1,20 @@
// export * from './deck-libs';
export * from './custom-components';
export * from './custom-hooks';
export * from './deck-components';
export * from './components';
export * from './deck-hooks';
export * from './plugin';
export * from './modules';
export * from './globals';
export * from './webpack';
export * from './utils';
export * from './class-mapper';
/**
* @deprecated use @decky/api instead
*/
export const definePlugin = (fn: any): any => {
return (...args: any[]) => {
// TODO: Maybe wrap this
return fn(...args);
};
};

77
src/logger.ts Normal file
View File

@@ -0,0 +1,77 @@
const bgStyle1 = 'background: #16a085; color: black;';
export const log = (name: string, ...args: any[]) => {
console.log(
`%c @decky/ui %c ${name} %c`,
bgStyle1,
'background: #1abc9c; color: black;',
'background: transparent;',
...args,
);
};
export const group = (name: string, ...args: any[]) => {
console.group(
`%c @decky/ui %c ${name} %c`,
bgStyle1,
'background: #1abc9c; color: black;',
'background: transparent;',
...args,
);
};
export const groupEnd = (name: string, ...args: any[]) => {
console.groupEnd();
if (args?.length > 0)
console.log(
`^ %c @decky/ui %c ${name} %c`,
bgStyle1,
'background: #1abc9c; color: black;',
'background: transparent;',
...args,
);
};
export const debug = (name: string, ...args: any[]) => {
console.debug(`%c @decky/ui %c ${name} %c`, bgStyle1, 'background: #1abc9c; color: black;', 'color: blue;', ...args);
};
export const warn = (name: string, ...args: any[]) => {
console.warn(`%c @decky/ui %c ${name} %c`, bgStyle1, 'background: #ffbb00; color: black;', 'color: blue;', ...args);
};
export const error = (name: string, ...args: any[]) => {
console.error(`%c @decky/ui %c ${name} %c`, bgStyle1, 'background: #FF0000;', 'background: transparent;', ...args);
};
class Logger {
constructor(private name: string) {
this.name = name;
}
log(...args: any[]) {
log(this.name, ...args);
}
debug(...args: any[]) {
debug(this.name, ...args);
}
warn(...args: any[]) {
warn(this.name, ...args);
}
error(...args: any[]) {
error(this.name, ...args);
}
group(...args: any[]) {
group(this.name, ...args);
}
groupEnd(...args: any[]) {
groupEnd(this.name, ...args);
}
}
export default Logger;

183
src/modules/Router.ts Normal file
View File

@@ -0,0 +1,183 @@
import Logger from '../logger';
import { Export, findModuleExport } from '../webpack';
export enum SideMenu {
None,
Main,
QuickAccess,
}
export enum QuickAccessTab {
Notifications,
RemotePlayTogetherControls,
VoiceChat,
Friends,
Settings,
Perf,
Help,
Music,
Decky = 999,
}
export enum DisplayStatus {
Invalid = 0,
Launching = 1,
Uninstalling = 2,
Installing = 3,
Running = 4,
Validating = 5,
Updating = 6,
Downloading = 7,
Synchronizing = 8,
ReadyToInstall = 9,
ReadyToPreload = 10,
ReadyToLaunch = 11,
RegionRestricted = 12,
PresaleOnly = 13,
InvalidPlatform = 14,
PreloadComplete = 16,
BorrowerLocked = 17,
UpdatePaused = 18,
UpdateQueued = 19,
UpdateRequired = 20,
UpdateDisabled = 21,
DownloadPaused = 22,
DownloadQueued = 23,
DownloadRequired = 24,
DownloadDisabled = 25,
LicensePending = 26,
LicenseExpired = 27,
AvailForFree = 28,
AvailToBorrow = 29,
AvailGuestPass = 30,
Purchase = 31,
Unavailable = 32,
NotLaunchable = 33,
CloudError = 34,
CloudOutOfDate = 35,
Terminating = 36,
}
export type AppOverview = {
appid: string;
display_name: string;
display_status: DisplayStatus;
sort_as: string;
};
export interface MenuStore {
OpenSideMenu(sideMenu: SideMenu): void;
OpenQuickAccessMenu(quickAccessTab?: QuickAccessTab): void;
OpenMainMenu(): void;
}
export interface WindowRouter {
BrowserWindow: Window;
MenuStore: MenuStore;
Navigate(path: string): void;
NavigateToChat(): void;
NavigateToSteamWeb(url: string): void;
NavigateBack(): void;
}
export interface WindowStore {
GamepadUIMainWindowInstance?: WindowRouter; // Current
SteamUIWindows: WindowRouter[];
OverlayWindows: WindowRouter[]; // Used by desktop GamepadUI
}
export interface Router {
WindowStore?: WindowStore;
/** @deprecated use {@link Navigation} instead */
CloseSideMenus(): void;
/** @deprecated use {@link Navigation} instead */
Navigate(path: string): void;
/** @deprecated use {@link Navigation} instead */
NavigateToAppProperties(): void;
/** @deprecated use {@link Navigation} instead */
NavigateToExternalWeb(url: string): void;
/** @deprecated use {@link Navigation} instead */
NavigateToInvites(): void;
/** @deprecated use {@link Navigation} instead */
NavigateToChat(): void;
/** @deprecated use {@link Navigation} instead */
NavigateToLibraryTab(): void;
/** @deprecated use {@link Navigation} instead */
NavigateToLayoutPreview(e: unknown): void;
/** @deprecated use {@link Navigation} instead */
OpenPowerMenu(unknown?: any): void;
get RunningApps(): AppOverview[];
get MainRunningApp(): AppOverview | undefined;
}
export const Router = findModuleExport((e: Export) => e.Navigate && e.NavigationManager) as Router;
export interface Navigation {
Navigate(path: string): void;
NavigateBack(): void;
NavigateToAppProperties(): void;
NavigateToExternalWeb(url: string): void;
NavigateToInvites(): void;
NavigateToChat(): void;
NavigateToLibraryTab(): void;
NavigateToLayoutPreview(e: unknown): void;
NavigateToSteamWeb(url: string): void;
OpenSideMenu(sideMenu: SideMenu): void;
OpenQuickAccessMenu(quickAccessTab?: QuickAccessTab): void;
OpenMainMenu(): void;
OpenPowerMenu(unknown?: any): void;
/** if calling this to perform navigation, call it after Navigate to prevent a race condition in desktop Big Picture mode that hides the overlay unintentionally */
CloseSideMenus(): void;
}
export let Navigation = {} as Navigation;
const logger = new Logger("Navigation");
try {
function createNavigationFunction(fncName: string, handler?: (win: any) => any) {
return (...args: any) => {
let win: WindowRouter | undefined;
try {
win = window.SteamUIStore.GetFocusedWindowInstance();
} catch (e) {
logger.warn("Navigation interface failed to call GetFocusedWindowInstance", e);
}
if (!win) {
logger.warn("Navigation interface could not find any focused window. Falling back to GamepadUIMainWindowInstance");
win = Router.WindowStore?.GamepadUIMainWindowInstance;
}
if (win) {
try {
const thisObj = handler && handler(win);
(thisObj || win)[fncName](...args);
} catch (e) {
logger.error("Navigation handler failed", e);
}
} else {
logger.error("Navigation interface could not find a window to navigate");
}
}
}
const newNavigation = {
Navigate: createNavigationFunction("Navigate"),
NavigateBack: createNavigationFunction("NavigateBack"),
NavigateToAppProperties: createNavigationFunction("AppProperties", win => win.Navigator),
NavigateToExternalWeb: createNavigationFunction("ExternalWeb", win => win.Navigator),
NavigateToInvites: createNavigationFunction("Invites", win => win.Navigator),
NavigateToChat: createNavigationFunction("Chat", win => win.Navigator),
NavigateToLibraryTab: createNavigationFunction("LibraryTab", win => win.Navigator),
NavigateToLayoutPreview: Router.NavigateToLayoutPreview?.bind(Router),
NavigateToSteamWeb: createNavigationFunction("NavigateToSteamWeb"),
OpenSideMenu: createNavigationFunction("OpenSideMenu", win => win.MenuStore),
OpenQuickAccessMenu: createNavigationFunction("OpenQuickAccessMenu", win => win.MenuStore),
OpenMainMenu: createNavigationFunction("OpenMainMenu", win => win.MenuStore),
CloseSideMenus: createNavigationFunction("CloseSideMenus", win => win.MenuStore),
OpenPowerMenu: Router.OpenPowerMenu?.bind(Router),
} as Navigation;
Object.assign(Navigation, newNavigation);
} catch (e) {
logger.error('Error initializing Navigation interface', e);
}

1
src/modules/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './Router';

View File

@@ -1,74 +0,0 @@
import type { ComponentType, ReactNode } from 'react';
import { RouteProps } from 'react-router';
export interface Plugin {
title: JSX.Element;
icon: JSX.Element;
content?: JSX.Element;
onDismount?(): void;
alwaysRender?: boolean;
}
interface ServerResponseSuccess<TRes> {
success: true;
result: TRes;
}
interface ServerResponseError {
success: false;
result: string;
}
export type ServerResponse<TRes> = ServerResponseSuccess<TRes> | ServerResponseError;
export type RoutePatch = (route: RouteProps) => RouteProps;
export interface RouterHook {
addRoute(path: string, component: ComponentType, props?: Omit<RouteProps, 'path' | 'children'>): void;
addPatch(path: string, patch: RoutePatch): RoutePatch;
removePatch(path: string, patch: RoutePatch): void;
removeRoute(path: string): void;
}
export interface ToastData {
title: ReactNode;
body: ReactNode;
onClick?: () => void;
logo?: ReactNode;
icon?: ReactNode;
className?: string;
contentClassName?: string;
duration?: number;
critical?: boolean;
}
export interface Toaster {
toast(toast: ToastData): void;
}
export interface FilePickerRes {
path: string;
realpath: string;
}
export interface ServerAPI {
routerHook: RouterHook;
toaster: Toaster;
openFilePicker(startPath: string, includeFiles?: boolean, regex?: RegExp): Promise<FilePickerRes>;
callPluginMethod<TArgs = {}, TRes = {}>(methodName: string, args: TArgs): Promise<ServerResponse<TRes>>;
callServerMethod<TArgs = {}, TRes = {}>(methodName: string, args: TArgs): Promise<ServerResponse<TRes>>;
fetchNoCors<TRes = {}>(url: RequestInfo, request?: RequestInit): Promise<ServerResponse<TRes>>;
executeInTab(tab: string, runAsync: boolean, code: string): Promise<unknown>;
injectCssIntoTab<TRes = string>(tab: string, style: string): Promise<ServerResponse<TRes>>;
removeCssFromTab(tab: string, cssId: string): Promise<unknown>;
}
type DefinePluginFn = (serverAPI: ServerAPI) => Plugin;
// TypeScript helper function
export const definePlugin = (fn: DefinePluginFn): DefinePluginFn => {
return (...args) => {
// TODO: Maybe wrap this
return fn(...args);
};
};

View File

@@ -1,5 +1,13 @@
export * from './patcher';
export * from './react';
export * from './static-classes';
export * from './react/react';
export * from './react/fc';
export * from './react/treepatcher';
declare global {
var FocusNavController: any;
var GamepadNavTree: any;
}
export function joinClassNames(...classes: string[]): string {
return classes.join(' ');
@@ -16,6 +24,22 @@ export function findSP(): Window {
// old (SP as host)
if (document.title == 'SP') return window;
// new (SP as popup)
return FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.find((x: any) => x.m_ID == 'root_1_').Root
.Element.ownerDocument.defaultView;
const navTrees = getGamepadNavigationTrees();
return navTrees?.find((x: any) => x.m_ID == 'root_1_')?.Root?.Element?.ownerDocument?.defaultView;
}
/**
* Gets the correct FocusNavController, as the Feb 22 2023 beta has two for some reason.
*/
export function getFocusNavController(): any {
return window.GamepadNavTree?.m_context?.m_controller || window.FocusNavController;
}
/**
* Gets the gamepad navigation trees as Valve seems to be moving them.
*/
export function getGamepadNavigationTrees(): any {
const focusNav = getFocusNavController();
const context = focusNav?.m_ActiveContext || focusNav?.m_LastActiveContext;
return context?.m_rgGamepadNavigationTrees;
}

View File

@@ -6,7 +6,7 @@ export interface PatchOptions {
singleShot?: boolean;
}
type GenericPatchHandler = (args: any[], ret?: any) => any;
export type GenericPatchHandler = (args: any[], ret?: any) => any;
export interface Patch {
original: Function;

139
src/utils/react/fc.ts Normal file
View File

@@ -0,0 +1,139 @@
// Utilities for patching function components
import { createElement, type FC } from 'react';
import { applyHookStubs, removeHookStubs } from './react';
import Logger from '../../logger';
export interface FCTrampoline {
component: FC
}
let loggingEnabled = false;
export function setFCTrampolineLoggingEnabled(value: boolean = true) { loggingEnabled = value };
let logger = new Logger('FCTrampoline');
/**
* Directly hooks a function component from its reference, redirecting it to a user-patchable wrapper in its returned object.
* This only works if the original component when called directly returns either nothing, null, or another child element.
*
* This works by tricking react into thinking it's a class component by cleverly working around its class component checks,
* keeping the unmodified function component intact as a mostly working constructor (as it is impossible to direcly modify a function),
* stubbing out hooks to prevent errors by detecting setter/getter triggers that occur direcly before/after the class component is instantiated by react,
* and creating a fake class component render method to trampoline out into your own handler.
*
* Due to the nature of this method of hooking a component, please only use this where it is *absolutely necessary.*
* Incorrect hook stubs can cause major instability, be careful when writing them. Refer to fakeRenderComponent for the hook stub implementation.
* Make sure your hook stubs can handle all the cases they could possibly need to within the component you are injecting into.
* You do not need to worry about its children, as they are never called due to the first instance never actually rendering.
*/
export function injectFCTrampoline(component: FC, customHooks?: any): FCTrampoline {
// It needs to be wrapped so React doesn't infinitely call the fake class render method.
const newComponent = function (this: any, ...args: any) {
loggingEnabled && logger.debug("new component rendering with props", args);
return component.apply(this, args);
}
const userComponent = { component: newComponent };
// Create a fake class component render method
component.prototype.render = function (...args: any[]) {
loggingEnabled && logger.debug("rendering trampoline", args, this);
// Pass through rendering via creating the component as a child so React can use function component logic instead of class component logic (setting up the hooks)
return createElement(userComponent.component, this.props, this.props.children);
};
component.prototype.isReactComponent = true;
let stubsApplied = false;
let oldCreateElement = window.SP_REACT.createElement;
const applyStubsIfNeeded = () => {
if (!stubsApplied) {
loggingEnabled && logger.debug("applied stubs");
stubsApplied = true;
applyHookStubs(customHooks)
// we have to redirect this to return an object with component's prototype as a constructor returning a value overrides its prototype
window.SP_REACT.createElement = () => {
loggingEnabled && logger.debug("createElement hook called");
loggingEnabled && console.trace("createElement trace");
return Object.create(component.prototype);
};
}
}
const removeStubsIfNeeded = () => {
if (stubsApplied) {
loggingEnabled && logger.debug("removed stubs");
stubsApplied = false;
removeHookStubs();
window.SP_REACT.createElement = oldCreateElement;
}
}
let renderHookStep = 0;
// Accessed two times, once directly before class instantiation, and again in some extra logic we don't need to worry about that we hanlde below just in case.
Object.defineProperty(component, "contextType", {
configurable: true,
get: function () {
loggingEnabled && logger.debug("get contexttype", this, stubsApplied, renderHookStep);
loggingEnabled && console.trace("contextType trace");
if (renderHookStep == 0) renderHookStep = 1;
else if (renderHookStep == 3) renderHookStep = 4;
return this._contextType;
},
set: function (value) {
this._contextType = value;
}
});
// Always accessed directly after contextType for the path we want to catch.
Object.defineProperty(component, "contextTypes", {
configurable: true,
get: function () {
loggingEnabled && logger.debug("get contexttypes", this, stubsApplied, renderHookStep);
loggingEnabled && console.trace("contextTypes trace");
if (renderHookStep == 1) {
renderHookStep = 2;
applyStubsIfNeeded();
};
return this._contextTypes;
},
set: function (value) {
this._contextTypes = value;
}
});
// Set directly after class is instantiated
Object.defineProperty(component.prototype, "updater", {
configurable: true,
get: function () {
return this._updater;
},
set: function (value) {
loggingEnabled && logger.debug("set updater", this, value, stubsApplied, renderHookStep);
loggingEnabled && console.trace("updater trace");
if (renderHookStep == 2) {
renderHookStep = 0;
removeStubsIfNeeded();
}
return this._updater = value;
}
});
// Prevents the second contextType+contextTypes access from leaving its hooks around
Object.defineProperty(component, "getDerivedStateFromProps", {
configurable: true,
get: function () {
loggingEnabled && logger.debug("get getDerivedStateFromProps", this, stubsApplied, renderHookStep);
loggingEnabled && console.trace("getDerivedStateFromProps trace");
if (renderHookStep == 2) {
renderHookStep = 0;
removeStubsIfNeeded();
}
return this._getDerivedStateFromProps;
},
set: function (value) {
this._getDerivedStateFromProps = value;
}
});
return userComponent;
}

View File

@@ -8,13 +8,37 @@ declare global {
}
}
export function fakeRenderComponent(fun: Function, customHooks: any = {}): any {
/**
* Create a Regular Expression to search for a React component that uses certain props in order.
*
* @export
* @param {string[]} propList Ordererd list of properties to search for
* @returns {RegExp} RegEx to call .test(component.toString()) on
*/
export function createPropListRegex(propList: string[], fromStart: boolean = true): RegExp {
let regexString = fromStart ? "const\{" : "";
propList.forEach((prop: any, propIdx) => {
regexString += `"?${prop}"?:[a-zA-Z_$]{1,2}`;
if (propIdx < propList.length - 1) {
regexString += ",";
}
});
// TODO provide a way to enable this
// console.debug(`[DFL:Utils] createPropListRegex generated regex "${regexString}" for props`, propList);
return new RegExp(regexString);
}
let oldHooks = {};
export function applyHookStubs(customHooks: any = {}): any {
const hooks = (window.SP_REACT as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher
.current;
// TODO: add more hooks
let oldHooks = {
oldHooks = {
useContext: hooks.useContext,
useCallback: hooks.useCallback,
useLayoutEffect: hooks.useLayoutEffect,
@@ -38,25 +62,58 @@ export function fakeRenderComponent(fun: Function, customHooks: any = {}): any {
Object.assign(hooks, customHooks);
const res = fun(hooks);
return hooks;
}
export function removeHookStubs() {
const hooks = (window.SP_REACT as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher
.current;
Object.assign(hooks, oldHooks);
oldHooks = {};
}
export function fakeRenderComponent(fun: Function, customHooks?: any): any {
const hooks = applyHookStubs(customHooks);
const res = fun(hooks); // TODO why'd we do this?
removeHookStubs();
return res;
}
export function wrapReactType(node: any, prop: any = 'type') {
return (node[prop] = { ...node[prop] });
if (node[prop]?.__DECKY_WRAPPED) {
return node[prop];
} else {
return (node[prop] = { ...node[prop], __DECKY_WRAPPED: true });
}
}
export function wrapReactClass(node: any, prop: any = 'type') {
const cls = node[prop];
const wrappedCls = class extends cls {};
return (node[prop] = wrappedCls);
if (node[prop]?.__DECKY_WRAPPED) {
return node[prop];
} else {
const cls = node[prop];
const wrappedCls = class extends cls {
static __DECKY_WRAPPED = true;
};
return (node[prop] = wrappedCls);
}
}
export function getReactRoot(o: HTMLElement | Element | Node) {
return (
(o as any)[Object.keys(o).find((k) => k.startsWith('__reactContainer$')) as string] ||
(o as any)['_reactRootContainer']?._internalRoot?.current
);
}
export function getReactInstance(o: HTMLElement | Element | Node) {
return o[Object.keys(o).find((k) => k.startsWith('__reactInternalInstance')) as string];
return (
(o as any)[Object.keys(o).find((k) => k.startsWith('__reactFiber')) as string] ||
(o as any)[Object.keys(o).find((k) => k.startsWith('__reactInternalInstance')) as string]
);
}
// Based on https://github.com/GooseMod/GooseMod/blob/9ef146515a9e59ed4e25665ed365fd72fc0dcf23/src/util/react.js#L20

View File

@@ -0,0 +1,100 @@
import Logger from '../../logger';
import { GenericPatchHandler, afterPatch } from '../patcher';
import { wrapReactClass, wrapReactType } from './react';
// TODO max size limit? could implement as a class extending map perhaps
type PatchedComponentCache = Map<any, any>;
export type GenericNodeStep = (node: any) => any;
// to patch a specific method of a class component other than render. TODO implement
// export type ClassNodeStep = [finder: GenericNodeStep, method: string];
// export type NodeStep = GenericNodeStep | ClassNodeStep;
export type NodeStep = GenericNodeStep;
export type ReactPatchHandler = GenericPatchHandler;
// These will get *very* spammy.
let loggingEnabled = false;
let perfLoggingEnabled = false;
export function setReactPatcherLoggingEnabled(value: boolean = true) { loggingEnabled = value };
export function setReactPatcherPerformanceLoggingEnabled(value: boolean = true) { perfLoggingEnabled = value };
function patchComponent(node: any, handler: GenericPatchHandler, steps: NodeStep[], step: number, caches: PatchedComponentCache[], logger: Logger, prop: string = 'type') {
loggingEnabled && logger.group('Patching node:', node);
// We need to take extra care to not mutate the original node.type
switch (typeof node?.[prop]) {
case 'function':
// Function component
const patch = afterPatch(node, prop, steps[step + 1] ? createStepHandler(handler, steps, step + 1, caches, logger) : handler);
loggingEnabled && logger.debug('Patched a function component', patch);
break;
case 'object':
if (node[prop]?.prototype?.render) {
// Class component
// TODO handle patching custom methods
wrapReactClass(node);
const patch = afterPatch(node[prop].prototype, 'render', steps[step + 1] ? createStepHandler(handler, steps, step + 1, caches, logger) : handler);
loggingEnabled && logger.debug('Patched class component', patch);
} else {
loggingEnabled && logger.debug('Patching forwardref/memo');
wrapReactType(node, prop);
// Step down the object
patchComponent(node[prop], handler, steps, step, caches, logger, node[prop]?.render ? 'render' : 'type');
}
break;
default:
logger.error('Unhandled component type', node);
break;
}
loggingEnabled && logger.groupEnd();
}
function handleStep(tree: any, handler: GenericPatchHandler, steps: NodeStep[], step: number, caches: PatchedComponentCache[], logger: Logger): any {
const startTime = (loggingEnabled || perfLoggingEnabled) ? performance.now() : 0;
const stepHandler = steps[step];
const cache = caches[step] || (caches[step] = new Map());
loggingEnabled && logger.debug(`Patch step ${step} running`, { tree, stepHandler, step, caches });
const node = stepHandler(tree);
if (node && node.type) {
loggingEnabled && logger.debug('Found node', node);
} else if (node) {
loggingEnabled && logger.error('Found node without type. Something is probably wrong.', node);
return tree;
} else {
loggingEnabled && logger.warn('Found no node. Depending on your usecase, this might be fine.', node);
return tree;
}
let cachedType;
if (cachedType = cache.get(node.type)) {
loggingEnabled && logger.debug('Found cached patched component', node);
node.type = cachedType;
(loggingEnabled || perfLoggingEnabled) && logger.debug(`Patch step ${step} took ${performance.now() - startTime}ms with cache`);
return tree;
}
const originalType = node.type;
patchComponent(node, handler, steps, step, caches, logger);
cache.set(originalType, node.type);
(loggingEnabled || perfLoggingEnabled) && logger.debug(`Patch step ${step} took ${performance.now() - startTime}ms`);
return tree;
};
function createStepHandler(handler: GenericPatchHandler, steps: NodeStep[], step: number, caches: PatchedComponentCache[], logger: Logger) {
loggingEnabled && logger.debug(`Creating handler for step ${step}`);
return (_: any, tree: any) => handleStep(tree, handler, steps, step, caches, logger);
}
// TODO handle createReactTreePatcher inside handler and cache it (or warn)
export function createReactTreePatcher(steps: NodeStep[], handler: GenericPatchHandler, debugName: string = 'ReactPatch'): GenericPatchHandler {
const caches: PatchedComponentCache[] = [];
const logger = new Logger(`ReactTreePatcher -> ${debugName}`);
loggingEnabled && logger.debug('Init with options:', steps, debugName);
return createStepHandler(handler, steps, 0, caches, logger);
}

1104
src/utils/static-classes.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,61 +1,105 @@
import Logger from './logger';
declare global {
interface Window {
webpackJsonp: any;
webpackChunksteamui: any;
}
}
// TODO
const logger = new Logger('Webpack');
// In most case an object with getters for each property. Look for the first call to r.d in the module, usually near or at the top.
export type ModuleID = string; // number string
export type Module = any;
export type Export = any;
type FilterFn = (module: any) => boolean;
type ExportFilterFn = (moduleExport: any, exportName?: any) => boolean;
type FindFn = (module: any) => any;
export let webpackCache: any = {};
let hasWebpack5 = false;
export let modules = new Map<ModuleID, Module>();
if (window.webpackJsonp && !window.webpackJsonp.deckyShimmed) {
// Webpack 4, currently on stable
const wpRequire = window.webpackJsonp.push([
[],
{ get_require: (mod: any, _exports: any, wpRequire: any) => (mod.exports = wpRequire) },
[['get_require']],
]);
delete wpRequire.m.get_require;
delete wpRequire.c.get_require;
webpackCache = wpRequire.c;
} else {
function initModuleCache() {
const startTime = performance.now();
logger.group('Webpack Module Init');
// Webpack 5, currently on beta
hasWebpack5 = true;
const id = Math.random();
let initReq: any;
// Generate a fake module ID
const id = Symbol("@decky/ui");
let webpackRequire!: ((id: any) => Module) & { m: object };
// Insert our module in a new chunk.
// The module will then be called with webpack's internal require function as its first argument
window.webpackChunksteamui.push([
[id],
{},
(r: any) => {
initReq = r;
webpackRequire = r;
},
]);
for (let i of Object.keys(initReq.m)) {
webpackCache[i] = initReq(i);
logger.log(
'Initializing all modules. Errors here likely do not matter, as they are usually just failing module side effects.',
);
// Loop over every module ID
for (let id of Object.keys(webpackRequire.m)) {
try {
const module = webpackRequire(id);
if (module) {
modules.set(id, module);
}
} catch (e) {
logger.debug('Ignoring require error for module', id, e);
}
}
logger.groupEnd(`Modules initialized in ${performance.now() - startTime}ms...`);
}
export const allModules: Module[] = hasWebpack5
? Object.values(webpackCache).filter((x) => x)
: Object.keys(webpackCache)
.map((x) => webpackCache[x].exports)
.filter((x) => x);
initModuleCache();
export const findModule = (filter: FilterFn) => {
for (const m of allModules) {
for (const m of modules.values()) {
if (m.default && filter(m.default)) return m.default;
if (filter(m)) return m;
}
};
export const findModuleDetailsByExport = (
filter: ExportFilterFn,
minExports?: number,
): [module: Module | undefined, moduleExport: any, exportName: any, moduleID: string | undefined] => {
for (const [id, m] of modules) {
if (!m) continue;
for (const mod of [m.default, m]) {
if (typeof mod !== 'object') continue;
if (minExports && Object.keys(mod).length < minExports) continue;
for (let exportName in mod) {
if (mod?.[exportName]) {
const filterRes = filter(mod[exportName], exportName);
if (filterRes) {
return [mod, mod[exportName], exportName, id];
} else {
continue;
}
}
}
}
}
return [undefined, undefined, undefined, undefined];
};
export const findModuleByExport = (filter: ExportFilterFn, minExports?: number) => {
return findModuleDetailsByExport(filter, minExports)?.[0];
};
export const findModuleExport = (filter: ExportFilterFn, minExports?: number) => {
return findModuleDetailsByExport(filter, minExports)?.[1];
};
/**
* @deprecated use findModuleExport instead
*/
export const findModuleChild = (filter: FindFn) => {
for (const m of allModules) {
for (const m of modules.values()) {
for (const mod of [m.default, m]) {
const filterRes = filter(mod);
if (filterRes) {
@@ -67,10 +111,13 @@ export const findModuleChild = (filter: FindFn) => {
}
};
/**
* @deprecated use createModuleMapping instead
*/
export const findAllModules = (filter: FilterFn) => {
const out = [];
for (const m of allModules) {
for (const m of modules.values()) {
if (m.default && filter(m.default)) out.push(m.default);
if (filter(m)) out.push(m);
}
@@ -78,7 +125,18 @@ export const findAllModules = (filter: FilterFn) => {
return out;
};
export const CommonUIModule = allModules.find((m: Module) => {
export const createModuleMapping = (filter: FilterFn) => {
const mapping = new Map<ModuleID, Module>();
for (const [id, m] of modules) {
if (m.default && filter(m.default)) mapping.set(id, m.default);
if (filter(m)) mapping.set(id, m);
}
return mapping;
};
export const CommonUIModule = findModule((m: Module) => {
if (typeof m !== 'object') return false;
for (let prop in m) {
if (m[prop]?.contextType?._currentValue && Object.keys(m).length > 60) return true;
@@ -86,18 +144,8 @@ export const CommonUIModule = allModules.find((m: Module) => {
return false;
});
export const IconsModule = findModule((m: Module) => {
if (typeof m !== 'object') return false;
for (let prop in m) {
if (m[prop]?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(m[prop].toString())) return true;
}
return false;
});
export const IconsModule = findModuleByExport(
(e) => e?.toString && /Spinner\)}\)?,.\.createElement\(\"path\",{d:\"M18 /.test(e.toString()),
);
export const ReactRouter = allModules.find((m: Module) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.computeRootMatch) return true;
}
return false;
});
export const ReactRouter = findModuleByExport((e) => e.computeRootMatch);

View File

@@ -14,10 +14,9 @@
"noImplicitThis": true,
"noImplicitAny": true,
"strict": true,
"suppressImplicitAnyIndexErrors": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true
"removeComments": true,
"allowSyntheticDefaultImports": true
},
"include": ["src", "globals.d.ts"],
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -1,5 +1,4 @@
{
"githubPages": false,
"categorizeByGroup": false,
"excludeExternals": true
}
"categorizeByGroup": false,
"excludeExternals": true
}