mirror of
https://github.com/SteamDeckHomebrew/decky-frontend-lib.git
synced 2026-05-20 10:00:08 +02:00
Compare commits
224 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
200125cb98 | ||
|
|
5ab8472ffe | ||
|
|
a43cd7f3c5 | ||
|
|
41401b1d01 | ||
|
|
8117693427 | ||
|
|
d6ecba605d | ||
|
|
bf58e08e4b | ||
|
|
e965816144 | ||
|
|
59ce9e450c | ||
|
|
980df687e7 | ||
|
|
bea9ac7774 | ||
|
|
063dedbbc1 | ||
|
|
723e2ff76e | ||
|
|
241b22cad7 | ||
|
|
93c59d8026 | ||
|
|
0f9fb5a3b8 | ||
|
|
8c6043b5a7 | ||
|
|
3aa07dc9ce | ||
|
|
3f335b3583 | ||
|
|
4c97097757 | ||
|
|
1ee43e86d6 | ||
|
|
db6ab9c448 | ||
|
|
4646f22b0c | ||
|
|
7eb484d55c | ||
|
|
5164f980b3 | ||
|
|
0457feec95 | ||
|
|
73b8d52c7f | ||
|
|
2b8d2ae4db | ||
|
|
48de8928e4 | ||
|
|
d60715b755 | ||
|
|
a370c1f7d3 | ||
|
|
d83bada4af | ||
|
|
1f7178f175 | ||
|
|
44fdf9ed3b | ||
|
|
1b5fd78bc2 | ||
|
|
d64c42ac31 | ||
|
|
0ec60edf4d | ||
|
|
2c3a9f81de | ||
|
|
00079a71cc | ||
|
|
c04f024b34 | ||
|
|
5dae77b003 | ||
|
|
6b8c28e69c | ||
|
|
3ef9648355 | ||
|
|
0da85355c2 | ||
|
|
dcdbb2d6c7 | ||
|
|
58e3d35e1e | ||
|
|
bd1dc85b92 | ||
|
|
aa78f4c0a2 | ||
|
|
b04044451a | ||
|
|
5d8f141114 | ||
|
|
2bfe62409f | ||
|
|
f5cefa9eea | ||
|
|
abfd2c0105 | ||
|
|
8cb7c273eb | ||
|
|
c440fa76b1 | ||
|
|
4e4859deb6 | ||
|
|
9579c41c6d | ||
|
|
4dd6afd91b | ||
|
|
b092d49bf8 | ||
|
|
7e0cb153b1 | ||
|
|
b1e503853f | ||
|
|
832ad697c7 | ||
|
|
62b454f712 | ||
|
|
ea19d62dab | ||
|
|
9f262097b9 | ||
|
|
e6e8c91ec7 | ||
|
|
688c7471cd | ||
|
|
6c1b12b95f | ||
|
|
0dfb2cedb6 | ||
|
|
2a78cc0116 | ||
|
|
1716782183 | ||
|
|
0cd498beac | ||
|
|
b728d90263 | ||
|
|
3384d2910d | ||
|
|
78c4cdb9e9 | ||
|
|
d20659e073 | ||
|
|
d91f49e728 | ||
|
|
735080fee3 | ||
|
|
86e6e4c0f5 | ||
|
|
46b1d6e8ea | ||
|
|
a7635b6305 | ||
|
|
bffd530bda | ||
|
|
9c79187d37 | ||
|
|
29e6439115 | ||
|
|
a8eeb917e2 | ||
|
|
17b99dfed8 | ||
|
|
9ef419cee1 | ||
|
|
5aad952936 | ||
|
|
c2b0fad298 | ||
|
|
4d4cfedfe0 | ||
|
|
bb12921863 | ||
|
|
223739af25 | ||
|
|
95d977df45 | ||
|
|
0f2692a3f2 | ||
|
|
ebf496bf61 | ||
|
|
0a5170e412 | ||
|
|
11dd82bbb1 | ||
|
|
153bb209d1 | ||
|
|
e27b638d26 | ||
|
|
7d287f10d6 | ||
|
|
9925bc8cfb | ||
|
|
179a93d5e1 | ||
|
|
5203ce348a | ||
|
|
3f47b5ccce | ||
|
|
d22d32677b | ||
|
|
30e319425b | ||
|
|
aebdfaa089 | ||
|
|
de914b1a35 | ||
|
|
4c4fda47e3 | ||
|
|
678084e4fd | ||
|
|
4f8f65d429 | ||
|
|
8a352d288d | ||
|
|
fd0d011cbf | ||
|
|
70219d90bc | ||
|
|
1ff9351f96 | ||
|
|
c256d341c8 | ||
|
|
c9b583945c | ||
|
|
c213204ff4 | ||
|
|
9128c1e7da | ||
|
|
a06650cf09 | ||
|
|
3170779c6b | ||
|
|
5ffa14bec8 | ||
|
|
ee51dc5fc0 | ||
|
|
b38ec17d8f | ||
|
|
8b54ee990e | ||
|
|
7e01781d40 | ||
|
|
2b31473614 | ||
|
|
8bfeae4b35 | ||
|
|
a7761321bc | ||
|
|
979a630f2b | ||
|
|
a4d1fcf086 | ||
|
|
e9e7f5c026 | ||
|
|
9a691cea09 | ||
|
|
44d9b90cdc | ||
|
|
585c3b8348 | ||
|
|
1287493b63 | ||
|
|
bf01a0184b | ||
|
|
fb5f043ba9 | ||
|
|
26f2c92dce | ||
|
|
01e53d4c13 | ||
|
|
18d341f82a | ||
|
|
3ca8c43a54 | ||
|
|
99353467ad | ||
|
|
b093ce93ba | ||
|
|
74a7cbaf94 | ||
|
|
8fb35e3c09 | ||
|
|
5d5cb31638 | ||
|
|
21f1e5f0af | ||
|
|
0fa43701a9 | ||
|
|
4d52eaea12 | ||
|
|
7e1182a83f | ||
|
|
167ded103a | ||
|
|
b8ddf3d927 | ||
|
|
62f4b354a8 | ||
|
|
79d229be50 | ||
|
|
f43157fde5 | ||
|
|
19819b7a5b | ||
|
|
8752f576a7 | ||
|
|
102a441124 | ||
|
|
62f40bd48f | ||
|
|
a7acc9ae45 | ||
|
|
be7f17a3c3 | ||
|
|
5d097b6108 | ||
|
|
ae98930eeb | ||
|
|
6ce0da996d | ||
|
|
b1591f86bb | ||
|
|
f7318f0210 | ||
|
|
1a34501868 | ||
|
|
7dacb23e8b | ||
|
|
7e5c7b5ac3 | ||
|
|
f71e4dedc8 | ||
|
|
419835204e | ||
|
|
86f33de2c0 | ||
|
|
0b6dc24c0d | ||
|
|
18ce1ad790 | ||
|
|
5a074b5bb6 | ||
|
|
26fae13c8e | ||
|
|
53faf55df1 | ||
|
|
b480d397c4 | ||
|
|
dfcb3bec19 | ||
|
|
cf7dc26a0c | ||
|
|
d3b87b26c5 | ||
|
|
b64dd9f723 | ||
|
|
82214fef4c | ||
|
|
e559a43af9 | ||
|
|
c53d7f8448 | ||
|
|
68a46263a4 | ||
|
|
56e7b2c492 | ||
|
|
1282636b0b | ||
|
|
2343274a5b | ||
|
|
f965c05c27 | ||
|
|
f8c8ae7c29 | ||
|
|
3085efd439 | ||
|
|
db8d91a7ff | ||
|
|
2091daaeac | ||
|
|
b1b2f4fa2d | ||
|
|
b146eab8d7 | ||
|
|
667933bd7c | ||
|
|
dbd01b11ca | ||
|
|
47a6fddc89 | ||
|
|
c57e0eed34 | ||
|
|
5adc5e14ed | ||
|
|
cea315a52c | ||
|
|
5b166d6db8 | ||
|
|
edcc43a6ee | ||
|
|
9723854ddc | ||
|
|
7501817b76 | ||
|
|
4affd4aaec | ||
|
|
6b3db72a14 | ||
|
|
58b69f0d6c | ||
|
|
c62102e993 | ||
|
|
2e66e5a555 | ||
|
|
ce525318d8 | ||
|
|
aac2d520a6 | ||
|
|
e761ee02ba | ||
|
|
c53f87b4a9 | ||
|
|
a656f4e57f | ||
|
|
0b50f2cf0b | ||
|
|
e48c7bbadd | ||
|
|
727fcc8186 | ||
|
|
dc196d53f5 | ||
|
|
f0379e5d19 | ||
|
|
fd94842647 | ||
|
|
3c171cfb8f |
15
.github/workflows/docs.yaml
vendored
15
.github/workflows/docs.yaml
vendored
@@ -1,9 +1,10 @@
|
|||||||
name: Generate docs
|
name: Generate docs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
branches:
|
# push:
|
||||||
- main
|
# branches:
|
||||||
|
# - main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -11,19 +12,19 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Setup | Checkout
|
- name: Setup | Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
path: lib
|
path: lib
|
||||||
- name: Setup | Checkout wiki
|
- name: Setup | Checkout wiki
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: SteamDeckHomebrew/wiki
|
repository: SteamDeckHomebrew/wiki
|
||||||
path: wiki
|
path: wiki
|
||||||
ssh-key: ${{ secrets.SSH_DEPLOY_KEY }}
|
ssh-key: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
- name: Setup | Node.js
|
- name: Setup | Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- name: Setup | Dependencies
|
- name: Setup | Dependencies
|
||||||
@@ -51,4 +52,4 @@ jobs:
|
|||||||
ssh: true
|
ssh: true
|
||||||
directory: ./wiki
|
directory: ./wiki
|
||||||
repository: SteamDeckHomebrew/wiki
|
repository: SteamDeckHomebrew/wiki
|
||||||
branch: main
|
branch: main
|
||||||
|
|||||||
8
.github/workflows/release.yaml
vendored
8
.github/workflows/release.yaml
vendored
@@ -12,13 +12,13 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Setup | Checkout
|
- name: Setup | Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup | Node.js
|
- name: Setup | Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
- name: Setup | Dependencies
|
- name: Setup | Dependencies
|
||||||
run: npm i -g pnpm && pnpm i --frozen-lockfile
|
run: npm i -g pnpm && pnpm i --frozen-lockfile
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -29,5 +29,5 @@ jobs:
|
|||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN_DECKY_ORG }}
|
||||||
run: pnpm exec semantic-release
|
run: pnpm exec semantic-release
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"branches": ["main", "dev"],
|
"branches": ["main", "v4-dev"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
[
|
[
|
||||||
"@semantic-release/commit-analyzer",
|
"@semantic-release/commit-analyzer",
|
||||||
{
|
{
|
||||||
"preset": "angular",
|
"preset": "angular",
|
||||||
"releaseRules": [
|
"releaseRules": [
|
||||||
{"type": "chore", "scope": "classes", "release": "patch"},
|
{ "type": "chore", "scope": "classes", "release": "patch" },
|
||||||
{"type": "*", "scope": "docs", "release": false}
|
{ "type": "docs", "scope": "steamclient", "release": "patch" },
|
||||||
|
{ "type": "*", "scope": "docs", "release": false }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
26
.vscode/tasks.json
vendored
26
.vscode/tasks.json
vendored
@@ -1,15 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"script": "dev",
|
"script": "dev",
|
||||||
"problemMatcher": [
|
"problemMatcher": ["$tsc-watch"],
|
||||||
"$tsc-watch"
|
"label": "npm: dev",
|
||||||
],
|
"detail": "tsc -b -w",
|
||||||
"label": "npm: dev",
|
"isBackground": true
|
||||||
"detail": "tsc -b -w",
|
}
|
||||||
"isBackground": true
|
]
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
509
CHANGELOG.md
509
CHANGELOG.md
@@ -1,3 +1,512 @@
|
|||||||
|
## [4.8.3](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.8.2...v4.8.3) (2024-12-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **utils:** add new nav root name ([a43cd7f](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/a43cd7f3c56a2e30e332417e5b4abde8f4492be1))
|
||||||
|
|
||||||
|
## [4.8.2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.8.1...v4.8.2) (2024-11-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Footer:** add missing focus/nav properties ([#113](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/113)) ([8117693](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/8117693427e4dba2f3b5bd24f36704d8d5e65ae2))
|
||||||
|
|
||||||
|
## [4.8.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.8.0...v4.8.1) (2024-10-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **DialogCheckbox:** prevent some potential issues in dialogcheckbox ([bf58e08](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/bf58e08e4b5b183737c9ad43a858b642ce593f93))
|
||||||
|
* prevent issues where toString may not be a function (what) ([e965816](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/e96581614471524cbf91bf962742df867ba3c3e9))
|
||||||
|
|
||||||
|
# [4.8.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.7.4...v4.8.0) (2024-10-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **components/Modal:** dont break in desktop ui ([bea9ac7](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/bea9ac777452b883c2f3f3ebf8510c22c4577cd0))
|
||||||
|
* **components/Router:** dont break in desktop ui ([980df68](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/980df687e7b7bf389f478e831f992fb9475eeb15))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **utils/react:** add some window-related utils ([063dedb](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/063dedbbc1573a825571c675bf6bfa8a1bc1c6cd))
|
||||||
|
|
||||||
|
## [4.7.4](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v4.7.3...v4.7.4) (2024-10-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **DialogCheckbox:** don't access getters to prevent their side effects from breaking the component ([241b22c](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/241b22cad711621a1695dfd11da857f13c3fffdf))
|
||||||
|
|
||||||
|
## [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)
|
## [3.18.2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.1...v3.18.2) (2022-12-11)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
Library used to develop plugins used for use with [decky-loader](https://github.com/SteamDeckHomebrew/decky-loader).
|
Library used to develop plugins used for use with [decky-loader](https://github.com/SteamDeckHomebrew/decky-loader).
|
||||||
|
|
||||||
## Decky Loader Discord [](https://discord.gg/ZU74G2NJzk)
|
## Decky Loader 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
|
## Developers and Contributors
|
||||||
|
|
||||||
@@ -20,8 +20,11 @@ This library can also theoretically be used to extend existing UI elements of th
|
|||||||
|
|
||||||
### Getting Started (Developers)
|
### 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.
|
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.
|
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
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
module.exports = {
|
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
5
global.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
SP_REACT: typeof React;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
globals.d.ts
vendored
5
globals.d.ts
vendored
@@ -1,5 +0,0 @@
|
|||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
SP_REACT: typeof React;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
59
package.json
59
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "decky-frontend-lib",
|
"name": "@decky/ui",
|
||||||
"version": "3.18.2",
|
"version": "4.8.3",
|
||||||
"description": "A library for building decky plugins",
|
"description": "A library for interacting with the Steam frontend in Decky plugins and elsewhere.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "shx rm -rf dist && tsc -b",
|
"build": "shx rm -rf dist && tsc -b",
|
||||||
"dev": "tsc -b -w",
|
"dev": "tsc -b -w",
|
||||||
"docs": "typedoc --tsconfig ./tsconfig.json src/**/*",
|
"docs": "typedoc --theme wiki-js --tsconfig ./tsconfig.json src/**/*",
|
||||||
"test": "echo 'No tests for now!'",
|
"test": "echo 'No tests for now!'",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"commit": "git-cz"
|
"commit": "git-cz"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"steam",
|
"steam",
|
||||||
"components"
|
"components"
|
||||||
],
|
],
|
||||||
"author": "Jonas Dellinger <jonas@dellinger.dev>",
|
"author": "SteamDeckHomebrew Team",
|
||||||
"license": "LGPL-2.1",
|
"license": "LGPL-2.1",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues"
|
"url": "https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues"
|
||||||
@@ -41,29 +41,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.0.2",
|
"@commitlint/cli": "^19.3.0",
|
||||||
"@commitlint/config-conventional": "^17.0.2",
|
"@commitlint/config-conventional": "^19.2.2",
|
||||||
"@commitlint/cz-commitlint": "^17.0.0",
|
"@commitlint/cz-commitlint": "^19.2.0",
|
||||||
"@semantic-release/changelog": "^6.0.1",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "^10.0.1",
|
||||||
"@types/jest": "^27.4.1",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/react": "16.14.0",
|
"@types/react": "18.3.3",
|
||||||
"@types/react-router": "5.1.18",
|
"@types/react-dom": "18.3.0",
|
||||||
"commitizen": "^4.2.4",
|
"@types/react-router": "5.1.20",
|
||||||
"husky": "^8.0.1",
|
"commitizen": "^4.3.0",
|
||||||
|
"husky": "^9.0.11",
|
||||||
"import-sort-style-module": "^6.0.0",
|
"import-sort-style-module": "^6.0.0",
|
||||||
"jest": "^27.5.1",
|
"jest": "^29.7.0",
|
||||||
"minimist": "^1.2.6",
|
"minimist": "^1.2.8",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^3.3.2",
|
||||||
"prettier-plugin-import-sort": "^0.0.7",
|
"prettier-plugin-import-sort": "^0.0.7",
|
||||||
"semantic-release": "^19.0.3",
|
"semantic-release": "^24.0.0",
|
||||||
"shx": "^0.3.4",
|
"shx": "^0.3.4",
|
||||||
"ts-jest": "^27.1.4",
|
"ts-jest": "^29.1.4",
|
||||||
"typedoc": "^0.23.15",
|
"typedoc": "^0.25.13",
|
||||||
"typedoc-plugin-markdown": "^3.13.6",
|
"typedoc-plugin-mdn-links": "^3.1.29",
|
||||||
"typedoc-plugin-mdn-links": "^2.0.0",
|
"typedoc-plugin-missing-exports": "^2.3.0",
|
||||||
"typedoc-plugin-missing-exports": "^1.0.0",
|
"typedoc-wikijs-theme": "^1.0.5",
|
||||||
"typescript": "^4.6.3"
|
"typescript": "^5.4.5"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"peerDependencyRules": {
|
"peerDependencyRules": {
|
||||||
@@ -71,6 +72,12 @@
|
|||||||
"react",
|
"react",
|
||||||
"react-dom"
|
"react-dom"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"updateConfig": {
|
||||||
|
"ignoreDependencies": [
|
||||||
|
"react",
|
||||||
|
"react-dom"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"importSort": {
|
"importSort": {
|
||||||
@@ -78,5 +85,9 @@
|
|||||||
"style": "module",
|
"style": "module",
|
||||||
"parser": "typescript"
|
"parser": "typescript"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://registry.npmjs.org/",
|
||||||
|
"tag": "latest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9233
pnpm-lock.yaml
generated
Executable file → Normal file
9233
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
40
src/class-mapper.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,15 @@ import { FC } from 'react';
|
|||||||
|
|
||||||
import { CommonUIModule } from '../webpack';
|
import { CommonUIModule } from '../webpack';
|
||||||
import { ItemProps } from './Item';
|
import { ItemProps } from './Item';
|
||||||
|
import { createPropListRegex } from '../utils';
|
||||||
|
|
||||||
export interface ButtonItemProps extends ItemProps {
|
export interface ButtonItemProps extends ItemProps {
|
||||||
onClick?(e: MouseEvent): void;
|
onClick?(e: MouseEvent): void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
const buttonItemRegex = createPropListRegex(["highlightOnFocus", "childrenContainerWidth"], false);
|
||||||
export const ButtonItem = Object.values(CommonUIModule).find(
|
export const ButtonItem = Object.values(CommonUIModule).find(
|
||||||
(mod: any) =>
|
(mod: any) =>
|
||||||
mod?.render?.toString()?.includes('"highlightOnFocus","childrenContainerWidth"') ||
|
(mod?.render?.toString && buttonItemRegex.test(mod.render.toString())) ||
|
||||||
mod?.render?.toString()?.includes('childrenContainerWidth:"min"'),
|
mod?.render?.toString?.().includes('childrenContainerWidth:"min"'),
|
||||||
) as FC<ButtonItemProps>;
|
) as FC<ButtonItemProps>;
|
||||||
@@ -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> {
|
export interface CarouselProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
@@ -20,9 +20,6 @@ export interface CarouselProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
scrollToAlignment?: 'center';
|
scrollToAlignment?: 'center';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Carousel = findModuleChild((m) => {
|
export const Carousel = findModuleExport((e: Export) => e.render?.toString().includes('setFocusedColumn:')) as FC<
|
||||||
if (typeof m !== 'object') return undefined;
|
CarouselProps & RefAttributes<HTMLDivElement>
|
||||||
for (let prop in m) {
|
>;
|
||||||
if (m[prop]?.render?.toString().includes('setFocusedColumn:')) return m[prop];
|
|
||||||
}
|
|
||||||
}) as VFC<CarouselProps & RefAttributes<HTMLDivElement>>;
|
|
||||||
14
src/components/ControlsList.ts
Normal file
14
src/components/ControlsList.ts
Normal 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'),
|
||||||
|
);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CSSProperties, FC, RefAttributes } from 'react';
|
import { CSSProperties, FC, ReactNode, RefAttributes } from 'react';
|
||||||
|
|
||||||
import { CommonUIModule } from '../webpack';
|
import { CommonUIModule } from '../webpack';
|
||||||
import { FooterLegendProps } from './FooterLegend';
|
import { FooterLegendProps } from './FooterLegend';
|
||||||
@@ -6,12 +6,13 @@ import { FooterLegendProps } from './FooterLegend';
|
|||||||
export interface DialogCommonProps extends RefAttributes<HTMLDivElement> {
|
export interface DialogCommonProps extends RefAttributes<HTMLDivElement> {
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DialogButtonProps extends DialogCommonProps, FooterLegendProps {
|
export interface DialogButtonProps extends DialogCommonProps, FooterLegendProps {
|
||||||
/**
|
/**
|
||||||
* Enables/disables the focus around the button.
|
* Enables/disables the focus around the button.
|
||||||
*
|
*
|
||||||
* @note
|
* @note
|
||||||
* Default value depends on context, so setting it to `false` will enable it.
|
* Default value depends on context, so setting it to `false` will enable it.
|
||||||
*/
|
*/
|
||||||
@@ -19,25 +20,25 @@ export interface DialogButtonProps extends DialogCommonProps, FooterLegendProps
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Disables the button - assigned `on*` methods will not be invoked if clicked.
|
* Disables the button - assigned `on*` methods will not be invoked if clicked.
|
||||||
*
|
*
|
||||||
* @note
|
* @note
|
||||||
* Depending on where it is, it might still get focus. In such case it can be
|
* Depending on where it is, it might still get focus. In such case it can be
|
||||||
* partially disabled separately.
|
* partially disabled separately.
|
||||||
*
|
*
|
||||||
* @see focusable.
|
* @see focusable.
|
||||||
*/
|
*/
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables/disables the navigation based focus on button - you won't be able to navigate to
|
* Enables/disables the navigation based focus on button - you won't be able to navigate to
|
||||||
* it via the gamepad or keyboard.
|
* it via the gamepad or keyboard.
|
||||||
*
|
*
|
||||||
* @note
|
* @note
|
||||||
* If set to `false`, it still can be clicked and **WILL** become focused until navigated away.
|
* 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.
|
* Depending on the context of where the button is, even a disabled button can focused.
|
||||||
*/
|
*/
|
||||||
focusable?: boolean;
|
focusable?: boolean;
|
||||||
|
|
||||||
onClick?(e: MouseEvent): void;
|
onClick?(e: MouseEvent): void;
|
||||||
onPointerDown?(e: PointerEvent): void;
|
onPointerDown?(e: PointerEvent): void;
|
||||||
onPointerUp?(e: PointerEvent): void;
|
onPointerUp?(e: PointerEvent): void;
|
||||||
@@ -51,13 +52,19 @@ export interface DialogButtonProps extends DialogCommonProps, FooterLegendProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CommonDialogDivs = Object.values(CommonUIModule).filter(
|
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(
|
const MappedDialogDivs = new Map(
|
||||||
Object.values(CommonDialogDivs).map((m: any) => {
|
Object.values(CommonDialogDivs).map((m: any) => {
|
||||||
const renderedDiv = m.render({});
|
try {
|
||||||
// Take only the first class name segment as it identifies the element we want
|
const renderedDiv = m.render({});
|
||||||
return [renderedDiv.props.className.split(' ')[0], m];
|
// 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];
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -71,14 +78,11 @@ export const DialogControlsSection = MappedDialogDivs.get('DialogControlsSection
|
|||||||
export const DialogControlsSectionHeader = MappedDialogDivs.get('DialogControlsSectionHeader') as FC<DialogCommonProps>;
|
export const DialogControlsSectionHeader = MappedDialogDivs.get('DialogControlsSectionHeader') as FC<DialogCommonProps>;
|
||||||
|
|
||||||
export const DialogButtonPrimary = Object.values(CommonUIModule).find(
|
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>;
|
) as FC<DialogButtonProps>;
|
||||||
|
|
||||||
export const DialogButtonSecondary = Object.values(CommonUIModule).find(
|
export const DialogButtonSecondary = Object.values(CommonUIModule).find(
|
||||||
(mod: any) =>
|
(mod: any) => mod?.render?.toString?.()?.includes('"DialogButton","_DialogLayout","Secondary"')
|
||||||
mod?.render?.toString()?.includes('Object.assign({type:"button"') &&
|
|
||||||
mod?.render?.toString()?.includes('DialogButton') &&
|
|
||||||
mod?.render?.toString()?.includes('Secondary'),
|
|
||||||
) as FC<DialogButtonProps>;
|
) as FC<DialogButtonProps>;
|
||||||
|
|
||||||
// This is the "main" button. The Primary can act as a submit button,
|
// This is the "main" button. The Primary can act as a submit button,
|
||||||
33
src/components/DialogCheckbox.ts
Normal file
33
src/components/DialogCheckbox.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { findModuleExport } 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not access KeyDown, SetChecked, Toggle here as they are getters and accessing them outside of a render breaks them globally
|
||||||
|
export const DialogCheckbox = findModuleExport(e =>
|
||||||
|
e?.prototype &&
|
||||||
|
typeof e?.prototype == "object" &&
|
||||||
|
"GetPanelElementProps" in e?.prototype &&
|
||||||
|
"SetChecked" in e?.prototype &&
|
||||||
|
"Toggle" in e?.prototype &&
|
||||||
|
// beta || stable as of oct 2 2024
|
||||||
|
(e?.prototype?.render?.toString?.().includes('="DialogCheckbox"') || (
|
||||||
|
e.contextType &&
|
||||||
|
e.prototype?.render?.toString?.().includes('fallback:')
|
||||||
|
))
|
||||||
|
) as FC<DialogCheckboxProps>;
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ReactNode, VFC } from 'react';
|
import { ReactNode, FC } from 'react';
|
||||||
|
|
||||||
import { CommonUIModule } from '../webpack';
|
import { CommonUIModule } from '../webpack';
|
||||||
import { ItemProps } from './Item';
|
import { ItemProps } from './Item';
|
||||||
|
import { createPropListRegex } from '../utils';
|
||||||
|
|
||||||
export interface SingleDropdownOption {
|
export interface SingleDropdownOption {
|
||||||
data: any;
|
data: any;
|
||||||
@@ -19,6 +20,11 @@ export interface MultiDropdownOption {
|
|||||||
|
|
||||||
export type DropdownOption = SingleDropdownOption | MultiDropdownOption;
|
export type DropdownOption = SingleDropdownOption | MultiDropdownOption;
|
||||||
|
|
||||||
|
export interface DropdownMenuPositionOptions {
|
||||||
|
[_: string]: unknown
|
||||||
|
bMatchWidth?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface DropdownProps {
|
export interface DropdownProps {
|
||||||
rgOptions: DropdownOption[];
|
rgOptions: DropdownOption[];
|
||||||
selectedOption: any;
|
selectedOption: any;
|
||||||
@@ -26,7 +32,7 @@ export interface DropdownProps {
|
|||||||
onMenuWillOpen?(showMenu: () => void): void;
|
onMenuWillOpen?(showMenu: () => void): void;
|
||||||
onMenuOpened?(): void;
|
onMenuOpened?(): void;
|
||||||
onChange?(data: SingleDropdownOption): void;
|
onChange?(data: SingleDropdownOption): void;
|
||||||
contextMenuPositionOptions?: any;
|
contextMenuPositionOptions?: DropdownMenuPositionOptions;
|
||||||
menuLabel?: string;
|
menuLabel?: string;
|
||||||
strDefaultLabel?: string;
|
strDefaultLabel?: string;
|
||||||
renderButtonValue?(element: ReactNode): ReactNode;
|
renderButtonValue?(element: ReactNode): ReactNode;
|
||||||
@@ -35,10 +41,11 @@ export interface DropdownProps {
|
|||||||
|
|
||||||
export const Dropdown = Object.values(CommonUIModule).find(
|
export const Dropdown = Object.values(CommonUIModule).find(
|
||||||
(mod: any) => mod?.prototype?.SetSelectedOption && mod?.prototype?.BuildMenu,
|
(mod: any) => mod?.prototype?.SetSelectedOption && mod?.prototype?.BuildMenu,
|
||||||
) as VFC<DropdownProps>;
|
) as FC<DropdownProps>;
|
||||||
|
|
||||||
export interface DropdownItemProps extends DropdownProps, ItemProps {}
|
export interface DropdownItemProps extends DropdownProps, ItemProps {}
|
||||||
|
|
||||||
|
const dropdownItemRegex = createPropListRegex(["dropDownControlRef", "description"], false);
|
||||||
export const DropdownItem = Object.values(CommonUIModule).find((mod: any) =>
|
export const DropdownItem = Object.values(CommonUIModule).find((mod: any) =>
|
||||||
mod?.toString()?.includes('"dropDownControlRef","description"'),
|
mod?.toString && dropdownItemRegex.test(mod.toString()),
|
||||||
) as VFC<DropdownItemProps>;
|
) as FC<DropdownItemProps>;
|
||||||
6
src/components/ErrorBoundary.ts
Normal file
6
src/components/ErrorBoundary.ts
Normal 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
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { FC, ReactNode, RefAttributes } from 'react';
|
import { FC, ReactNode, RefAttributes } from 'react';
|
||||||
|
|
||||||
import { findModuleChild } from '../webpack';
|
import { Export, findModuleExport } from '../webpack';
|
||||||
import { FooterLegendProps } from './FooterLegend';
|
import { FooterLegendProps } from './FooterLegend';
|
||||||
|
|
||||||
export interface FieldProps extends FooterLegendProps {
|
export interface FieldProps extends FooterLegendProps {
|
||||||
|
children?: ReactNode;
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
bottomSeparator?: 'standard' | 'thick' | 'none';
|
bottomSeparator?: 'standard' | 'thick' | 'none';
|
||||||
description?: ReactNode;
|
description?: ReactNode;
|
||||||
@@ -23,9 +24,6 @@ export interface FieldProps extends FooterLegendProps {
|
|||||||
onClick?: (e: CustomEvent | MouseEvent) => void;
|
onClick?: (e: CustomEvent | MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Field = findModuleChild((m) => {
|
export const Field = findModuleExport((e: Export) => e?.render?.toString().includes('"shift-children-below"')) as FC<
|
||||||
if (typeof m !== 'object') return undefined;
|
FieldProps & RefAttributes<HTMLDivElement>
|
||||||
for (let prop in m) {
|
>;
|
||||||
if (m[prop]?.render?.toString().includes('"shift-children-below"')) return m[prop];
|
|
||||||
}
|
|
||||||
}) as FC<FieldProps & RefAttributes<HTMLDivElement>>;
|
|
||||||
15
src/components/FocusRing.ts
Normal file
15
src/components/FocusRing.ts
Normal 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>;
|
||||||
21
src/components/Focusable.ts
Normal file
21
src/components/Focusable.ts
Normal 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>>;
|
||||||
117
src/components/FooterLegend.ts
Normal file
117
src/components/FooterLegend.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export enum GamepadButton {
|
||||||
|
INVALID,
|
||||||
|
OK,
|
||||||
|
CANCEL,
|
||||||
|
SECONDARY,
|
||||||
|
OPTIONS,
|
||||||
|
BUMPER_LEFT,
|
||||||
|
BUMPER_RIGHT,
|
||||||
|
TRIGGER_LEFT,
|
||||||
|
TRIGGER_RIGHT,
|
||||||
|
DIR_UP,
|
||||||
|
DIR_DOWN,
|
||||||
|
DIR_LEFT,
|
||||||
|
DIR_RIGHT,
|
||||||
|
SELECT,
|
||||||
|
START,
|
||||||
|
LSTICK_CLICK,
|
||||||
|
RSTICK_CLICK,
|
||||||
|
LSTICK_TOUCH,
|
||||||
|
RSTICK_TOUCH,
|
||||||
|
LPAD_TOUCH,
|
||||||
|
LPAD_CLICK,
|
||||||
|
RPAD_TOUCH,
|
||||||
|
RPAD_CLICK,
|
||||||
|
REAR_LEFT_UPPER,
|
||||||
|
REAR_LEFT_LOWER,
|
||||||
|
REAR_RIGHT_UPPER,
|
||||||
|
REAR_RIGHT_LOWER,
|
||||||
|
STEAM_GUIDE,
|
||||||
|
STEAM_QUICK_MENU,
|
||||||
|
}
|
||||||
|
export enum NavEntryPositionPreferences {
|
||||||
|
/**
|
||||||
|
* Always give focus to the first child element.
|
||||||
|
*/
|
||||||
|
FIRST,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always give focus to the last child element.
|
||||||
|
*/
|
||||||
|
LAST,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Give focus to the child element that would maintain the flow in the X axis.
|
||||||
|
*
|
||||||
|
* Imagine you have a calculator window with 9 standard buttons.
|
||||||
|
* You have 3 rows of buttons, with 3 buttons per row.
|
||||||
|
* If you select button with number 8 and navigate down, the buttons
|
||||||
|
* will be navigated in the following order 8->5->3.
|
||||||
|
* The flow is maintained for the X axis while you're navigating the Y axis.
|
||||||
|
*/
|
||||||
|
MAINTAIN_X,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Give focus to the child element that would maintain the flow in the Y axis.
|
||||||
|
*
|
||||||
|
* Imagine you have a calculator window with 9 standard buttons.
|
||||||
|
* You have 3 columns of buttons, with 3 buttons per column.
|
||||||
|
* If you select button with number 4 and navigate right, the buttons
|
||||||
|
* will be navigated in the following order 4->5->6.
|
||||||
|
* The flow is maintained for the Y axis while you're navigating the X axis.
|
||||||
|
*/
|
||||||
|
MAINTAIN_Y,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Give focus to the first child element with `preferredFocus == true` prop.
|
||||||
|
*/
|
||||||
|
PREFERRED_CHILD,
|
||||||
|
}
|
||||||
|
export interface GamepadEventDetail {
|
||||||
|
button: number;
|
||||||
|
is_repeat?: boolean;
|
||||||
|
source: number;
|
||||||
|
}
|
||||||
|
export declare type ActionDescriptionMap = {
|
||||||
|
[key in GamepadButton]?: ReactNode;
|
||||||
|
};
|
||||||
|
export declare type GamepadEvent = CustomEvent<GamepadEventDetail>;
|
||||||
|
export interface FooterLegendProps {
|
||||||
|
/**
|
||||||
|
* Navigation entry strategy to be used when gaining focus during navigation.
|
||||||
|
*
|
||||||
|
* This is meant to be used on a parent container that has children. Once the
|
||||||
|
* container (e.g. Focusable) is navigated to and has children in it, the children
|
||||||
|
* is then navigated to (focused) using the provided strategy.
|
||||||
|
*
|
||||||
|
* If no strategy is provided, it seems that the `NavEntryPositionPreferences.FIRST`
|
||||||
|
* is used initialy, but for the next time the parent remembers previously focused
|
||||||
|
* child and focused back on it instead.
|
||||||
|
*/
|
||||||
|
navEntryPreferPosition?: NavEntryPositionPreferences;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the element as the preferred child (to be focused) whenever the parent uses the
|
||||||
|
* `NavEntryPositionPreferences.PREFERRED_CHILD` navigation strategy.
|
||||||
|
*/
|
||||||
|
preferredFocus?: boolean;
|
||||||
|
|
||||||
|
actionDescriptionMap?: ActionDescriptionMap;
|
||||||
|
onOKActionDescription?: ReactNode;
|
||||||
|
onCancelActionDescription?: ReactNode;
|
||||||
|
onSecondaryActionDescription?: ReactNode;
|
||||||
|
onOptionsActionDescription?: ReactNode;
|
||||||
|
onMenuActionDescription?: ReactNode;
|
||||||
|
onButtonDown?: (evt: GamepadEvent) => void;
|
||||||
|
onButtonUp?: (evt: GamepadEvent) => void;
|
||||||
|
onOKButton?: (evt: GamepadEvent) => void;
|
||||||
|
onCancelButton?: (evt: GamepadEvent) => void;
|
||||||
|
onSecondaryButton?: (evt: GamepadEvent) => void;
|
||||||
|
onOptionsButton?: (evt: GamepadEvent) => void;
|
||||||
|
onGamepadDirection?: (evt: GamepadEvent) => void;
|
||||||
|
onGamepadFocus?: (evt: GamepadEvent) => void;
|
||||||
|
onGamepadBlur?: (evt: GamepadEvent) => void;
|
||||||
|
onMenuButton?: (evt: GamepadEvent) => void;
|
||||||
|
}
|
||||||
@@ -3,9 +3,11 @@ import { ReactNode } from 'react';
|
|||||||
export interface ItemProps {
|
export interface ItemProps {
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
description?: ReactNode;
|
description?: ReactNode;
|
||||||
|
children?: ReactNode;
|
||||||
layout?: 'below' | 'inline';
|
layout?: 'below' | 'inline';
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
bottomSeparator?: 'standard' | 'thick' | 'none';
|
bottomSeparator?: 'standard' | 'thick' | 'none';
|
||||||
indentLevel?: number;
|
indentLevel?: number;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
|
highlightOnFocus?: boolean;
|
||||||
}
|
}
|
||||||
20
src/components/Marquee.ts
Normal file
20
src/components/Marquee.ts
Normal 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
53
src/components/Menu.ts
Executable 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)
|
||||||
|
*/
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { FC, ReactNode } from 'react';
|
import { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
import { findSP } from '../utils';
|
import { findSP } from '../utils';
|
||||||
import { findModule, findModuleChild } from '../webpack';
|
import { Export, findModule, findModuleDetailsByExport, findModuleExport } from '../webpack';
|
||||||
|
|
||||||
// All of the popout options + strTitle are related. Proper usage is not yet known...
|
// All of the popout options + strTitle are related. Proper usage is not yet known...
|
||||||
export interface ShowModalProps {
|
export interface ShowModalProps {
|
||||||
browserContext?: unknown; // This is another Deck Object that is yet to be found
|
browserContext?: unknown;
|
||||||
bForcePopOut?: boolean;
|
bForcePopOut?: boolean;
|
||||||
bHideActionIcons?: boolean;
|
bHideActionIcons?: boolean;
|
||||||
bHideMainWindowForPopouts?: boolean;
|
bHideMainWindowForPopouts?: boolean;
|
||||||
@@ -29,39 +29,18 @@ export interface ShowModalResult {
|
|||||||
Update: (modal: ReactNode) => void;
|
Update: (modal: ReactNode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showModalRaw:
|
const showModalRaw: (
|
||||||
| ((
|
modal: ReactNode,
|
||||||
modal: ReactNode,
|
parent?: EventTarget,
|
||||||
parent?: EventTarget,
|
title?: string,
|
||||||
title?: string,
|
props?: ShowModalProps,
|
||||||
props?: ShowModalProps,
|
unknown1?: unknown,
|
||||||
unknown1?: unknown,
|
hideActions?: { bHideActions?: boolean },
|
||||||
hideActions?: { bHideActions?: boolean },
|
modalManager?: unknown,
|
||||||
modalManager?: unknown,
|
) => ShowModalResult = findModuleExport(
|
||||||
) => Promise<ShowModalResult>)
|
(e: Export) =>
|
||||||
| void = findModuleChild((m) => {
|
typeof e === 'function' && e.toString().includes('props.bDisableBackgroundDismiss') && !e?.prototype?.Cancel,
|
||||||
if (typeof m !== 'object') return undefined;
|
);
|
||||||
for (let prop in m) {
|
|
||||||
if (
|
|
||||||
typeof m[prop] === 'function' &&
|
|
||||||
m[prop].toString().includes('props.bDisableBackgroundDismiss') &&
|
|
||||||
!m[prop]?.prototype?.Cancel
|
|
||||||
) {
|
|
||||||
return m[prop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const oldShowModalRaw:
|
|
||||||
| ((modal: ReactNode, parent?: EventTarget, props?: ShowModalProps) => Promise<ShowModalResult>)
|
|
||||||
| void = 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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const showModal = (
|
export const showModal = (
|
||||||
modal: ReactNode,
|
modal: ReactNode,
|
||||||
@@ -70,16 +49,10 @@ export const showModal = (
|
|||||||
strTitle: 'Decky Dialog',
|
strTitle: 'Decky Dialog',
|
||||||
bHideMainWindowForPopouts: false,
|
bHideMainWindowForPopouts: false,
|
||||||
},
|
},
|
||||||
): Promise<ShowModalResult> => {
|
): ShowModalResult => {
|
||||||
if (showModalRaw) {
|
return showModalRaw(modal, parent || findSP() || window, props.strTitle, props, undefined, {
|
||||||
return showModalRaw(modal, parent || findSP(), props.strTitle, props, undefined, {
|
bHideActions: props.bHideActionIcons,
|
||||||
bHideActions: props.bHideActionIcons,
|
});
|
||||||
});
|
|
||||||
} else if (oldShowModalRaw) {
|
|
||||||
return oldShowModalRaw(modal, parent || findSP(), props);
|
|
||||||
} else {
|
|
||||||
throw new Error('[DFL:Modals]: Cannot find showModal function');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ModalRootProps {
|
export interface ModalRootProps {
|
||||||
@@ -109,35 +82,36 @@ export interface ConfirmModalProps extends ModalRootProps {
|
|||||||
bMiddleDisabled?: boolean;
|
bMiddleDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConfirmModal = findModuleChild((m) => {
|
export const ConfirmModal = findModuleExport(
|
||||||
if (typeof m !== 'object') return undefined;
|
(e: Export) => !e?.prototype?.OK && e?.prototype?.Cancel && e?.prototype?.render,
|
||||||
for (let prop in m) {
|
) as FC<ConfirmModalProps>;
|
||||||
if (!m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) {
|
|
||||||
return m[prop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) as FC<ConfirmModalProps>;
|
|
||||||
|
|
||||||
// new
|
export const ModalRoot = Object.values(
|
||||||
export const ModalRoot = (Object.values(
|
|
||||||
findModule((m: any) => {
|
findModule((m: any) => {
|
||||||
if (typeof m !== 'object') return false;
|
if (typeof m !== 'object') return false;
|
||||||
|
|
||||||
for (let prop in m) {
|
for (let prop in m) {
|
||||||
if (m[prop]?.toString()?.includes('"ModalManager","DialogWrapper"')) {
|
if (m[prop]?.m_mapModalManager && Object.values(m)?.find((x: any) => x?.type)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}) || {},
|
}) || {},
|
||||||
)?.find((x: any) => x?.type?.toString()?.includes('((function(){')) ||
|
)?.find((x: any) => x?.type?.toString?.()?.includes('((function(){')) as FC<ModalRootProps>;
|
||||||
// old
|
|
||||||
findModuleChild((m) => {
|
interface SimpleModalProps {
|
||||||
if (typeof m !== 'object') return undefined;
|
active?: boolean;
|
||||||
for (let prop in m) {
|
children: ReactNode;
|
||||||
if (m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) {
|
}
|
||||||
return m[prop];
|
|
||||||
}
|
const [ModalModule, _ModalPosition] = findModuleDetailsByExport((e: Export) => e?.toString().includes('.ModalPosition'), 5)
|
||||||
}
|
|
||||||
})) as FC<ModalRootProps>;
|
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
26
src/components/Panel.ts
Normal 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>;
|
||||||
37
src/components/ProgressBar.ts
Normal file
37
src/components/ProgressBar.ts
Normal 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
15
src/components/Scroll.ts
Normal 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()),[])'),
|
||||||
|
);
|
||||||
30
src/components/SidebarNavigation.ts
Normal file
30
src/components/SidebarNavigation.ts
Normal 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>;
|
||||||
@@ -25,8 +25,10 @@ export interface SliderFieldProps extends ItemProps {
|
|||||||
valueSuffix?: string;
|
valueSuffix?: string;
|
||||||
minimumDpadGranularity?: number;
|
minimumDpadGranularity?: number;
|
||||||
onChange?(value: number): void;
|
onChange?(value: number): void;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SliderField = Object.values(CommonUIModule).find((mod: any) =>
|
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>;
|
) as FC<SliderFieldProps>;
|
||||||
8
src/components/Spinner.ts
Executable file
8
src/components/Spinner.ts
Executable 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
12
src/components/SteamSpinner.ts
Executable 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
70
src/components/Tabs.tsx
Normal 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>;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeEventHandler, HTMLAttributes, ReactNode, VFC } from 'react';
|
import { ChangeEventHandler, HTMLAttributes, ReactNode, FC } from 'react';
|
||||||
|
|
||||||
import { CommonUIModule, Module } from '../webpack';
|
import { CommonUIModule, Module } from '../webpack';
|
||||||
|
|
||||||
@@ -25,4 +25,4 @@ export interface TextFieldProps extends HTMLAttributes<HTMLInputElement> {
|
|||||||
|
|
||||||
export const TextField = Object.values(CommonUIModule).find(
|
export const TextField = Object.values(CommonUIModule).find(
|
||||||
(mod: Module) => mod?.validateUrl && mod?.validateEmail,
|
(mod: Module) => mod?.validateUrl && mod?.validateEmail,
|
||||||
) as VFC<TextFieldProps>;
|
) as FC<TextFieldProps>;
|
||||||
@@ -10,5 +10,5 @@ export interface ToggleProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Toggle = Object.values(CommonUIModule).find((mod: any) =>
|
export const Toggle = Object.values(CommonUIModule).find((mod: any) =>
|
||||||
mod?.render?.toString()?.includes('.ToggleOff)'),
|
mod?.render?.toString?.()?.includes('.ToggleOff)'),
|
||||||
) as FC<ToggleProps>;
|
) as FC<ToggleProps>;
|
||||||
@@ -4,11 +4,13 @@ import { CommonUIModule } from '../webpack';
|
|||||||
import { ItemProps } from './Item';
|
import { ItemProps } from './Item';
|
||||||
|
|
||||||
export interface ToggleFieldProps extends ItemProps {
|
export interface ToggleFieldProps extends ItemProps {
|
||||||
|
highlightOnFocus?: boolean;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onChange?(checked: boolean): void;
|
onChange?(checked: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToggleField = Object.values(CommonUIModule).find((mod: any) =>
|
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>;
|
) as FC<ToggleFieldProps>;
|
||||||
@@ -5,6 +5,7 @@ export * from './ControlsList';
|
|||||||
export * from './Dialog';
|
export * from './Dialog';
|
||||||
export * from './DialogCheckbox';
|
export * from './DialogCheckbox';
|
||||||
export * from './Dropdown';
|
export * from './Dropdown';
|
||||||
|
export * from './ErrorBoundary';
|
||||||
export * from './Field';
|
export * from './Field';
|
||||||
export * from './Focusable';
|
export * from './Focusable';
|
||||||
export * from './FocusRing';
|
export * from './FocusRing';
|
||||||
@@ -14,20 +15,12 @@ export * from './Menu';
|
|||||||
export * from './Modal';
|
export * from './Modal';
|
||||||
export * from './Panel';
|
export * from './Panel';
|
||||||
export * from './ProgressBar';
|
export * from './ProgressBar';
|
||||||
export * from './Router';
|
|
||||||
export * from './SidebarNavigation';
|
export * from './SidebarNavigation';
|
||||||
export * from './SliderField';
|
export * from './SliderField';
|
||||||
export * from './Spinner';
|
export * from './Spinner';
|
||||||
export * from './static-classes';
|
|
||||||
export * from './SteamSpinner';
|
export * from './SteamSpinner';
|
||||||
export * from './Tabs';
|
export * from './Tabs';
|
||||||
export * from './TextField';
|
export * from './TextField';
|
||||||
export * from './Toggle';
|
export * from './Toggle';
|
||||||
export * from './ToggleField';
|
export * from './ToggleField';
|
||||||
export * from './SteamClient';
|
export * from './Scroll';
|
||||||
|
|
||||||
import {SteamClient} from './SteamClient'
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var SteamClient: SteamClient;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CSSProperties, FC, useState } from 'react';
|
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 {
|
interface ColorPickerModalProps {
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
|
|||||||
183
src/custom-components/ReorderableList.tsx
Normal file
183
src/custom-components/ReorderableList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { FC, ImgHTMLAttributes, useState } from 'react';
|
import { FC, ImgHTMLAttributes, useState } from 'react';
|
||||||
|
|
||||||
import { Spinner } from '../deck-components';
|
import { Spinner } from '../components';
|
||||||
|
|
||||||
interface SuspensefulImageProps extends ImgHTMLAttributes<HTMLImageElement> {
|
interface SuspensefulImageProps extends ImgHTMLAttributes<HTMLImageElement> {
|
||||||
suspenseWidth?: string | number;
|
suspenseWidth?: string | number;
|
||||||
@@ -13,6 +13,8 @@ export const SuspensefulImage: FC<SuspensefulImageProps> = (props) => {
|
|||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.src = props.src || '';
|
img.src = props.src || '';
|
||||||
img.addEventListener('load', () => {
|
img.addEventListener('load', () => {
|
||||||
@@ -21,7 +23,7 @@ export const SuspensefulImage: FC<SuspensefulImageProps> = (props) => {
|
|||||||
img.addEventListener('error', () => {
|
img.addEventListener('error', () => {
|
||||||
setError(true);
|
setError(true);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [props.src]);
|
||||||
|
|
||||||
return loading ? (
|
return loading ? (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './SuspensefulImage';
|
export * from './SuspensefulImage';
|
||||||
export * from './ColorPickerModal';
|
export * from './ColorPickerModal';
|
||||||
|
export * from './ReorderableList';
|
||||||
|
|||||||
65
src/custom-hooks/useQuickAccessVisible.ts
Normal file
65
src/custom-hooks/useQuickAccessVisible.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var FocusNavController: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQuickAccessWindow(): Window | null {
|
|
||||||
try {
|
|
||||||
const context = FocusNavController?.m_ActiveContext || FocusNavController?.m_LastActiveContext;
|
|
||||||
const navTrees = context?.m_rgGamepadNavigationTrees || FocusNavController?.m_rgGamepadNavigationTrees;
|
|
||||||
return navTrees?.find((tree: any) => tree?.id === "QuickAccess-NA")?.m_Root?.m_element?.ownerDocument.defaultView ?? null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
const [isVisible, setIsVisible] = useState(getQuickAccessWindow()?.document.hasFocus() ?? true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const quickAccessWindow = getQuickAccessWindow();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { findModuleChild } from '../webpack';
|
|
||||||
import { FC } from 'react';
|
|
||||||
|
|
||||||
export interface ControlsListProps {
|
|
||||||
alignItems?: 'left' | 'right' | 'center';
|
|
||||||
spacing?: 'standard' | 'extra';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ControlsList: FC<ControlsListProps> = findModuleChild((m) => {
|
|
||||||
if (typeof m !== 'object') return;
|
|
||||||
for (const prop in m) {
|
|
||||||
if (m[prop]?.toString && m[prop].toString().includes('().ControlsListChild') && m[prop].toString().includes('().ControlsListOuterPanel')) {
|
|
||||||
return m[prop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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.contextType &&
|
|
||||||
m.prototype?.render.toString().includes('fallback:') &&
|
|
||||||
m?.prototype?.SetChecked &&
|
|
||||||
m?.prototype?.Toggle &&
|
|
||||||
m?.prototype?.GetPanelElementProps
|
|
||||||
) as FC<DialogCheckboxProps>;
|
|
||||||
@@ -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>;
|
|
||||||
@@ -1,22 +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;
|
|
||||||
noFocusRing?: boolean;
|
|
||||||
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>>;
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
export enum GamepadButton {
|
|
||||||
INVALID,
|
|
||||||
OK,
|
|
||||||
CANCEL,
|
|
||||||
SECONDARY,
|
|
||||||
OPTIONS,
|
|
||||||
BUMPER_LEFT,
|
|
||||||
BUMPER_RIGHT,
|
|
||||||
TRIGGER_LEFT,
|
|
||||||
TRIGGER_RIGHT,
|
|
||||||
DIR_UP,
|
|
||||||
DIR_DOWN,
|
|
||||||
DIR_LEFT,
|
|
||||||
DIR_RIGHT,
|
|
||||||
SELECT,
|
|
||||||
START,
|
|
||||||
LSTICK_CLICK,
|
|
||||||
RSTICK_CLICK,
|
|
||||||
LSTICK_TOUCH,
|
|
||||||
RSTICK_TOUCH,
|
|
||||||
LPAD_TOUCH,
|
|
||||||
LPAD_CLICK,
|
|
||||||
RPAD_TOUCH,
|
|
||||||
RPAD_CLICK,
|
|
||||||
REAR_LEFT_UPPER,
|
|
||||||
REAR_LEFT_LOWER,
|
|
||||||
REAR_RIGHT_UPPER,
|
|
||||||
REAR_RIGHT_LOWER,
|
|
||||||
STEAM_GUIDE,
|
|
||||||
STEAM_QUICK_MENU,
|
|
||||||
}
|
|
||||||
export declare enum NavEntryPositionPreferences {
|
|
||||||
FIRST,
|
|
||||||
LAST,
|
|
||||||
MAINTAIN_X,
|
|
||||||
MAINTAIN_Y,
|
|
||||||
PREFERRED_CHILD
|
|
||||||
}
|
|
||||||
export interface GamepadEventDetail {
|
|
||||||
button: number;
|
|
||||||
is_repeat?: boolean;
|
|
||||||
source: number;
|
|
||||||
}
|
|
||||||
export declare type ActionDescriptionMap = {
|
|
||||||
[key in GamepadButton]?: ReactNode
|
|
||||||
}
|
|
||||||
export declare type GamepadEvent = CustomEvent<GamepadEventDetail>;
|
|
||||||
export interface FooterLegendProps {
|
|
||||||
actionDescriptionMap?: ActionDescriptionMap;
|
|
||||||
onOKActionDescription?: ReactNode;
|
|
||||||
onCancelActionDescription?: ReactNode;
|
|
||||||
onSecondaryActionDescription?: ReactNode;
|
|
||||||
onOptionsActionDescription?: ReactNode;
|
|
||||||
onMenuActionDescription?: ReactNode;
|
|
||||||
onButtonDown?: (evt: GamepadEvent) => void;
|
|
||||||
onButtonUp?: (evt: GamepadEvent) => void;
|
|
||||||
onOKButton?: (evt: GamepadEvent) => void;
|
|
||||||
onCancelButton?: (evt: GamepadEvent) => void;
|
|
||||||
onSecondaryButton?: (evt: GamepadEvent) => void;
|
|
||||||
onOptionsButton?: (evt: GamepadEvent) => void;
|
|
||||||
onGamepadDirection?: (evt: GamepadEvent) => void;
|
|
||||||
onGamepadFocus?: (evt: GamepadEvent) => void;
|
|
||||||
onGamepadBlur?: (evt: GamepadEvent) => void;
|
|
||||||
onMenuButton?: (evt: GamepadEvent) => void;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { CSSProperties, FC } from 'react';
|
|
||||||
|
|
||||||
import { findModuleChild } 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> = findModuleChild((m) => {
|
|
||||||
if (typeof m !== 'object') return;
|
|
||||||
for (const prop in m) {
|
|
||||||
if (m[prop]?.toString && m[prop].toString().includes('.Marquee') && m[prop].toString().includes('--fade-length')) {
|
|
||||||
return m[prop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { FC, ReactNode } from 'react';
|
|
||||||
|
|
||||||
import { fakeRenderComponent } from '../utils';
|
|
||||||
import { findModuleChild } from '../webpack';
|
|
||||||
import { FooterLegendProps } from './FooterLegend';
|
|
||||||
|
|
||||||
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 extends FooterLegendProps {
|
|
||||||
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]?.toString()?.includes('bInGamepadUI:') &&
|
|
||||||
fakeRenderComponent(() => m[prop]())?.type?.prototype?.RenderSubMenu) ||
|
|
||||||
(m[prop]?.prototype?.RenderSubMenu && m[prop]?.prototype?.ShowSubMenu)
|
|
||||||
) {
|
|
||||||
return m[prop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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> = findModuleChild((m) => {
|
|
||||||
if (typeof m !== 'object') return undefined;
|
|
||||||
|
|
||||||
for (let prop in m) {
|
|
||||||
if (
|
|
||||||
m[prop]?.render?.toString()?.includes('bPlayAudio:') ||
|
|
||||||
(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)
|
|
||||||
*/
|
|
||||||
@@ -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;
|
|
||||||
@@ -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>;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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>;
|
|
||||||
@@ -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>>;
|
|
||||||
@@ -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>>;
|
|
||||||
@@ -1,134 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tabsComponent: any;
|
|
||||||
|
|
||||||
const getTabs = async () => {
|
|
||||||
if (tabsComponent) return tabsComponent;
|
|
||||||
// @ts-ignore
|
|
||||||
while (!window?.DeckyPluginLoader?.routerHook?.routes) {
|
|
||||||
console.debug('[DFL:Tabs]: Waiting for Decky router...');
|
|
||||||
await sleep(500);
|
|
||||||
}
|
|
||||||
return (tabsComponent = fakeRenderComponent(
|
|
||||||
() => {
|
|
||||||
return findInReactTree(
|
|
||||||
findInReactTree(
|
|
||||||
// @ts-ignore
|
|
||||||
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(){'));
|
|
||||||
} 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>);
|
|
||||||
@@ -1,478 +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
|
|
||||||
>;
|
|
||||||
|
|
||||||
type AppDetailsHeaderClasses = Record<
|
|
||||||
| 'AddBoxSizer'
|
|
||||||
| 'Background'
|
|
||||||
| 'Bottom'
|
|
||||||
| 'BottomCenter'
|
|
||||||
| 'BottomLeft'
|
|
||||||
| 'BottomRight'
|
|
||||||
| 'BoxSizer'
|
|
||||||
| 'BoxSizerButtonContainer'
|
|
||||||
| 'BoxSizerContainer'
|
|
||||||
| 'BoxSizerDelete'
|
|
||||||
| 'BoxSizerDragBox'
|
|
||||||
| 'BoxSizerEdge'
|
|
||||||
| 'BoxSizerGridBox'
|
|
||||||
| 'BoxSizerInfo'
|
|
||||||
| 'BoxSizerSettings'
|
|
||||||
| 'BoxSizerValidRegion'
|
|
||||||
| 'CenterCenter'
|
|
||||||
| 'DialogButton'
|
|
||||||
| 'EdgeDown'
|
|
||||||
| 'FallbackArt'
|
|
||||||
| 'Features'
|
|
||||||
| 'FullscreenEnterActive'
|
|
||||||
| 'FullscreenEnterDone'
|
|
||||||
| 'FullscreenEnterStart'
|
|
||||||
| 'FullscreenExitActive'
|
|
||||||
| 'FullscreenExitDone'
|
|
||||||
| 'FullscreenExitStart'
|
|
||||||
| 'HeaderBackgroundImage'
|
|
||||||
| 'ImgBlur'
|
|
||||||
| 'ImgBlurBackdrop'
|
|
||||||
| 'ImgContainer'
|
|
||||||
| 'ImgSrc'
|
|
||||||
| 'Left'
|
|
||||||
| 'Loaded'
|
|
||||||
| 'Middle'
|
|
||||||
| 'NoArt'
|
|
||||||
| 'PinBox'
|
|
||||||
| 'Right'
|
|
||||||
| 'SVGTitle'
|
|
||||||
| 'SaveBoxSizer'
|
|
||||||
| 'TextNameSpace'
|
|
||||||
| 'TitleImageContainer'
|
|
||||||
| 'TitleLogo'
|
|
||||||
| 'TitleSection'
|
|
||||||
| 'Top'
|
|
||||||
| 'TopCapsule'
|
|
||||||
| 'TopGradient'
|
|
||||||
| 'TopLeft'
|
|
||||||
| 'TopRight'
|
|
||||||
| 'UpperCenter'
|
|
||||||
| 'UpperLeft'
|
|
||||||
| 'duration-app-launch',
|
|
||||||
string
|
|
||||||
>;
|
|
||||||
|
|
||||||
type AppDetailsClasses = Record<
|
|
||||||
| 'BreakNarrow'
|
|
||||||
| 'BreakShort'
|
|
||||||
| 'BreakTall'
|
|
||||||
| 'BreakUltraWide'
|
|
||||||
| 'BreakWide'
|
|
||||||
| 'Container'
|
|
||||||
| 'GamepadUIBreakNarrow'
|
|
||||||
| 'GamepadUIBreakShort'
|
|
||||||
| 'GamepadUIBreakWide'
|
|
||||||
| 'Glassy'
|
|
||||||
| 'Header'
|
|
||||||
| 'HeaderLoaded'
|
|
||||||
| 'InnerContainer'
|
|
||||||
| 'ItemFocusAnim-darkGrey'
|
|
||||||
| 'ItemFocusAnim-darkerGrey'
|
|
||||||
| 'ItemFocusAnim-darkerGrey-nocolor'
|
|
||||||
| 'ItemFocusAnim-green'
|
|
||||||
| 'ItemFocusAnim-grey'
|
|
||||||
| 'ItemFocusAnimBorder-darkGrey'
|
|
||||||
| 'PlayBar'
|
|
||||||
| 'PreventScrolling'
|
|
||||||
| 'RightBreakNarrow'
|
|
||||||
| 'RightBreakUltraNarrow'
|
|
||||||
| 'RightBreakUltraWide'
|
|
||||||
| 'RightBreakWide'
|
|
||||||
| 'ScrollContainer'
|
|
||||||
| 'ShowPlayBar'
|
|
||||||
| 'Throbber'
|
|
||||||
| 'duration-app-launch'
|
|
||||||
| 'fadein'
|
|
||||||
| 'focusAnimation'
|
|
||||||
| 'hoverAnimation',
|
|
||||||
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'),
|
|
||||||
);
|
|
||||||
export const appDetailsHeaderClasses: AppDetailsHeaderClasses = findModule(
|
|
||||||
(mod) => typeof mod === 'object' && mod?.TopCapsule?.includes('sharedappdetailsheader'),
|
|
||||||
);
|
|
||||||
export const appDetailsClasses: AppDetailsClasses = findModule(
|
|
||||||
(mod) => typeof mod === 'object' && mod?.HeaderLoaded?.includes('appdetails_'),
|
|
||||||
);
|
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
declare global {
|
||||||
|
var SteamClient: SteamClient;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Apps {
|
export interface Apps {
|
||||||
RegisterForAppOverviewChanges: any;
|
RegisterForAppOverviewChanges: any;
|
||||||
RegisterForAppDetails: any;
|
RegisterForAppDetails: any;
|
||||||
@@ -217,6 +221,14 @@ export type AppLanguages = {
|
|||||||
strShortName: string;
|
strShortName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LogoPinPositions = 'BottomLeft' | 'UpperLeft' | 'CenterCenter' | 'UpperCenter' | 'BottomCenter';
|
||||||
|
|
||||||
|
export interface LogoPosition {
|
||||||
|
pinnedPosition: LogoPinPositions;
|
||||||
|
nWidthPct: number;
|
||||||
|
nHeightPct: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppDetails {
|
export interface AppDetails {
|
||||||
achievements: AppAchievements;
|
achievements: AppAchievements;
|
||||||
bCanMoveInstallFolder: boolean;
|
bCanMoveInstallFolder: boolean;
|
||||||
@@ -292,9 +304,18 @@ export interface AppDetails {
|
|||||||
vecMusicAlbums: any[];
|
vecMusicAlbums: any[];
|
||||||
vecPlatforms: string[];
|
vecPlatforms: string[];
|
||||||
vecScreenShots: any[];
|
vecScreenShots: any[];
|
||||||
|
libraryAssets?: {
|
||||||
|
logoPosition?: LogoPosition;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SteamAppOverview {
|
export interface SteamAppOverview {
|
||||||
display_name: string;
|
display_name: string;
|
||||||
gameid: 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
2
src/globals/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './SteamClient';
|
||||||
|
export * from './stores';
|
||||||
57
src/globals/stores.ts
Normal file
57
src/globals/stores.ts
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/index.ts
16
src/index.ts
@@ -1,8 +1,20 @@
|
|||||||
// export * from './deck-libs';
|
// export * from './deck-libs';
|
||||||
export * from './custom-components';
|
export * from './custom-components';
|
||||||
export * from './custom-hooks';
|
export * from './custom-hooks';
|
||||||
export * from './deck-components';
|
export * from './components';
|
||||||
export * from './deck-hooks';
|
export * from './deck-hooks';
|
||||||
export * from './plugin';
|
export * from './modules';
|
||||||
|
export * from './globals';
|
||||||
export * from './webpack';
|
export * from './webpack';
|
||||||
export * from './utils';
|
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
77
src/logger.ts
Normal 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
183
src/modules/Router.ts
Normal 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 Main Window Instance");
|
||||||
|
win = Router.WindowStore?.GamepadUIMainWindowInstance || Router?.WindowStore?.SteamUIWindows?.[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
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
1
src/modules/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './Router';
|
||||||
@@ -1,80 +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;
|
|
||||||
addGlobalComponent(name: string, component: ComponentType): void;
|
|
||||||
removeRoute(path: string): void;
|
|
||||||
removePatch(path: string, patch: RoutePatch): void;
|
|
||||||
removeGlobalComponent(name: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToastData {
|
|
||||||
title: ReactNode;
|
|
||||||
body: ReactNode;
|
|
||||||
onClick?: () => void;
|
|
||||||
logo?: ReactNode;
|
|
||||||
icon?: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
contentClassName?: string;
|
|
||||||
duration?: number;
|
|
||||||
critical?: boolean;
|
|
||||||
eType?: number;
|
|
||||||
sound?: number;
|
|
||||||
playSound?: boolean;
|
|
||||||
showToast?: 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);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
export * from './patcher';
|
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 {
|
export function joinClassNames(...classes: string[]): string {
|
||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
@@ -16,7 +24,22 @@ export function findSP(): Window {
|
|||||||
// old (SP as host)
|
// old (SP as host)
|
||||||
if (document.title == 'SP') return window;
|
if (document.title == 'SP') return window;
|
||||||
// new (SP as popup)
|
// new (SP as popup)
|
||||||
const context = FocusNavController.m_ActiveContext || FocusNavController.m_LastActiveContext;
|
const navTrees = getGamepadNavigationTrees();
|
||||||
return context.m_rgGamepadNavigationTrees.find((x: any) => x.m_ID == 'root_1_').Root
|
return navTrees?.find((x: any) => x.m_ID == 'GamepadUI_Full_Root' || x.m_ID == 'root_1_')?.Root?.Element?.ownerDocument?.defaultView;
|
||||||
.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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export interface PatchOptions {
|
|||||||
singleShot?: boolean;
|
singleShot?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type GenericPatchHandler = (args: any[], ret?: any) => any;
|
export type GenericPatchHandler = (args: any[], ret?: any) => any;
|
||||||
|
|
||||||
export interface Patch {
|
export interface Patch {
|
||||||
original: Function;
|
original: Function;
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
// this shouldn't need to be redeclared but it does for some reason
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
SP_REACT: typeof React;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fakeRenderComponent(fun: Function, 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 = {
|
|
||||||
useContext: hooks.useContext,
|
|
||||||
useCallback: hooks.useCallback,
|
|
||||||
useLayoutEffect: hooks.useLayoutEffect,
|
|
||||||
useEffect: hooks.useEffect,
|
|
||||||
useMemo: hooks.useMemo,
|
|
||||||
useRef: hooks.useRef,
|
|
||||||
useState: hooks.useState,
|
|
||||||
};
|
|
||||||
|
|
||||||
hooks.useCallback = (cb: Function) => cb;
|
|
||||||
hooks.useContext = (cb: any) => cb._currentValue;
|
|
||||||
hooks.useLayoutEffect = (_: Function) => {}; //cb();
|
|
||||||
hooks.useMemo = (cb: Function, _: any[]) => cb;
|
|
||||||
hooks.useEffect = (_: Function) => {}; //cb();
|
|
||||||
hooks.useRef = (val: any) => ({ current: val || {} });
|
|
||||||
hooks.useState = (v: any) => {
|
|
||||||
let val = v;
|
|
||||||
|
|
||||||
return [val, (n: any) => (val = n)];
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(hooks, customHooks);
|
|
||||||
|
|
||||||
const res = fun(hooks);
|
|
||||||
|
|
||||||
Object.assign(hooks, oldHooks);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function wrapReactType(node: any, prop: any = 'type') {
|
|
||||||
return (node[prop] = { ...node[prop] });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function wrapReactClass(node: any, prop: any = 'type') {
|
|
||||||
const cls = node[prop];
|
|
||||||
const wrappedCls = class extends cls {};
|
|
||||||
return (node[prop] = wrappedCls);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getReactInstance(o: HTMLElement | Element | Node) {
|
|
||||||
return o[Object.keys(o).find((k) => k.startsWith('__reactInternalInstance')) as string];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Based on https://github.com/GooseMod/GooseMod/blob/9ef146515a9e59ed4e25665ed365fd72fc0dcf23/src/util/react.js#L20
|
|
||||||
export interface findInTreeOpts {
|
|
||||||
walkable?: string[];
|
|
||||||
ignore?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare type findInTreeFilter = (element: any) => boolean;
|
|
||||||
|
|
||||||
export const findInTree = (parent: any, filter: findInTreeFilter, opts: findInTreeOpts): any => {
|
|
||||||
const { walkable = null, ignore = [] } = opts ?? {};
|
|
||||||
|
|
||||||
if (!parent || typeof parent !== 'object') {
|
|
||||||
// Parent is invalid to search through
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter(parent)) return parent; // Parent matches, just return
|
|
||||||
|
|
||||||
if (Array.isArray(parent)) {
|
|
||||||
// Parent is an array, go through values
|
|
||||||
return parent.map((x) => findInTree(x, filter, opts)).find((x) => x);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parent is an object, go through values (or option to only use certain keys)
|
|
||||||
return (walkable || Object.keys(parent))
|
|
||||||
.map((x) => !ignore.includes(x) && findInTree(parent[x], filter, opts))
|
|
||||||
.find((x: any) => x);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findInReactTree = (node: any, filter: findInTreeFilter) =>
|
|
||||||
findInTree(node, filter, {
|
|
||||||
// Specialised findInTree for React nodes
|
|
||||||
walkable: ['props', 'children', 'child', 'sibling'],
|
|
||||||
});
|
|
||||||
139
src/utils/react/fc.ts
Normal file
139
src/utils/react/fc.ts
Normal 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;
|
||||||
|
}
|
||||||
171
src/utils/react/react.ts
Normal file
171
src/utils/react/react.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Ref, useState } from 'react';
|
||||||
|
|
||||||
|
// this shouldn't need to be redeclared but it does for some reason
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
SP_REACT: typeof React;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
oldHooks = {
|
||||||
|
useContext: hooks.useContext,
|
||||||
|
useCallback: hooks.useCallback,
|
||||||
|
useLayoutEffect: hooks.useLayoutEffect,
|
||||||
|
useEffect: hooks.useEffect,
|
||||||
|
useMemo: hooks.useMemo,
|
||||||
|
useRef: hooks.useRef,
|
||||||
|
useState: hooks.useState,
|
||||||
|
};
|
||||||
|
|
||||||
|
hooks.useCallback = (cb: Function) => cb;
|
||||||
|
hooks.useContext = (cb: any) => cb._currentValue;
|
||||||
|
hooks.useLayoutEffect = (_: Function) => {}; //cb();
|
||||||
|
hooks.useMemo = (cb: Function, _: any[]) => cb;
|
||||||
|
hooks.useEffect = (_: Function) => {}; //cb();
|
||||||
|
hooks.useRef = (val: any) => ({ current: val || {} });
|
||||||
|
hooks.useState = (v: any) => {
|
||||||
|
let val = v;
|
||||||
|
|
||||||
|
return [val, (n: any) => (val = n)];
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(hooks, customHooks);
|
||||||
|
|
||||||
|
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') {
|
||||||
|
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') {
|
||||||
|
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 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
|
||||||
|
export interface findInTreeOpts {
|
||||||
|
walkable?: string[];
|
||||||
|
ignore?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare type findInTreeFilter = (element: any) => boolean;
|
||||||
|
|
||||||
|
export const findInTree = (parent: any, filter: findInTreeFilter, opts: findInTreeOpts): any => {
|
||||||
|
const { walkable = null, ignore = [] } = opts ?? {};
|
||||||
|
|
||||||
|
if (!parent || typeof parent !== 'object') {
|
||||||
|
// Parent is invalid to search through
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter(parent)) return parent; // Parent matches, just return
|
||||||
|
|
||||||
|
if (Array.isArray(parent)) {
|
||||||
|
// Parent is an array, go through values
|
||||||
|
return parent.map((x) => findInTree(x, filter, opts)).find((x) => x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent is an object, go through values (or option to only use certain keys)
|
||||||
|
return (walkable || Object.keys(parent))
|
||||||
|
.map((x) => !ignore.includes(x) && findInTree(parent[x], filter, opts))
|
||||||
|
.find((x: any) => x);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findInReactTree = (node: any, filter: findInTreeFilter) =>
|
||||||
|
findInTree(node, filter, {
|
||||||
|
// Specialised findInTree for React nodes
|
||||||
|
walkable: ['props', 'children', 'child', 'sibling'],
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the parent window of a DOM element
|
||||||
|
*/
|
||||||
|
export function getParentWindow<WindowType = Window>(elem: HTMLElement | null): WindowType | null | undefined {
|
||||||
|
return elem?.ownerDocument?.defaultView as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook to find the host window of a component
|
||||||
|
* Pass the returned ref into a React element and window will be its host window.
|
||||||
|
* @returns [ref, window]
|
||||||
|
*/
|
||||||
|
export function useWindowRef<RefElementType extends HTMLElement, WindowType = Window>(): [Ref<RefElementType>, WindowType | null | undefined] {
|
||||||
|
const [win, setWin] = useState<WindowType | null | undefined>(null);
|
||||||
|
|
||||||
|
return [(elem) => setWin(getParentWindow<WindowType>(elem)), win];
|
||||||
|
}
|
||||||
100
src/utils/react/treepatcher.ts
Normal file
100
src/utils/react/treepatcher.ts
Normal 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
1104
src/utils/static-classes.ts
Normal file
File diff suppressed because it is too large
Load Diff
138
src/webpack.ts
138
src/webpack.ts
@@ -1,61 +1,105 @@
|
|||||||
|
import Logger from './logger';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
webpackJsonp: any;
|
|
||||||
webpackChunksteamui: 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 Module = any;
|
||||||
|
export type Export = any;
|
||||||
type FilterFn = (module: any) => boolean;
|
type FilterFn = (module: any) => boolean;
|
||||||
|
type ExportFilterFn = (moduleExport: any, exportName?: any) => boolean;
|
||||||
type FindFn = (module: any) => any;
|
type FindFn = (module: any) => any;
|
||||||
|
|
||||||
export let webpackCache: any = {};
|
export let modules = new Map<ModuleID, Module>();
|
||||||
let hasWebpack5 = false;
|
|
||||||
|
|
||||||
if (window.webpackJsonp && !window.webpackJsonp.deckyShimmed) {
|
function initModuleCache() {
|
||||||
// Webpack 4, currently on stable
|
const startTime = performance.now();
|
||||||
const wpRequire = window.webpackJsonp.push([
|
logger.group('Webpack Module Init');
|
||||||
[],
|
|
||||||
{ 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 {
|
|
||||||
// Webpack 5, currently on beta
|
// Webpack 5, currently on beta
|
||||||
hasWebpack5 = true;
|
// Generate a fake module ID
|
||||||
const id = Math.random();
|
const id = Symbol("@decky/ui");
|
||||||
let initReq: any;
|
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([
|
window.webpackChunksteamui.push([
|
||||||
[id],
|
[id],
|
||||||
{},
|
{},
|
||||||
(r: any) => {
|
(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
|
initModuleCache();
|
||||||
? Object.values(webpackCache).filter((x) => x)
|
|
||||||
: Object.keys(webpackCache)
|
|
||||||
.map((x) => webpackCache[x].exports)
|
|
||||||
.filter((x) => x);
|
|
||||||
|
|
||||||
export const findModule = (filter: FilterFn) => {
|
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 (m.default && filter(m.default)) return m.default;
|
||||||
if (filter(m)) return m;
|
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) => {
|
export const findModuleChild = (filter: FindFn) => {
|
||||||
for (const m of allModules) {
|
for (const m of modules.values()) {
|
||||||
for (const mod of [m.default, m]) {
|
for (const mod of [m.default, m]) {
|
||||||
const filterRes = filter(mod);
|
const filterRes = filter(mod);
|
||||||
if (filterRes) {
|
if (filterRes) {
|
||||||
@@ -67,10 +111,13 @@ export const findModuleChild = (filter: FindFn) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use createModuleMapping instead
|
||||||
|
*/
|
||||||
export const findAllModules = (filter: FilterFn) => {
|
export const findAllModules = (filter: FilterFn) => {
|
||||||
const out = [];
|
const out = [];
|
||||||
|
|
||||||
for (const m of allModules) {
|
for (const m of modules.values()) {
|
||||||
if (m.default && filter(m.default)) out.push(m.default);
|
if (m.default && filter(m.default)) out.push(m.default);
|
||||||
if (filter(m)) out.push(m);
|
if (filter(m)) out.push(m);
|
||||||
}
|
}
|
||||||
@@ -78,7 +125,18 @@ export const findAllModules = (filter: FilterFn) => {
|
|||||||
return out;
|
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;
|
if (typeof m !== 'object') return false;
|
||||||
for (let prop in m) {
|
for (let prop in m) {
|
||||||
if (m[prop]?.contextType?._currentValue && Object.keys(m).length > 60) return true;
|
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;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const IconsModule = findModule((m: Module) => {
|
export const IconsModule = findModuleByExport(
|
||||||
if (typeof m !== 'object') return false;
|
(e) => e?.toString && /Spinner\)}\)?,.\.createElement\(\"path\",{d:\"M18 /.test(e.toString()),
|
||||||
for (let prop in m) {
|
);
|
||||||
if (m[prop]?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(m[prop].toString())) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ReactRouter = allModules.find((m: Module) => {
|
export const ReactRouter = findModuleByExport((e) => e.computeRootMatch);
|
||||||
if (typeof m !== 'object') return undefined;
|
|
||||||
for (let prop in m) {
|
|
||||||
if (m[prop]?.computeRootMatch) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -14,10 +14,9 @@
|
|||||||
"noImplicitThis": true,
|
"noImplicitThis": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"suppressImplicitAnyIndexErrors": true,
|
"removeComments": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true
|
||||||
"skipLibCheck": true
|
|
||||||
},
|
},
|
||||||
"include": ["src", "globals.d.ts"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"githubPages": false,
|
"categorizeByGroup": false,
|
||||||
"categorizeByGroup": false,
|
"excludeExternals": true
|
||||||
"excludeExternals": true
|
}
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user