Compare commits
354 Commits
features/c
...
v1.0.7
Author | SHA1 | Date | |
---|---|---|---|
|
ccca41a168 | ||
|
af4c7d8a90 | ||
|
77c691651e | ||
|
e14ec96e21 | ||
|
ff1f337699 | ||
|
3ca87848e5 | ||
|
9420735bc7 | ||
|
6807820f6c | ||
|
3cad0d60b4 | ||
|
fbbc198b1b | ||
|
224ae979f2 | ||
|
27fc20118b | ||
|
60ccd8cc25 | ||
|
5c1519156b | ||
|
58270ef925 | ||
|
68055977d2 | ||
|
2b0c25b45c | ||
|
c154965d70 | ||
|
614234ad20 | ||
|
2489bc6831 | ||
|
d95be1acba | ||
|
1d995e58f5 | ||
|
6114bc6b16 | ||
|
ae98401bd3 | ||
|
992a77e76c | ||
|
291f38115c | ||
|
8a52189378 | ||
|
70278f64a9 | ||
|
7b1d03699d | ||
|
8b40760187 | ||
|
de0990434e | ||
|
369d179bbf | ||
|
8f4e8d0a9b | ||
|
62f7e4d45c | ||
|
fce7d6b681 | ||
|
69715070b9 | ||
|
d9fb7f45b5 | ||
|
e32388e3ac | ||
|
823fa2d0c3 | ||
|
3cdffe444e | ||
|
429ab9d363 | ||
|
072ad3d7e6 | ||
|
6e22a96d9e | ||
|
bc4c7473e8 | ||
|
399305fd8a | ||
|
0c030a3a27 | ||
|
49aacccc19 | ||
|
6ab4b48b47 | ||
|
103cd14361 | ||
|
41226f0358 | ||
|
5d3148cf50 | ||
|
d628924b59 | ||
|
f19504f828 | ||
|
a38ac956df | ||
|
34276301c3 | ||
|
b2ecb02335 | ||
|
25d32ce292 | ||
|
cb4c50842f | ||
|
83757b19be | ||
|
75050e74ca | ||
|
8a43e9b25f | ||
|
84cfe531af | ||
|
68e80c4d4c | ||
|
b4f98e5066 | ||
|
9602f527d8 | ||
|
64180362fd | ||
|
6e44b8c47e | ||
|
2641adb29b | ||
|
ef5b3e1818 | ||
|
7fe3a2c805 | ||
|
c4d2b4a8cd | ||
|
08a2030bbc | ||
|
5fe0600d6c | ||
|
ebdcbcaf0c | ||
|
1d287357ca | ||
|
e49151ff33 | ||
|
7478e880a8 | ||
|
be7fa0cb3f | ||
|
9338b92b1a | ||
|
efb0ef8b9c | ||
|
fd766bc98f | ||
|
0e5b8b07c9 | ||
|
7582feb603 | ||
|
6329499b1d | ||
|
32cdb63885 | ||
|
ea748dfb60 | ||
|
6c5fcc4119 | ||
|
26f2b51eb9 | ||
|
075b0e0970 | ||
|
10fd51071e | ||
|
e70abc57b6 | ||
|
a8678db78c | ||
|
bedb7b212b | ||
|
b39cbcd934 | ||
|
19c9a13273 | ||
|
c525672777 | ||
|
a772aa62f5 | ||
|
23a461c36d | ||
|
da2d317555 | ||
|
95df164e44 | ||
|
ae9fe7fcfd | ||
|
f0240ec345 | ||
|
15aa2299c3 | ||
|
06aa72c636 | ||
|
1713450540 | ||
|
eecc555dac | ||
|
5a3fbbfb30 | ||
|
cc51f6e2d2 | ||
|
8113ed3c8c | ||
|
b8ed72286b | ||
|
9c5a149fb1 | ||
|
cf2bf2b43a | ||
|
e6f759eecd | ||
|
933216fcd5 | ||
|
bcbbc79365 | ||
|
374531d10e | ||
|
2e5d27b6b6 | ||
|
2172cae779 | ||
|
e740f55450 | ||
|
aff1b68d6b | ||
|
074542f0b3 | ||
|
b0c41d556a | ||
|
af0d34b155 | ||
|
6dd705f951 | ||
|
259f0284f0 | ||
|
cb9eb1f772 | ||
|
42b4eebca1 | ||
|
a9ee0c7e50 | ||
|
73b7f11d7a | ||
|
d806be1346 | ||
|
1f73cfa91a | ||
|
7e6077367a | ||
|
103c499310 | ||
|
9dcafbf468 | ||
|
1742bb6020 | ||
|
0743c1215e | ||
|
94ad8e8f61 | ||
|
989bd36eeb | ||
|
4974c53f9c | ||
|
47de9fab2e | ||
|
3efc79224f | ||
|
456164253d | ||
|
c257f86576 | ||
|
f6122a00ca | ||
|
f1bdfdd6b9 | ||
|
c8f2141114 | ||
|
fea8c60a40 | ||
|
a67db11dc2 | ||
|
9a088b7a31 | ||
|
ebb8da0f23 | ||
|
f2e0542614 | ||
|
ee24439795 | ||
|
022bf17140 | ||
|
2de461985d | ||
|
2d08dd8a9c | ||
|
49b45d8262 | ||
|
8a5a5c7d1e | ||
|
53d0a55561 | ||
|
25ef5d60b4 | ||
|
c74241fde6 | ||
|
4d8145f12c | ||
|
d4f70218ba | ||
|
6b4b4772bb | ||
|
54010aab94 | ||
|
62b2acebe6 | ||
|
41dddc9eee | ||
|
4760af7f0e | ||
|
06d32ae414 | ||
|
2564ab73f5 | ||
|
5e97cc0fc3 | ||
|
b9e9d9bd64 | ||
|
daf3a1dcac | ||
|
f1fb79d2c5 | ||
|
0ff6d3dd41 | ||
|
734054ff68 | ||
|
f94cbfb2f4 | ||
|
fc09460d82 | ||
|
e884738f42 | ||
|
c583bad6bf | ||
|
36b787812e | ||
|
836ae72076 | ||
|
d0a40bc0ed | ||
|
3b4879f9d9 | ||
|
a0a1a4d139 | ||
|
bad96b7887 | ||
|
7a4402f142 | ||
|
3e9672c6b8 | ||
|
a9fee6248e | ||
|
f0ee16f173 | ||
|
3db3c63b42 | ||
|
4fc41c8c0b | ||
|
47c181beec | ||
|
c4fc01c7ff | ||
|
5a94201578 | ||
|
6b55dee9fb | ||
|
a85ec594a7 | ||
|
c2c6c9fccb | ||
|
b60f6cb18d | ||
|
bb398970ef | ||
|
50a96e8047 | ||
|
c5b5b754e2 | ||
|
0f644dff73 | ||
|
6210d3a597 | ||
|
e7573382fe | ||
|
f4d7a1f4fb | ||
|
5dd0a3a746 | ||
|
c9fac8ffff | ||
|
f93607fc66 | ||
|
63ffb5bebc | ||
|
2788d264d4 | ||
|
91f1d68e29 | ||
|
7e4f4f1794 | ||
|
9f7ec0aa8d | ||
|
0239bb0aac | ||
|
ec20556d5c | ||
|
11191b5943 | ||
|
1f72a0fc27 | ||
|
31ec1ec1b4 | ||
|
0f7c80fd4d | ||
|
b5bc88c7d4 | ||
|
b42b8d755f | ||
|
bfe1fd9912 | ||
|
c45d89697a | ||
|
0a92bd6521 | ||
|
33c33eb0fd | ||
|
dcf1148bb4 | ||
|
58e28b4281 | ||
|
bb14d4989d | ||
|
9bcdc8451f | ||
|
46b14cb2e0 | ||
|
9240865f65 | ||
|
e85d763f22 | ||
|
82911386db | ||
|
e63ed9cac4 | ||
|
ba45ecda56 | ||
|
7ff2d2ba8a | ||
|
a5154d6283 | ||
|
5ce2dc1bb4 | ||
|
8f2c247f27 | ||
|
43f41d20fa | ||
|
50c356e397 | ||
|
503a2ec517 | ||
|
83b3b1f16b | ||
|
2628bdce42 | ||
|
8b0911b86a | ||
|
47d127a895 | ||
|
410613726b | ||
|
8b3f290e3c | ||
|
a788813383 | ||
|
e1de6f88fe | ||
|
ae86848cf6 | ||
|
84ec839b04 | ||
|
b30508aef8 | ||
|
eabbf7d9bd | ||
|
be088f9072 | ||
|
2ca98a87d2 | ||
|
b49ac6b541 | ||
|
82e444e196 | ||
|
a96f8a89f3 | ||
|
4642b54260 | ||
|
15b257a7b0 | ||
|
0dbec8d0cd | ||
|
e5b23ff556 | ||
|
9110d1f9bd | ||
|
81edc14070 | ||
|
b48c8d8a4a | ||
|
8380328465 | ||
|
30ca4f1cf9 | ||
|
62e0787cf2 | ||
|
cc7c14ec88 | ||
|
a86452e774 | ||
|
dddb28192c | ||
|
2133823bd3 | ||
|
1176896a1b | ||
|
f3aba3edb0 | ||
|
409e54a9d8 | ||
|
31fb19b8c9 | ||
|
a26f636c9b | ||
|
8ba9c96f20 | ||
|
57f3feba68 | ||
|
010523eeac | ||
|
15f12073cf | ||
|
58636a9a82 | ||
|
0bc894d065 | ||
|
6f38c4b7fe | ||
|
c1d2f0078f | ||
|
3c8084ec36 | ||
|
3b65384b94 | ||
|
e0450531ef | ||
|
b032e9b6e3 | ||
|
1e6b967d24 | ||
|
460f329e4f | ||
|
3a3a52c493 | ||
|
4e57ae66f1 | ||
|
f7d9be9140 | ||
|
955573d31b | ||
|
6a8564089b | ||
|
7d5ade21fc | ||
|
d69dfd6205 | ||
|
177d353f50 | ||
|
a13c0df1cd | ||
|
0af4579204 | ||
|
851d07f31a | ||
|
963a7332b4 | ||
|
440baf6028 | ||
|
9663e229a6 | ||
|
0cb24cad7e | ||
|
65620f4976 | ||
|
cb7469afad | ||
|
2c3dee4120 | ||
|
c20dc269d2 | ||
|
a7795533df | ||
|
5e1b42120c | ||
|
676f5c7e30 | ||
|
13c73699e9 | ||
|
64aed87de4 | ||
|
1944f3957f | ||
|
04d6f341ee | ||
|
0c25278c59 | ||
|
0fda900ccc | ||
|
8adf7ca155 | ||
|
b905743077 | ||
|
a43a41f61f | ||
|
3af9a14a0e | ||
|
739b1e47d4 | ||
|
d72542405a | ||
|
95aa2d9d8d | ||
|
93859883c1 | ||
|
37105ac416 | ||
|
f6e0efe20a | ||
|
1764206e19 | ||
|
6b0caaae37 | ||
|
c76e9f5e3d | ||
|
ce73a5f172 | ||
|
9548978d80 | ||
|
13882b5732 | ||
|
49e72bab32 | ||
|
bbd3633038 | ||
|
c65f757bc4 | ||
|
b87f0bf3f9 | ||
|
670b7d7d01 | ||
|
f492d26379 | ||
|
56b00f715a | ||
|
fe5a78ddc9 | ||
|
5e7c155f6e | ||
|
e06ba68c40 | ||
|
d6fe937a70 | ||
|
2f46b934c9 | ||
|
2af324c302 | ||
|
4bddcee40b | ||
|
559edbfffe | ||
|
6c38362401 | ||
|
00402c69d6 | ||
|
30dd4b9e01 |
@ -2,7 +2,26 @@
|
|||||||
"root": true,
|
"root": true,
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"ignorePatterns": ["dist", "browser"],
|
"ignorePatterns": ["dist", "browser"],
|
||||||
"plugins": ["header", "simple-import-sort", "unused-imports"],
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"header",
|
||||||
|
"simple-import-sort",
|
||||||
|
"unused-imports",
|
||||||
|
"path-alias"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"alias": {
|
||||||
|
"map": [
|
||||||
|
["@webpack", "./src/webpack"],
|
||||||
|
["@webpack/common", "./src/webpack/common"],
|
||||||
|
["@utils", "./src/utils"],
|
||||||
|
["@api", "./src/api"],
|
||||||
|
["@components", "./src/components"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
// Since it's only been a month and Vencord has already been stolen
|
// Since it's only been a month and Vencord has already been stolen
|
||||||
// by random skids who rebranded it to "AlphaCord" and erased all license
|
// by random skids who rebranded it to "AlphaCord" and erased all license
|
||||||
@ -18,7 +37,7 @@
|
|||||||
" * Vencord, a modification for Discord's desktop app",
|
" * Vencord, a modification for Discord's desktop app",
|
||||||
{
|
{
|
||||||
"pattern": " \\* Copyright \\(c\\) \\d{4}",
|
"pattern": " \\* Copyright \\(c\\) \\d{4}",
|
||||||
"template": " * Copyright (c) 2022 Vendicated and contributors"
|
"template": " * Copyright (c) 2023 Vendicated and contributors"
|
||||||
},
|
},
|
||||||
" *",
|
" *",
|
||||||
" * This program is free software: you can redistribute it and/or modify",
|
" * This program is free software: you can redistribute it and/or modify",
|
||||||
@ -63,9 +82,13 @@
|
|||||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||||
"no-duplicate-imports": "error",
|
"no-duplicate-imports": "error",
|
||||||
"no-extra-semi": "error",
|
"no-extra-semi": "error",
|
||||||
"consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }],
|
|
||||||
"dot-notation": "error",
|
"dot-notation": "error",
|
||||||
"no-useless-escape": "error",
|
"no-useless-escape": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"extra": "i"
|
||||||
|
}
|
||||||
|
],
|
||||||
"no-fallthrough": "error",
|
"no-fallthrough": "error",
|
||||||
"for-direction": "error",
|
"for-direction": "error",
|
||||||
"no-async-promise-executor": "error",
|
"no-async-promise-executor": "error",
|
||||||
@ -88,6 +111,8 @@
|
|||||||
"simple-import-sort/imports": "error",
|
"simple-import-sort/imports": "error",
|
||||||
"simple-import-sort/exports": "error",
|
"simple-import-sort/exports": "error",
|
||||||
|
|
||||||
"unused-imports/no-unused-imports": "error"
|
"unused-imports/no-unused-imports": "error",
|
||||||
|
|
||||||
|
"path-alias/no-relative": "error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
22
.github/ISSUE_TEMPLATE/blank.yml
vendored
Normal file
22
.github/ISSUE_TEMPLATE/blank.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
name: Blank Template
|
||||||
|
description: Use this only if your issue does not fit into another template. **DO NOT ASK FOR SUPPORT OR REQUEST PLUGINS**
|
||||||
|
labels: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: info-sec
|
||||||
|
attributes:
|
||||||
|
label: Tell us all about it.
|
||||||
|
description: Go nuts, let us know what you're wanting to bring attention to.
|
||||||
|
placeholder: ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: agreement-check
|
||||||
|
attributes:
|
||||||
|
label: Request Agreement
|
||||||
|
description: DO NOT USE THIS TEMPLATE FOR SUPPORT OR PLUGIN REQUESTS!!! For Support, **join our Discord**. For plugin requests, **use discussions**
|
||||||
|
options:
|
||||||
|
- label: This is not a support or plugin request
|
||||||
|
required: true
|
66
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
66
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
name: Bug/Crash Report
|
||||||
|
description: Create a bug or crash report for Vencord
|
||||||
|
labels: [bug]
|
||||||
|
title: "[Bug] <title>"
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
id: discord
|
||||||
|
attributes:
|
||||||
|
label: Discord Account
|
||||||
|
description: Who on Discord is making this request? Not required but encouraged for easier follow-up
|
||||||
|
placeholder: username#0000
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: bug-description
|
||||||
|
attributes:
|
||||||
|
label: What happens when the bug or crash occurs?
|
||||||
|
description: Where does this bug or crash occur, when does it occur, etc.
|
||||||
|
placeholder: The bug/crash happens sometimes when I do ..., causing this to not work/the app to crash. I think it happens because of ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behaviour
|
||||||
|
attributes:
|
||||||
|
label: What is the expected behaviour?
|
||||||
|
description: Simply detail what the expected behaviour is.
|
||||||
|
placeholder: I expect Vencord/Discord to open the ... page instead of ..., it prevents me from doing ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps-to-take
|
||||||
|
attributes:
|
||||||
|
label: How do you recreate this bug or crash?
|
||||||
|
description: Give us a list of steps in order to recreate the bug or crash.
|
||||||
|
placeholder: |
|
||||||
|
1. Do ...
|
||||||
|
2. Then ...
|
||||||
|
3. Do this ..., ... and then ...
|
||||||
|
4. Observe "the bug" or "the crash"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: crash-log
|
||||||
|
attributes:
|
||||||
|
label: Errors
|
||||||
|
description: Open the Developer Console with Ctrl/Cmd + Shift + i. Then look for any red errors (Ignore network errors like Failed to load resource) and paste them between the "```".
|
||||||
|
value: |
|
||||||
|
```
|
||||||
|
Replace this text with your crash-log.
|
||||||
|
```
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: agreement-check
|
||||||
|
attributes:
|
||||||
|
label: Request Agreement
|
||||||
|
description: We only accept reports for bugs that happen on Discord Stable. Canary and PTB are Development branches and may be unstable
|
||||||
|
options:
|
||||||
|
- label: I am using Discord Stable or tried on Stable and this bug happens there as well
|
||||||
|
required: true
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Vencord Support Server
|
||||||
|
url: https://discord.gg/D9uwnFnqmd
|
||||||
|
about: If you need help regarding Vencord, please join our support server!
|
||||||
|
- name: Vencord Installer
|
||||||
|
url: https://github.com/Vencord/Installer
|
||||||
|
about: You can find the Vencord Installer here
|
32
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
32
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Create a feature request for Vencord. To request new plugins, please use the Discussions tab
|
||||||
|
labels: [enhancement]
|
||||||
|
title: "[Feature Request] <title>"
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
id: discord
|
||||||
|
attributes:
|
||||||
|
label: Discord Account
|
||||||
|
description: Who on Discord is making this request? Not required but encouraged for easier follow-up
|
||||||
|
placeholder: username#0000
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: feature-basic-description
|
||||||
|
attributes:
|
||||||
|
label: What is it that you'd like to see?
|
||||||
|
description: Describe the feature you want added as detailed as possible
|
||||||
|
placeholder: I think ... would be a cool feature to add. This would be awesome, thanks!
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: agreement-check
|
||||||
|
attributes:
|
||||||
|
label: Request Agreement
|
||||||
|
description: DO NOT USE THIS TEMPLATE FOR PLUGIN REQUESTS!!! For plugin requests, **use discussions**
|
||||||
|
options:
|
||||||
|
- label: This is not a plugin request
|
||||||
|
required: true
|
65
.github/workflows/build.yml
vendored
65
.github/workflows/build.yml
vendored
@ -1,8 +1,15 @@
|
|||||||
name: Build latest
|
name: Build DevBuild
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths:
|
||||||
|
- .github/workflows/build.yml
|
||||||
|
- src/**
|
||||||
|
- browser/**
|
||||||
|
- scripts/build/**
|
||||||
|
- package.json
|
||||||
|
- pnpm-lock.yaml
|
||||||
env:
|
env:
|
||||||
FORCE_COLOR: true
|
FORCE_COLOR: true
|
||||||
|
|
||||||
@ -15,42 +22,54 @@ jobs:
|
|||||||
|
|
||||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
- name: Use Node.js 18
|
- name: Use Node.js 19
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 19
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build web
|
- name: Build web
|
||||||
run: pnpm buildWeb
|
run: pnpm buildWeb --standalone
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build --standalone
|
run: pnpm build --standalone
|
||||||
|
|
||||||
- name: Get some values needed for the release
|
- name: Clean up obsolete files
|
||||||
id: vars
|
|
||||||
shell: bash
|
|
||||||
run: |
|
run: |
|
||||||
echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
rm -rf dist/extension* Vencord.user.css
|
||||||
|
|
||||||
- uses: dev-drprasad/delete-tag-and-release@085c6969f18bad0de1b9f3fe6692a3cd01f64fe5 # v0.2.0
|
- name: Get some values needed for the release
|
||||||
with:
|
id: release_values
|
||||||
delete_release: true
|
run: |
|
||||||
tag_name: devbuild
|
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Upload DevBuild as release
|
||||||
|
run: |
|
||||||
|
gh release upload devbuild --clobber dist/*
|
||||||
|
gh release edit devbuild --title "DevBuild $RELEASE_TAG"
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
RELEASE_TAG: ${{ env.release_tag }}
|
||||||
|
|
||||||
- name: Create the release
|
- name: Upload DevBuild to builds repo
|
||||||
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v1
|
run: |
|
||||||
|
git config --global user.name "$USERNAME"
|
||||||
|
git config --global user.email actions@github.com
|
||||||
|
|
||||||
|
git clone https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git upload
|
||||||
|
cd upload
|
||||||
|
|
||||||
|
GLOBIGNORE=.git:.gitignore:README.md:LICENSE
|
||||||
|
rm -rf *
|
||||||
|
cp -r ../dist/* .
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git commit -m "Builds for https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA"
|
||||||
|
git push --force https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
API_TOKEN: ${{ secrets.BUILDS_TOKEN }}
|
||||||
with:
|
GH_REPO: Vencord/builds
|
||||||
tag_name: devbuild
|
USERNAME: GitHub-Actions
|
||||||
name: Dev Build ${{ steps.vars.outputs.sha_short }}
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
files: |
|
|
||||||
dist/*
|
|
||||||
|
61
.github/workflows/publish.yml
vendored
Normal file
61
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
name: Release Browser Extension
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: check that tag matches package.json version
|
||||||
|
run: |
|
||||||
|
pkg_version="v$(jq -r .version < package.json)"
|
||||||
|
if [[ "${{ github.ref_name }}" != "$pkg_version" ]]; then
|
||||||
|
echo "Tag ${{ github.ref_name }} does not match package.json version $pkg_version" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
|
- name: Use Node.js 19
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 19
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build web
|
||||||
|
run: pnpm buildWeb --standalone
|
||||||
|
|
||||||
|
- name: Publish extension
|
||||||
|
run: |
|
||||||
|
cd dist/extension-unpacked
|
||||||
|
|
||||||
|
# Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later
|
||||||
|
EXIT_CODE=0
|
||||||
|
|
||||||
|
# Chrome
|
||||||
|
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$?
|
||||||
|
|
||||||
|
# Firefox
|
||||||
|
npm i -g web-ext@7.4.0 web-ext-submit@7.4.0
|
||||||
|
web-ext-submit || EXIT_CODE=$?
|
||||||
|
|
||||||
|
exit $EXIT_CODE
|
||||||
|
env:
|
||||||
|
# Chrome
|
||||||
|
EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
|
||||||
|
CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
|
||||||
|
CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||||
|
REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||||
|
|
||||||
|
# Firefox
|
||||||
|
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
|
||||||
|
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}
|
||||||
|
|
57
.github/workflows/reportBrokenPlugins.yml
vendored
Normal file
57
.github/workflows/reportBrokenPlugins.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
name: Test Patches
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
# Every day at midnight
|
||||||
|
- cron: 0 0 * * *
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
TestPlugins:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
|
- name: Use Node.js 19
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 19
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm add puppeteer
|
||||||
|
|
||||||
|
sudo apt-get install -y chromium-browser
|
||||||
|
|
||||||
|
- name: Build web
|
||||||
|
run: pnpm buildWeb --standalone
|
||||||
|
|
||||||
|
- name: Create Report
|
||||||
|
timeout-minutes: 10
|
||||||
|
run: |
|
||||||
|
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||||
|
export CHROMIUM_BIN=$(which chromium-browser)
|
||||||
|
|
||||||
|
esbuild test/generateReport.ts > dist/report.mjs
|
||||||
|
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||||
|
env:
|
||||||
|
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||||
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
|
- name: Create Report (Canary)
|
||||||
|
timeout-minutes: 10
|
||||||
|
if: success() || failure() # even run if previous one failed
|
||||||
|
run: |
|
||||||
|
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||||
|
export CHROMIUM_BIN=$(which chromium-browser)
|
||||||
|
export USE_CANARY=true
|
||||||
|
|
||||||
|
esbuild test/generateReport.ts > dist/report.mjs
|
||||||
|
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||||
|
env:
|
||||||
|
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||||
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@ -23,5 +23,8 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Lint & Test if it compiles
|
- name: Lint & Test if desktop version compiles
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|
||||||
|
- name: Lint & Test if web version compiles
|
||||||
|
run: pnpm testWeb
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -5,6 +5,7 @@ node_modules
|
|||||||
vencord_installer
|
vencord_installer
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
yarn.lock
|
yarn.lock
|
||||||
package-lock.json
|
package-lock.json
|
||||||
@ -18,3 +19,6 @@ lerna-debug.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
src/userplugins
|
src/userplugins
|
||||||
|
|
||||||
|
ExtensionCache/
|
||||||
|
settings/
|
||||||
|
6
.stylelintrc.json
Normal file
6
.stylelintrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "stylelint-config-standard",
|
||||||
|
"rules": {
|
||||||
|
"indentation": 4
|
||||||
|
}
|
||||||
|
}
|
10
.vscode/extensions.json
vendored
10
.vscode/extensions.json
vendored
@ -1,3 +1,11 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [ "EditorConfig.EditorConfig" ]
|
"recommendations": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"eamodio.gitlens",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"ExodiusStudios.comment-anchors",
|
||||||
|
"formulahendry.auto-rename-tag",
|
||||||
|
"GregorBiswanger.json2ts",
|
||||||
|
"stylelint.vscode-stylelint"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
37
.vscode/launch.json
vendored
Normal file
37
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
// this allows you to debug Vencord from VSCode.
|
||||||
|
// How to use:
|
||||||
|
// You need to run Discord via the command line to pass some flags to it.
|
||||||
|
// If you want to debug the main (node.js) process (preload.ts, ipcMain/*, patcher.ts),
|
||||||
|
// add the --inspect flag
|
||||||
|
// To debug the renderer (99% of Vencord), add the --remote-debugging-port=9223 flag
|
||||||
|
//
|
||||||
|
// Now launch the desired configuration in VSCode and start Discord with the flags.
|
||||||
|
// For example, to debug both process, run Electron: All then launch Discord with
|
||||||
|
// discord --remote-debugging-port=9223 --inspect
|
||||||
|
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Electron: Main",
|
||||||
|
"type": "node",
|
||||||
|
"request": "attach",
|
||||||
|
"port": 9229,
|
||||||
|
"timeout": 30000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Electron: Renderer",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "attach",
|
||||||
|
"port": 9223,
|
||||||
|
"timeout": 30000,
|
||||||
|
"webRoot": "${workspaceFolder}/src"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Electron: All",
|
||||||
|
"configurations": ["Electron: Main", "Electron: Renderer"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
34
README.md
34
README.md
@ -1,38 +1,30 @@
|
|||||||
# Vencord
|
# Vencord
|
||||||
|
|
||||||
A Discord client mod that does things differently
|
The cutest Discord client mod
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Works on Discord's latest update that breaks all other mods
|
- Super easy to install (one click installer)
|
||||||
- Browser Support (experimental): Run Vencord in your Browser instead of the desktop app
|
- 90+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
||||||
- Custom Css and Themes: Manually edit `%appdata%/Vencord/settings/quickCss.css` / `~/.config/Vencord/settings/quickCss.css` with your favourite editor and the client will automatically apply your changes. To import BetterDiscord themes, just add `@import url(theUrl)` on the top of this file. (Make sure the url is a github raw URL or similar and only contains plain text, and NOT a nice looking website)
|
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
|
||||||
- Many Useful™ plugins - [List](https://github.com/Vendicated/Vencord/tree/main/src/plugins)
|
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
||||||
- Experiments
|
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
||||||
- Proper context isolation -> Works in newer Electron versions (Confirmed working on versions 13-22)
|
- Works in all Electron versions (Confirmed working on versions 13-23)
|
||||||
- Inline patches: Patch Discord's code with regex replacements! See [the experiments plugin](src/plugins/experiments.ts) for an example. While being more complex, this is more powerful than monkey patching since you can patch only small parts of functions instead of fully replacing them, access non exported/local variables and even replace constants (like in the aforementioned experiments patch!)
|
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
||||||
|
|
||||||
## Installing / Uninstalling
|
## Installing / Uninstalling
|
||||||
|
|
||||||
Read [Megu's Installation Guide!](docs/1_INSTALLING.md)
|
[![Download and run the Installer ](https://img.shields.io/github/v/release/Vencord/Installer?label=Download%20Vencord%20Installer&style=for-the-badge)](https://github.com/Vencord/Installer#usage)
|
||||||
|
|
||||||
## Installing on Browser
|
## Installing on Browser
|
||||||
|
|
||||||
Run the same commands as in the regular install method. Now run
|
[![Get it on the Firefox Webstore](https://blog.mozilla.org/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [![Get it on the Chrome Webstore](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
|
||||||
|
|
||||||
```sh
|
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that QuickCSS and plugins making use of external resources will not work with the UserScript.
|
||||||
pnpm buildWeb
|
|
||||||
```
|
|
||||||
|
|
||||||
You will find the built extension at dist/extension.zip. Now just install this extension in your Browser
|
## Building from Source
|
||||||
|
|
||||||
## Installing Plugins
|
See the docs folder
|
||||||
|
|
||||||
Vencord comes with a bunch of plugins out of the box!
|
|
||||||
However, if you want to install your own ones, create a `userplugins` folder in the `src` directory and create or clone your plugins in there.
|
|
||||||
Don't forget to rebuild!
|
|
||||||
|
|
||||||
Want to learn how to create your own plugin, and maybe PR it into Vencord? See the [Contributing](#contributing) section below!
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
107
browser/GMPolyfill.js
Normal file
107
browser/GMPolyfill.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fetchOptions(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const opt = {
|
||||||
|
method: "OPTIONS",
|
||||||
|
url: url,
|
||||||
|
};
|
||||||
|
opt.onload = resp => resolve(resp.responseHeaders);
|
||||||
|
opt.ontimeout = () => reject("fetch timeout");
|
||||||
|
opt.onerror = () => reject("fetch error");
|
||||||
|
opt.onabort = () => reject("fetch abort");
|
||||||
|
GM_xmlhttpRequest(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHeaders(headers) {
|
||||||
|
if (!headers)
|
||||||
|
return {};
|
||||||
|
const result = {};
|
||||||
|
const headersArr = headers.trim().split("\n");
|
||||||
|
for (var i = 0; i < headersArr.length; i++) {
|
||||||
|
var row = headersArr[i];
|
||||||
|
var index = row.indexOf(":")
|
||||||
|
, key = row.slice(0, index).trim().toLowerCase()
|
||||||
|
, value = row.slice(index + 1).trim();
|
||||||
|
|
||||||
|
if (result[key] === undefined) {
|
||||||
|
result[key] = value;
|
||||||
|
} else if (Array.isArray(result[key])) {
|
||||||
|
result[key].push(value);
|
||||||
|
} else {
|
||||||
|
result[key] = [result[key], value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if CORS permits request
|
||||||
|
async function checkCors(url, method) {
|
||||||
|
const headers = parseHeaders(await fetchOptions(url));
|
||||||
|
|
||||||
|
const origin = headers["access-control-allow-origin"];
|
||||||
|
if (origin !== "*" && origin !== window.location.origin) return false;
|
||||||
|
|
||||||
|
const methods = headers["access-control-allow-methods"]?.split(/,\s/g);
|
||||||
|
if (methods && !methods.includes(method)) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function blobTo(to, blob) {
|
||||||
|
if (to === "arrayBuffer" && blob.arrayBuffer) return blob.arrayBuffer();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
var fileReader = new FileReader();
|
||||||
|
fileReader.onload = event => resolve(event.target.result);
|
||||||
|
if (to === "arrayBuffer") fileReader.readAsArrayBuffer(blob);
|
||||||
|
else if (to === "text") fileReader.readAsText(blob, "utf-8");
|
||||||
|
else reject("unknown to");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function GM_fetch(url, opt) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
checkCors(url, opt?.method || "GET")
|
||||||
|
.then(can => {
|
||||||
|
if (can) {
|
||||||
|
// https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest
|
||||||
|
const options = opt || {};
|
||||||
|
options.url = url;
|
||||||
|
options.data = options.body;
|
||||||
|
options.responseType = "blob";
|
||||||
|
options.onload = resp => {
|
||||||
|
var blob = resp.response;
|
||||||
|
resp.blob = () => Promise.resolve(blob);
|
||||||
|
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
|
||||||
|
resp.text = () => blobTo("text", blob);
|
||||||
|
resp.json = async () => JSON.parse(await blobTo("text", blob));
|
||||||
|
resolve(resp);
|
||||||
|
};
|
||||||
|
options.ontimeout = () => reject("fetch timeout");
|
||||||
|
options.onerror = () => reject("fetch error");
|
||||||
|
options.onabort = () => reject("fetch abort");
|
||||||
|
GM_xmlhttpRequest(options);
|
||||||
|
} else {
|
||||||
|
reject("CORS issue");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export const fetch = GM_fetch;
|
@ -16,8 +16,8 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import IpcEvents from "../src/utils/IpcEvents";
|
|
||||||
import * as DataStore from "../src/api/DataStore";
|
import * as DataStore from "../src/api/DataStore";
|
||||||
|
import IpcEvents from "../src/utils/IpcEvents";
|
||||||
|
|
||||||
// Discord deletes this so need to store in variable
|
// Discord deletes this so need to store in variable
|
||||||
const { localStorage } = window;
|
const { localStorage } = window;
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
if (typeof browser === "undefined") {
|
|
||||||
var browser = chrome;
|
|
||||||
}
|
|
||||||
|
|
||||||
browser.webRequest.onHeadersReceived.addListener(({ responseHeaders, url }) => {
|
|
||||||
const cspIdx = responseHeaders.findIndex(h => h.name === "content-security-policy");
|
|
||||||
if (cspIdx !== -1)
|
|
||||||
responseHeaders.splice(cspIdx, 1);
|
|
||||||
|
|
||||||
if (url.endsWith(".css")) {
|
|
||||||
const contentType = responseHeaders.find(h => h.name === "content-type");
|
|
||||||
if (contentType)
|
|
||||||
contentType.value = "text/css";
|
|
||||||
else
|
|
||||||
responseHeaders.push({
|
|
||||||
name: "content-type",
|
|
||||||
value: "text/json"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
responseHeaders
|
|
||||||
};
|
|
||||||
}, { urls: ["*://*.discord.com/*"] }, ["blocking", "responseHeaders"]);
|
|
@ -2,7 +2,18 @@ if (typeof browser === "undefined") {
|
|||||||
var browser = chrome;
|
var browser = chrome;
|
||||||
}
|
}
|
||||||
|
|
||||||
var script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
script.src = browser.runtime.getURL("dist/Vencord.js");
|
script.src = browser.runtime.getURL("dist/Vencord.js");
|
||||||
// documentElement because we load before body/head are ready
|
|
||||||
document.documentElement.appendChild(script);
|
const style = document.createElement("link");
|
||||||
|
style.type = "text/css";
|
||||||
|
style.rel = "stylesheet";
|
||||||
|
style.href = browser.runtime.getURL("dist/Vencord.css");
|
||||||
|
|
||||||
|
document.documentElement.append(script);
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
"DOMContentLoaded",
|
||||||
|
() => document.documentElement.append(style),
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
BIN
browser/icon.png
Normal file
BIN
browser/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
@ -1,32 +1,51 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 3,
|
||||||
|
"minimum_chrome_version": "91",
|
||||||
|
|
||||||
"name": "Vencord Web",
|
"name": "Vencord Web",
|
||||||
"description": "Yeee",
|
"description": "The cutest Discord mod now in your browser",
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Vendicated",
|
"author": "Vendicated",
|
||||||
"homepage_url": "https://github.com/Vendicated/Vencord",
|
"homepage_url": "https://github.com/Vendicated/Vencord",
|
||||||
"background": {
|
"icons": {
|
||||||
"scripts": [
|
"128": "icon.png"
|
||||||
"background.js"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"host_permissions": [
|
||||||
|
"*://*.discord.com/*",
|
||||||
|
"https://raw.githubusercontent.com/*"
|
||||||
|
],
|
||||||
|
|
||||||
|
"permissions": ["declarativeNetRequest"],
|
||||||
|
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"matches": [
|
"matches": ["*://*.discord.com/*"],
|
||||||
"*://*.discord.com/*"
|
"js": ["content.js"]
|
||||||
],
|
|
||||||
"js": [
|
|
||||||
"content.js"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"permissions": [
|
|
||||||
"*://*.discord.com/*",
|
|
||||||
"webRequest",
|
|
||||||
"webRequestBlocking"
|
|
||||||
],
|
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
"dist/Vencord.js"
|
{
|
||||||
|
"resources": ["dist/Vencord.js", "dist/Vencord.css"],
|
||||||
|
"matches": ["*://*.discord.com/*"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"declarative_net_request": {
|
||||||
|
"rule_resources": [
|
||||||
|
{
|
||||||
|
"id": "modifyResponseHeaders",
|
||||||
|
"enabled": true,
|
||||||
|
"path": "modifyResponseHeaders.json"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "vencord-firefox@vendicated.dev",
|
||||||
|
"strict_min_version": "109.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
38
browser/modifyResponseHeaders.json
Normal file
38
browser/modifyResponseHeaders.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"action": {
|
||||||
|
"type": "modifyHeaders",
|
||||||
|
"responseHeaders": [
|
||||||
|
{
|
||||||
|
"header": "content-security-policy",
|
||||||
|
"operation": "remove"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": "content-security-policy-report-only",
|
||||||
|
"operation": "remove"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"condition": {
|
||||||
|
"resourceTypes": ["main_frame"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"action": {
|
||||||
|
"type": "modifyHeaders",
|
||||||
|
"responseHeaders": [
|
||||||
|
{
|
||||||
|
"header": "content-type",
|
||||||
|
"operation": "set",
|
||||||
|
"value": "text/css"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"condition": {
|
||||||
|
"resourceTypes": ["stylesheet"],
|
||||||
|
"urlFilter": "https://raw.githubusercontent.com/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -7,7 +7,7 @@
|
|||||||
// @supportURL https://github.com/Vendicated/Vencord
|
// @supportURL https://github.com/Vendicated/Vencord
|
||||||
// @license GPL-3.0
|
// @license GPL-3.0
|
||||||
// @match *://*.discord.com/*
|
// @match *://*.discord.com/*
|
||||||
// @grant none
|
// @grant GM_xmlhttpRequest
|
||||||
// @run-at document-start
|
// @run-at document-start
|
||||||
// @compatible chrome Chrome + Tampermonkey or Violentmonkey
|
// @compatible chrome Chrome + Tampermonkey or Violentmonkey
|
||||||
// @compatible firefox Firefox Tampermonkey
|
// @compatible firefox Firefox Tampermonkey
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
// FIXME: Delete this soon, for now it is needed so people can update
|
|
||||||
|
|
||||||
import("./scripts/build/build.mjs");
|
|
@ -1,3 +1,6 @@
|
|||||||
|
> **Warning**
|
||||||
|
> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead.
|
||||||
|
|
||||||
# Installation Guide
|
# Installation Guide
|
||||||
|
|
||||||
Welcome to Megu's Installation Guide! In this file, you will learn about how to download, install, and uninstall Vencord!
|
Welcome to Megu's Installation Guide! In this file, you will learn about how to download, install, and uninstall Vencord!
|
||||||
@ -180,7 +183,6 @@ In `index.js`:
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
|
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
|
||||||
require("../app.asar");
|
|
||||||
```
|
```
|
||||||
|
|
||||||
And in `package.json`:
|
And in `package.json`:
|
||||||
|
@ -15,7 +15,7 @@ You don't need to run `pnpm build` every time you make a change. Instead, use `p
|
|||||||
3. In `index.ts`, copy-paste the following template code:
|
3. In `index.ts`, copy-paste the following template code:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import definePlugin from "../../utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Epic Plugin",
|
name: "Epic Plugin",
|
||||||
|
71
package.json
71
package.json
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.0.0",
|
"version": "1.0.7",
|
||||||
"description": "A Discord client mod that does things differently",
|
"description": "The cutest Discord client mod",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
@ -20,32 +20,75 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build/build.mjs",
|
"build": "node scripts/build/build.mjs",
|
||||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||||
"inject": "node scripts/patcher/install.js",
|
"inject": "node scripts/runInstaller.mjs",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
|
"lint-styles": "stylelint \"src/**/*.css\"",
|
||||||
"lint:fix": "pnpm lint --fix",
|
"lint:fix": "pnpm lint --fix",
|
||||||
"test": "pnpm lint && pnpm build && pnpm testTsc",
|
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc",
|
||||||
|
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
||||||
"testTsc": "tsc --noEmit",
|
"testTsc": "tsc --noEmit",
|
||||||
"uninject": "node scripts/patcher/uninstall.js",
|
"uninject": "node scripts/runInstaller.mjs",
|
||||||
"watch": "node scripts/build/build.mjs --watch"
|
"watch": "node scripts/build/build.mjs --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"console-menu": "^0.1.0",
|
"@vap/core": "0.0.12",
|
||||||
|
"@vap/shiki": "0.10.3",
|
||||||
"fflate": "^0.7.4"
|
"fflate": "^0.7.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^18.7.13",
|
"@types/diff": "^5.0.2",
|
||||||
"@types/react": "^18.0.17",
|
"@types/lodash": "^4.14.191",
|
||||||
|
"@types/node": "^18.11.18",
|
||||||
|
"@types/react": "^18.0.27",
|
||||||
|
"@types/react-dom": "^18.0.10",
|
||||||
"@types/yazl": "^2.4.2",
|
"@types/yazl": "^2.4.2",
|
||||||
"@typescript-eslint/parser": "^5.39.0",
|
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||||
|
"@typescript-eslint/parser": "^5.49.0",
|
||||||
|
"diff": "^5.1.0",
|
||||||
"discord-types": "^1.3.26",
|
"discord-types": "^1.3.26",
|
||||||
"esbuild": "^0.15.5",
|
"esbuild": "^0.15.18",
|
||||||
"eslint": "^8.24.0",
|
"eslint": "^8.28.0",
|
||||||
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"eslint-plugin-header": "^3.1.1",
|
"eslint-plugin-header": "^3.1.1",
|
||||||
|
"eslint-plugin-path-alias": "^1.0.0",
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
|
"highlight.js": "10.6.0",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"puppeteer-core": "^19.6.0",
|
||||||
"standalone-electron-types": "^1.0.0",
|
"standalone-electron-types": "^1.0.0",
|
||||||
"type-fest": "^3.1.0",
|
"stylelint": "^14.16.1",
|
||||||
"typescript": "^4.8.4"
|
"stylelint-config-standard": "^29.0.0",
|
||||||
|
"type-fest": "^3.5.3",
|
||||||
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@7.13.4"
|
"packageManager": "pnpm@7.13.4",
|
||||||
|
"pnpm": {
|
||||||
|
"patchedDependencies": {
|
||||||
|
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
||||||
|
"eslint@8.28.0": "patches/eslint@8.28.0.patch"
|
||||||
|
},
|
||||||
|
"peerDependencyRules": {
|
||||||
|
"ignoreMissing": [
|
||||||
|
"eslint-plugin-import",
|
||||||
|
"eslint"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"allowedDeprecatedVersions": {
|
||||||
|
"source-map-resolve": "*",
|
||||||
|
"resolve-url": "*",
|
||||||
|
"source-map-url": "*",
|
||||||
|
"urix": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webExt": {
|
||||||
|
"artifactsDir": "./dist",
|
||||||
|
"build": {
|
||||||
|
"overwriteDest": true
|
||||||
|
},
|
||||||
|
"sourceDir": "./dist/extension-v2-unpacked"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13
patches/eslint-plugin-path-alias@1.0.0.patch
Normal file
13
patches/eslint-plugin-path-alias@1.0.0.patch
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
diff --git a/lib/rules/no-relative.js b/lib/rules/no-relative.js
|
||||||
|
index 71594c83f1f4f733ffcc6047d7f7084348335dbe..d8623d87c89499c442171db3272cba07c9efabbe 100644
|
||||||
|
--- a/lib/rules/no-relative.js
|
||||||
|
+++ b/lib/rules/no-relative.js
|
||||||
|
@@ -41,7 +41,7 @@ module.exports = {
|
||||||
|
ImportDeclaration(node) {
|
||||||
|
const importPath = node.source.value;
|
||||||
|
|
||||||
|
- if (!/^(\.?\.\/)/.test(importPath)) {
|
||||||
|
+ if (!/^(\.\.\/)/.test(importPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
45
patches/eslint@8.28.0.patch
Normal file
45
patches/eslint@8.28.0.patch
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
diff --git a/lib/rules/no-useless-escape.js b/lib/rules/no-useless-escape.js
|
||||||
|
index 2046a148a17fd1d5f3a4bbc9f45f7700259d11fa..f4898c6b57355a4fd72c43a9f32bf1a36a6ccf4a 100644
|
||||||
|
--- a/lib/rules/no-useless-escape.js
|
||||||
|
+++ b/lib/rules/no-useless-escape.js
|
||||||
|
@@ -97,12 +97,30 @@ module.exports = {
|
||||||
|
escapeBackslash: "Replace the `\\` with `\\\\` to include the actual backslash character."
|
||||||
|
},
|
||||||
|
|
||||||
|
- schema: []
|
||||||
|
+ schema: [{
|
||||||
|
+ type: "object",
|
||||||
|
+ properties: {
|
||||||
|
+ extra: {
|
||||||
|
+ type: "string",
|
||||||
|
+ default: ""
|
||||||
|
+ },
|
||||||
|
+ extraCharClass: {
|
||||||
|
+ type: "string",
|
||||||
|
+ default: ""
|
||||||
|
+ },
|
||||||
|
+ },
|
||||||
|
+ additionalProperties: false
|
||||||
|
+ }]
|
||||||
|
},
|
||||||
|
|
||||||
|
create(context) {
|
||||||
|
+ const options = context.options[0] || {};
|
||||||
|
+ const { extra, extraCharClass } = options || ''
|
||||||
|
const sourceCode = context.getSourceCode();
|
||||||
|
|
||||||
|
+ const NON_CHARCLASS_ESCAPES = union(REGEX_NON_CHARCLASS_ESCAPES, new Set(extra))
|
||||||
|
+ const CHARCLASS_ESCAPES = union(REGEX_GENERAL_ESCAPES, new Set(extraCharClass))
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* Reports a node
|
||||||
|
* @param {ASTNode} node The node to report
|
||||||
|
@@ -238,7 +256,7 @@ module.exports = {
|
||||||
|
.filter(charInfo => charInfo.escaped)
|
||||||
|
|
||||||
|
// Filter out characters that are valid to escape, based on their position in the regular expression.
|
||||||
|
- .filter(charInfo => !(charInfo.inCharClass ? REGEX_GENERAL_ESCAPES : REGEX_NON_CHARCLASS_ESCAPES).has(charInfo.text))
|
||||||
|
+ .filter(charInfo => !(charInfo.inCharClass ? CHARCLASS_ESCAPES : NON_CHARCLASS_ESCAPES).has(charInfo.text))
|
||||||
|
|
||||||
|
// Report all the remaining characters.
|
||||||
|
.forEach(charInfo => report(node, charInfo.index, charInfo.text));
|
2029
pnpm-lock.yaml
generated
2029
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -19,22 +19,17 @@
|
|||||||
|
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
|
|
||||||
import { commonOpts, gitHash, globPlugins, isStandalone } from "./common.mjs";
|
import { commonOpts, globPlugins, isStandalone, watch } from "./common.mjs";
|
||||||
|
|
||||||
const defines = {
|
const defines = {
|
||||||
IS_STANDALONE: isStandalone
|
IS_STANDALONE: isStandalone,
|
||||||
|
IS_DEV: JSON.stringify(watch)
|
||||||
};
|
};
|
||||||
if (defines.IS_STANDALONE === "false")
|
if (defines.IS_STANDALONE === "false")
|
||||||
// If this is a local build (not standalone), optimise
|
// If this is a local build (not standalone), optimise
|
||||||
// for the specific platform we're on
|
// for the specific platform we're on
|
||||||
defines["process.platform"] = JSON.stringify(process.platform);
|
defines["process.platform"] = JSON.stringify(process.platform);
|
||||||
|
|
||||||
const header = `
|
|
||||||
// Vencord ${gitHash}
|
|
||||||
// Standalone: ${defines.IS_STANDALONE}
|
|
||||||
// Platform: ${defines["process.platform"] || "Universal"}
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.BuildOptions}
|
* @type {esbuild.BuildOptions}
|
||||||
*/
|
*/
|
||||||
@ -47,25 +42,25 @@ const nodeCommonOpts = {
|
|||||||
bundle: true,
|
bundle: true,
|
||||||
external: ["electron", ...commonOpts.external],
|
external: ["electron", ...commonOpts.external],
|
||||||
define: defines,
|
define: defines,
|
||||||
banner: {
|
|
||||||
js: header
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
|
||||||
|
const sourcemap = watch ? "inline" : "external";
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...nodeCommonOpts,
|
...nodeCommonOpts,
|
||||||
entryPoints: ["src/preload.ts"],
|
entryPoints: ["src/preload.ts"],
|
||||||
outfile: "dist/preload.js",
|
outfile: "dist/preload.js",
|
||||||
footer: { js: "//# sourceURL=VencordPreload\n//# sourceMappingURL=vencord://preload.js.map" },
|
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
|
||||||
sourcemap: "external",
|
sourcemap,
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...nodeCommonOpts,
|
...nodeCommonOpts,
|
||||||
entryPoints: ["src/patcher.ts"],
|
entryPoints: ["src/patcher.ts"],
|
||||||
outfile: "dist/patcher.js",
|
outfile: "dist/patcher.js",
|
||||||
footer: { js: "//# sourceURL=VencordPatcher\n//# sourceMappingURL=vencord://patcher.js.map" },
|
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
|
||||||
sourcemap: "external",
|
sourcemap,
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOpts,
|
...commonOpts,
|
||||||
@ -73,16 +68,16 @@ await Promise.all([
|
|||||||
outfile: "dist/renderer.js",
|
outfile: "dist/renderer.js",
|
||||||
format: "iife",
|
format: "iife",
|
||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
footer: { js: "//# sourceURL=VencordRenderer\n//# sourceMappingURL=vencord://renderer.js.map" },
|
footer: { js: "//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
|
||||||
globalName: "Vencord",
|
globalName: "Vencord",
|
||||||
sourcemap: "external",
|
sourcemap,
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins,
|
globPlugins,
|
||||||
...commonOpts.plugins
|
...commonOpts.plugins
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
IS_WEB: "false",
|
...defines,
|
||||||
IS_STANDALONE: isStandalone
|
IS_WEB: false
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
]).catch(err => {
|
]).catch(err => {
|
||||||
|
103
scripts/build/buildWeb.mjs
Executable file → Normal file
103
scripts/build/buildWeb.mjs
Executable file → Normal file
@ -20,13 +20,13 @@
|
|||||||
|
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
import { zip } from "fflate";
|
import { zip } from "fflate";
|
||||||
import { readFileSync, writeFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { readFile } from "fs/promises";
|
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
// wtf is this assert syntax
|
// wtf is this assert syntax
|
||||||
import PackageJSON from "../../package.json" assert { type: "json" };
|
import PackageJSON from "../../package.json" assert { type: "json" };
|
||||||
import { commonOpts, fileIncludePlugin, gitHashPlugin, gitRemotePlugin, globPlugins } from "./common.mjs";
|
import { commonOpts, globPlugins, watch } from "./common.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.BuildOptions}
|
* @type {esbuild.BuildOptions}
|
||||||
@ -39,14 +39,13 @@ const commonOptions = {
|
|||||||
external: ["plugins", "git-hash"],
|
external: ["plugins", "git-hash"],
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins,
|
globPlugins,
|
||||||
gitHashPlugin,
|
...commonOpts.plugins,
|
||||||
gitRemotePlugin,
|
|
||||||
fileIncludePlugin
|
|
||||||
],
|
],
|
||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
define: {
|
define: {
|
||||||
IS_WEB: "true",
|
IS_WEB: "true",
|
||||||
IS_STANDALONE: "true"
|
IS_STANDALONE: "true",
|
||||||
|
IS_DEV: JSON.stringify(watch)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -59,32 +58,88 @@ await Promise.all(
|
|||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
|
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
|
||||||
|
define: {
|
||||||
|
"window": "unsafeWindow",
|
||||||
|
...(commonOptions?.define)
|
||||||
|
},
|
||||||
outfile: "dist/Vencord.user.js",
|
outfile: "dist/Vencord.user.js",
|
||||||
banner: {
|
banner: {
|
||||||
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", PackageJSON.version)
|
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${PackageJSON.version}.${new Date().getTime()}`)
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
|
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
|
||||||
js: "Object.defineProperty(window,'Vencord',{get:()=>Vencord});"
|
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
zip({
|
/**
|
||||||
dist: {
|
* @type {(target: string, files: string[], shouldZip: boolean) => Promise<void>}
|
||||||
"Vencord.js": readFileSync("dist/browser.js")
|
*/
|
||||||
},
|
async function buildPluginZip(target, files, shouldZip) {
|
||||||
...Object.fromEntries(await Promise.all(["background.js", "content.js", "manifest.json"].map(async f => [
|
const entries = {
|
||||||
f,
|
"dist/Vencord.js": await readFile("dist/browser.js"),
|
||||||
await readFile(join("browser", f))
|
"dist/Vencord.css": await readFile("dist/browser.css"),
|
||||||
]))),
|
...Object.fromEntries(await Promise.all(files.map(async f => {
|
||||||
}, {}, (err, data) => {
|
let content = await readFile(join("browser", f));
|
||||||
if (err) {
|
if (f.startsWith("manifest")) {
|
||||||
console.error(err);
|
const json = JSON.parse(content.toString("utf-8"));
|
||||||
process.exitCode = 1;
|
json.version = PackageJSON.version;
|
||||||
} else {
|
content = new TextEncoder().encode(JSON.stringify(json));
|
||||||
writeFileSync("dist/extension.zip", data);
|
|
||||||
console.info("Extension written to dist/extension.zip");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
f.startsWith("manifest") ? "manifest.json" : f,
|
||||||
|
content
|
||||||
|
];
|
||||||
|
}))),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shouldZip) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
zip(entries, {}, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
const out = join("dist", target);
|
||||||
|
writeFile(out, data).then(() => {
|
||||||
|
console.info("Extension written to " + out);
|
||||||
|
resolve();
|
||||||
|
}).catch(reject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await rm(target, { recursive: true, force: true });
|
||||||
|
await Promise.all(Object.entries(entries).map(async ([file, content]) => {
|
||||||
|
const dest = join("dist", target, file);
|
||||||
|
const parentDirectory = join(dest, "..");
|
||||||
|
await mkdir(parentDirectory, { recursive: true });
|
||||||
|
await writeFile(dest, content);
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.info("Unpacked Extension written to dist/" + target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => {
|
||||||
|
const cssRuntime = `
|
||||||
|
;document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(
|
||||||
|
Object.assign(document.createElement("style"), {
|
||||||
|
textContent: \`${content.replaceAll("`", "\\`")}\`,
|
||||||
|
id: "vencord-css-core"
|
||||||
|
})
|
||||||
|
), { once: true });
|
||||||
|
`;
|
||||||
|
|
||||||
|
return appendFile("dist/Vencord.user.js", cssRuntime);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
appendCssRuntime,
|
||||||
|
buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true),
|
||||||
|
buildPluginZip("extension-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
|
||||||
|
]);
|
||||||
|
|
||||||
|
@ -17,18 +17,25 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { exec, execSync } from "child_process";
|
import { exec, execSync } from "child_process";
|
||||||
import esbuild from "esbuild";
|
import { existsSync, readFileSync } from "fs";
|
||||||
import { existsSync } from "fs";
|
|
||||||
import { readdir, readFile } from "fs/promises";
|
import { readdir, readFile } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join, relative } from "path";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
export const watch = process.argv.includes("--watch");
|
export const watch = process.argv.includes("--watch");
|
||||||
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
||||||
|
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
||||||
|
export const banner = {
|
||||||
|
js: `
|
||||||
|
// Vencord ${gitHash}
|
||||||
|
// Standalone: ${isStandalone}
|
||||||
|
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
|
||||||
|
`.trim()
|
||||||
|
};
|
||||||
|
|
||||||
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
export const makeAllPackagesExternalPlugin = {
|
export const makeAllPackagesExternalPlugin = {
|
||||||
name: "make-all-packages-external",
|
name: "make-all-packages-external",
|
||||||
@ -39,7 +46,7 @@ export const makeAllPackagesExternalPlugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
export const globPlugins = {
|
export const globPlugins = {
|
||||||
name: "glob-plugins",
|
name: "glob-plugins",
|
||||||
@ -61,11 +68,12 @@ export const globPlugins = {
|
|||||||
if (!existsSync(`./src/${dir}`)) continue;
|
if (!existsSync(`./src/${dir}`)) continue;
|
||||||
const files = await readdir(`./src/${dir}`);
|
const files = await readdir(`./src/${dir}`);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
if (file.startsWith(".")) continue;
|
||||||
if (file === "index.ts") {
|
if (file === "index.ts") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const mod = `p${i}`;
|
const mod = `p${i}`;
|
||||||
code += `import ${mod} from "./${dir}/${file.replace(/.tsx?$/, "")}";\n`;
|
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
|
||||||
plugins += `[${mod}.name]:${mod},\n`;
|
plugins += `[${mod}.name]:${mod},\n`;
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
@ -79,9 +87,8 @@ export const globPlugins = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
export const gitHashPlugin = {
|
export const gitHashPlugin = {
|
||||||
name: "git-hash-plugin",
|
name: "git-hash-plugin",
|
||||||
@ -97,7 +104,7 @@ export const gitHashPlugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
export const gitRemotePlugin = {
|
export const gitRemotePlugin = {
|
||||||
name: "git-remote-plugin",
|
name: "git-remote-plugin",
|
||||||
@ -119,7 +126,7 @@ export const gitRemotePlugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
export const fileIncludePlugin = {
|
export const fileIncludePlugin = {
|
||||||
name: "file-include-plugin",
|
name: "file-include-plugin",
|
||||||
@ -141,8 +148,33 @@ export const fileIncludePlugin = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styleModule = readFileSync("./scripts/build/module/style.js", "utf-8");
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.BuildOptions}
|
* @type {import("esbuild").Plugin}
|
||||||
|
*/
|
||||||
|
export const stylePlugin = {
|
||||||
|
name: "style-plugin",
|
||||||
|
setup: ({ onResolve, onLoad }) => {
|
||||||
|
onResolve({ filter: /\.css\?managed$/, namespace: "file" }, ({ path, resolveDir }) => ({
|
||||||
|
path: relative(process.cwd(), join(resolveDir, path.replace("?managed", ""))),
|
||||||
|
namespace: "managed-style",
|
||||||
|
}));
|
||||||
|
onLoad({ filter: /\.css$/, namespace: "managed-style" }, async ({ path }) => {
|
||||||
|
const css = await readFile(path, "utf-8");
|
||||||
|
const name = relative(process.cwd(), path).replaceAll("\\", "/");
|
||||||
|
|
||||||
|
return {
|
||||||
|
loader: "js",
|
||||||
|
contents: styleModule
|
||||||
|
.replaceAll("STYLE_SOURCE", JSON.stringify(css))
|
||||||
|
.replaceAll("STYLE_NAME", JSON.stringify(name))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("esbuild").BuildOptions}
|
||||||
*/
|
*/
|
||||||
export const commonOpts = {
|
export const commonOpts = {
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
@ -151,6 +183,12 @@ export const commonOpts = {
|
|||||||
minify: !watch,
|
minify: !watch,
|
||||||
sourcemap: watch ? "inline" : "",
|
sourcemap: watch ? "inline" : "",
|
||||||
legalComments: "linked",
|
legalComments: "linked",
|
||||||
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin],
|
banner,
|
||||||
external: ["~plugins", "~git-hash", "~git-remote"]
|
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
||||||
|
external: ["~plugins", "~git-hash", "~git-remote"],
|
||||||
|
inject: ["./scripts/build/inject/react.mjs"],
|
||||||
|
jsxFactory: "VencordCreateElement",
|
||||||
|
jsxFragment: "VencordFragment",
|
||||||
|
// Work around https://github.com/evanw/esbuild/issues/2460
|
||||||
|
tsconfig: "./scripts/build/tsconfig.esbuild.json"
|
||||||
};
|
};
|
||||||
|
21
scripts/build/inject/react.mjs
Normal file
21
scripts/build/inject/react.mjs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment");
|
||||||
|
export let VencordCreateElement =
|
||||||
|
(...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);
|
@ -16,18 +16,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Devs } from "../utils/constants";
|
(window.VencordStyles ??= new Map()).set(STYLE_NAME, {
|
||||||
import definePlugin from "../utils/types";
|
name: STYLE_NAME,
|
||||||
|
source: STYLE_SOURCE,
|
||||||
export default definePlugin({
|
classNames: {},
|
||||||
name: "SilentTyping",
|
dom: null,
|
||||||
authors: [Devs.Ven],
|
|
||||||
description: "Hide that you are typing",
|
|
||||||
patches: [{
|
|
||||||
find: "startTyping:",
|
|
||||||
replacement: {
|
|
||||||
match: /startTyping:.+?,stop/,
|
|
||||||
replace: "startTyping:()=>{},stop"
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default STYLE_NAME;
|
7
scripts/build/tsconfig.esbuild.json
Normal file
7
scripts/build/tsconfig.esbuild.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Work around https://github.com/evanw/esbuild/issues/2460
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react"
|
||||||
|
}
|
||||||
|
}
|
20
scripts/checkNodeVersion.js
Normal file
20
scripts/checkNodeVersion.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (Number(process.versions.node.split(".")[0]) < 18)
|
||||||
|
throw `Your node version (${process.version}) is too old, please update to v18 or higher https://nodejs.org/en/download/`;
|
62
scripts/genPluginList.js
Normal file
62
scripts/genPluginList.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// A script to automatically generate a list of all plugins.
|
||||||
|
// Just copy paste the entire file into a running Vencord install and it will prompt you
|
||||||
|
// to save the file
|
||||||
|
|
||||||
|
// eslint-disable-next-line spaced-comment
|
||||||
|
/// <reference types="../src/modules"/>
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
/**
|
||||||
|
* @type {typeof import("~plugins").default}
|
||||||
|
*/
|
||||||
|
const Plugins = Vencord.Plugins.plugins;
|
||||||
|
|
||||||
|
const header = `
|
||||||
|
<!-- This file is auto generated, do not edit -->
|
||||||
|
|
||||||
|
# Vencord Plugins
|
||||||
|
`;
|
||||||
|
|
||||||
|
let tableOfContents = "\n\n";
|
||||||
|
|
||||||
|
let list = "\n\n";
|
||||||
|
|
||||||
|
for (const p of Object.values(Plugins).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||||
|
tableOfContents += `- [${p.name}](#${p.name.replaceAll(" ", "-")})\n`;
|
||||||
|
|
||||||
|
list += `## ${p.name}
|
||||||
|
|
||||||
|
${p.description}
|
||||||
|
|
||||||
|
**Authors**: ${p.authors.map(a => a.name).join(", ")}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (p.commands?.length) {
|
||||||
|
list += "\n\n#### Commands\n";
|
||||||
|
for (const cmd of p.commands) {
|
||||||
|
list += `${cmd.name} - ${cmd.description}\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list += "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(header + tableOfContents + list);
|
||||||
|
})();
|
@ -1,341 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const path = require("path");
|
|
||||||
const readline = require("readline");
|
|
||||||
const fs = require("fs");
|
|
||||||
const menu = require("console-menu");
|
|
||||||
|
|
||||||
const BRANCH_NAMES = [
|
|
||||||
"Discord",
|
|
||||||
"DiscordPTB",
|
|
||||||
"DiscordCanary",
|
|
||||||
"DiscordDevelopment",
|
|
||||||
"discord",
|
|
||||||
"discordptb",
|
|
||||||
"discordcanary",
|
|
||||||
"discorddevelopment",
|
|
||||||
"discord-ptb",
|
|
||||||
"discord-canary",
|
|
||||||
"discord-development",
|
|
||||||
// Flatpak
|
|
||||||
"com.discordapp.Discord",
|
|
||||||
"com.discordapp.DiscordPTB",
|
|
||||||
"com.discordapp.DiscordCanary",
|
|
||||||
"com.discordapp.DiscordDevelopment",
|
|
||||||
];
|
|
||||||
|
|
||||||
const MACOS_DISCORD_DIRS = [
|
|
||||||
"Discord.app",
|
|
||||||
"Discord PTB.app",
|
|
||||||
"Discord Canary.app",
|
|
||||||
"Discord Development.app",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (process.platform === "linux" && process.env.SUDO_USER) {
|
|
||||||
process.env.HOME = fs
|
|
||||||
.readFileSync("/etc/passwd", "utf-8")
|
|
||||||
.match(new RegExp(`^${process.env.SUDO_USER}.+$`, "m"))[0]
|
|
||||||
.split(":")[5];
|
|
||||||
}
|
|
||||||
|
|
||||||
const LINUX_DISCORD_DIRS = [
|
|
||||||
"/usr/share",
|
|
||||||
"/usr/lib64",
|
|
||||||
"/opt",
|
|
||||||
`${process.env.HOME}/.local/share`,
|
|
||||||
"/var/lib/flatpak/app",
|
|
||||||
`${process.env.HOME}/.local/share/flatpak/app`,
|
|
||||||
];
|
|
||||||
|
|
||||||
const FLATPAK_NAME_MAPPING = {
|
|
||||||
DiscordCanary: "discord-canary",
|
|
||||||
DiscordPTB: "discord-ptb",
|
|
||||||
DiscordDevelopment: "discord-development",
|
|
||||||
Discord: "discord",
|
|
||||||
};
|
|
||||||
|
|
||||||
const ENTRYPOINT = path
|
|
||||||
.join(process.cwd(), "dist", "patcher.js")
|
|
||||||
.replace(/\\/g, "/");
|
|
||||||
|
|
||||||
function question(question) {
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout,
|
|
||||||
terminal: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise(resolve => {
|
|
||||||
rl.question(question, answer => {
|
|
||||||
rl.close();
|
|
||||||
resolve(answer);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMenuItem(installations) {
|
|
||||||
const menuItems = installations.map(info => ({
|
|
||||||
title: info.patched ? "[MODIFIED] " + info.location : info.location,
|
|
||||||
info,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const result = await menu(
|
|
||||||
[
|
|
||||||
...menuItems,
|
|
||||||
{ title: "Specify custom path", info: "custom" },
|
|
||||||
{ title: "Exit without patching", exit: true }
|
|
||||||
],
|
|
||||||
{
|
|
||||||
header: "Select a Discord installation to patch:",
|
|
||||||
border: true,
|
|
||||||
helpMessage:
|
|
||||||
"Use the up/down arrow keys to select an option. " +
|
|
||||||
"Press ENTER to confirm.",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!result || !result.info || result.exit) {
|
|
||||||
console.log("No installation selected.");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.info === "custom") {
|
|
||||||
const customPath = await question("Please enter the path: ");
|
|
||||||
if (!customPath || !fs.existsSync(customPath)) {
|
|
||||||
console.log("No such Path or not specifed.");
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceDir = path.join(customPath, "resources");
|
|
||||||
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
|
||||||
console.log("Unsupported Install. resources/app.asar not found");
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
const appDir = path.join(resourceDir, "app");
|
|
||||||
result.info = {
|
|
||||||
branch: "unknown",
|
|
||||||
patched: fs.existsSync(appDir),
|
|
||||||
location: customPath,
|
|
||||||
versions: [{
|
|
||||||
path: appDir,
|
|
||||||
name: null
|
|
||||||
}],
|
|
||||||
arch: process.platform === "linux" ? "linux" : "win32",
|
|
||||||
isFlatpak: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.info.patched) {
|
|
||||||
const answer = await question(
|
|
||||||
"This installation has already been modified. Overwrite? [Y/n]: "
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!["y", "yes", "yeah", ""].includes(answer.toLowerCase())) {
|
|
||||||
console.log("Not patching.");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.info;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWindowsDirs() {
|
|
||||||
const dirs = [];
|
|
||||||
for (const dir of fs.readdirSync(process.env.LOCALAPPDATA)) {
|
|
||||||
if (!BRANCH_NAMES.includes(dir)) continue;
|
|
||||||
|
|
||||||
const location = path.join(process.env.LOCALAPPDATA, dir);
|
|
||||||
if (!fs.statSync(location).isDirectory()) continue;
|
|
||||||
|
|
||||||
const appDirs = fs
|
|
||||||
.readdirSync(location, { withFileTypes: true })
|
|
||||||
.filter(file => file.isDirectory())
|
|
||||||
.filter(file => file.name.startsWith("app-"))
|
|
||||||
.map(file => path.join(location, file.name));
|
|
||||||
|
|
||||||
const versions = [];
|
|
||||||
let patched = false;
|
|
||||||
|
|
||||||
for (const fqAppDir of appDirs) {
|
|
||||||
const resourceDir = path.join(fqAppDir, "resources");
|
|
||||||
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const appDir = path.join(resourceDir, "app");
|
|
||||||
if (fs.existsSync(appDir)) {
|
|
||||||
patched = true;
|
|
||||||
}
|
|
||||||
versions.push({
|
|
||||||
path: appDir,
|
|
||||||
name: /app-([0-9.]+)/.exec(fqAppDir)[1],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appDirs.length) {
|
|
||||||
dirs.push({
|
|
||||||
branch: dir,
|
|
||||||
patched,
|
|
||||||
location,
|
|
||||||
versions,
|
|
||||||
arch: "win32",
|
|
||||||
flatpak: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dirs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDarwinDirs() {
|
|
||||||
const dirs = [];
|
|
||||||
for (const dir of fs.readdirSync("/Applications")) {
|
|
||||||
if (!MACOS_DISCORD_DIRS.includes(dir)) continue;
|
|
||||||
|
|
||||||
const location = path.join("/Applications", dir, "Contents");
|
|
||||||
if (!fs.existsSync(location)) continue;
|
|
||||||
if (!fs.statSync(location).isDirectory()) continue;
|
|
||||||
|
|
||||||
const appDirs = fs
|
|
||||||
.readdirSync(location, { withFileTypes: true })
|
|
||||||
.filter(file => file.isDirectory())
|
|
||||||
.filter(file => file.name.startsWith("Resources"))
|
|
||||||
.map(file => path.join(location, file.name));
|
|
||||||
|
|
||||||
const versions = [];
|
|
||||||
let patched = false;
|
|
||||||
|
|
||||||
for (const resourceDir of appDirs) {
|
|
||||||
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const appDir = path.join(resourceDir, "app");
|
|
||||||
if (fs.existsSync(appDir)) {
|
|
||||||
patched = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
versions.push({
|
|
||||||
path: appDir,
|
|
||||||
name: null, // MacOS installs have no version number
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appDirs.length) {
|
|
||||||
dirs.push({
|
|
||||||
branch: dir,
|
|
||||||
patched,
|
|
||||||
location,
|
|
||||||
versions,
|
|
||||||
arch: "win32",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dirs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLinuxDirs() {
|
|
||||||
const dirs = [];
|
|
||||||
for (const dir of LINUX_DISCORD_DIRS) {
|
|
||||||
if (!fs.existsSync(dir)) continue;
|
|
||||||
for (const branch of fs.readdirSync(dir)) {
|
|
||||||
if (!BRANCH_NAMES.includes(branch)) continue;
|
|
||||||
|
|
||||||
const location = path.join(dir, branch);
|
|
||||||
if (!fs.statSync(location).isDirectory()) continue;
|
|
||||||
|
|
||||||
const isFlatpak = location.includes("/flatpak/");
|
|
||||||
|
|
||||||
let appDirs = [];
|
|
||||||
|
|
||||||
if (isFlatpak) {
|
|
||||||
const fqDir = path.join(location, "current", "active", "files");
|
|
||||||
if (!/com\.discordapp\.(\w+)\//.test(fqDir)) continue;
|
|
||||||
const branchName = /com\.discordapp\.(\w+)\//.exec(fqDir)[1];
|
|
||||||
if (!Object.keys(FLATPAK_NAME_MAPPING).includes(branchName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const appDir = path.join(
|
|
||||||
fqDir,
|
|
||||||
FLATPAK_NAME_MAPPING[branchName]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fs.existsSync(appDir)) continue;
|
|
||||||
if (!fs.statSync(appDir).isDirectory()) continue;
|
|
||||||
|
|
||||||
const resourceDir = path.join(appDir, "resources");
|
|
||||||
|
|
||||||
appDirs.push(resourceDir);
|
|
||||||
} else {
|
|
||||||
appDirs = fs
|
|
||||||
.readdirSync(location, { withFileTypes: true })
|
|
||||||
.filter(file => file.isDirectory())
|
|
||||||
.filter(
|
|
||||||
file =>
|
|
||||||
file.name.startsWith("app-") ||
|
|
||||||
file.name === "resources"
|
|
||||||
)
|
|
||||||
.map(file => path.join(location, file.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
const versions = [];
|
|
||||||
let patched = false;
|
|
||||||
|
|
||||||
for (const resourceDir of appDirs) {
|
|
||||||
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const appDir = path.join(resourceDir, "app");
|
|
||||||
if (fs.existsSync(appDir)) {
|
|
||||||
patched = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const version = /app-([0-9.]+)/.exec(resourceDir);
|
|
||||||
|
|
||||||
versions.push({
|
|
||||||
path: appDir,
|
|
||||||
name: version && version.length > 1 ? version[1] : null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appDirs.length) {
|
|
||||||
dirs.push({
|
|
||||||
branch,
|
|
||||||
patched,
|
|
||||||
location,
|
|
||||||
versions,
|
|
||||||
arch: "linux",
|
|
||||||
isFlatpak,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dirs;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
BRANCH_NAMES,
|
|
||||||
MACOS_DISCORD_DIRS,
|
|
||||||
LINUX_DISCORD_DIRS,
|
|
||||||
FLATPAK_NAME_MAPPING,
|
|
||||||
ENTRYPOINT,
|
|
||||||
question,
|
|
||||||
getMenuItem,
|
|
||||||
getWindowsDirs,
|
|
||||||
getDarwinDirs,
|
|
||||||
getLinuxDirs,
|
|
||||||
};
|
|
@ -1,130 +0,0 @@
|
|||||||
#!/usr/bin/node
|
|
||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
|
||||||
const { execSync } = require("child_process");
|
|
||||||
|
|
||||||
console.log("\nVencord Installer\n");
|
|
||||||
|
|
||||||
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
|
|
||||||
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(path.join(process.cwd(), "dist", "patcher.js"))) {
|
|
||||||
console.log("You need to build the project first. Run:", "pnpm build");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
getMenuItem,
|
|
||||||
getWindowsDirs,
|
|
||||||
getDarwinDirs,
|
|
||||||
getLinuxDirs,
|
|
||||||
ENTRYPOINT,
|
|
||||||
} = require("./common");
|
|
||||||
|
|
||||||
switch (process.platform) {
|
|
||||||
case "win32":
|
|
||||||
install(getWindowsDirs());
|
|
||||||
break;
|
|
||||||
case "darwin":
|
|
||||||
install(getDarwinDirs());
|
|
||||||
break;
|
|
||||||
case "linux":
|
|
||||||
install(getLinuxDirs());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("Unknown OS");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function install(installations) {
|
|
||||||
const selected = await getMenuItem(installations);
|
|
||||||
|
|
||||||
// Attempt to give flatpak perms
|
|
||||||
if (selected.isFlatpak) {
|
|
||||||
try {
|
|
||||||
const { branch } = selected;
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const globalCmd = `flatpak override ${branch} --filesystem=${cwd}`;
|
|
||||||
const userCmd = `flatpak override --user ${branch} --filesystem=${cwd}`;
|
|
||||||
const cmd = selected.location.startsWith("/home")
|
|
||||||
? userCmd
|
|
||||||
: globalCmd;
|
|
||||||
execSync(cmd);
|
|
||||||
console.log("Successfully gave write perms to Discord Flatpak.");
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Failed to give write perms to Discord Flatpak.");
|
|
||||||
console.log(
|
|
||||||
"Try running this script as an administrator:",
|
|
||||||
"sudo pnpm inject"
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const version of selected.versions) {
|
|
||||||
const dir = version.path;
|
|
||||||
// Check if we have write perms to the install directory...
|
|
||||||
try {
|
|
||||||
fs.accessSync(selected.location, fs.constants.W_OK);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("No write access to", selected.location);
|
|
||||||
console.error(
|
|
||||||
"Try running this script as an administrator:",
|
|
||||||
"sudo pnpm inject"
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (fs.existsSync(dir) && fs.lstatSync(dir).isDirectory()) {
|
|
||||||
fs.rmSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(dir)) {
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(dir, "index.js"),
|
|
||||||
`require("${ENTRYPOINT}");`
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(dir, "package.json"),
|
|
||||||
JSON.stringify({
|
|
||||||
name: "discord",
|
|
||||||
main: "index.js",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const requiredFiles = ["index.js", "package.json"];
|
|
||||||
|
|
||||||
if (requiredFiles.every(f => fs.existsSync(path.join(dir, f)))) {
|
|
||||||
console.log(
|
|
||||||
"Successfully patched",
|
|
||||||
version.name
|
|
||||||
? `${selected.branch} ${version.name}`
|
|
||||||
: selected.branch
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log("Failed to patch", dir);
|
|
||||||
console.log("Files in directory:", fs.readdirSync(dir));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
#!/usr/bin/node
|
|
||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
console.log("\nVencord Uninstaller\n");
|
|
||||||
|
|
||||||
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
|
|
||||||
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
getMenuItem,
|
|
||||||
getWindowsDirs,
|
|
||||||
getDarwinDirs,
|
|
||||||
getLinuxDirs,
|
|
||||||
} = require("./common");
|
|
||||||
|
|
||||||
switch (process.platform) {
|
|
||||||
case "win32":
|
|
||||||
uninstall(getWindowsDirs());
|
|
||||||
break;
|
|
||||||
case "darwin":
|
|
||||||
uninstall(getDarwinDirs());
|
|
||||||
break;
|
|
||||||
case "linux":
|
|
||||||
uninstall(getLinuxDirs());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("Unknown OS");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uninstall(installations) {
|
|
||||||
const selected = await getMenuItem(installations);
|
|
||||||
|
|
||||||
for (const version of selected.versions) {
|
|
||||||
const dir = version.path;
|
|
||||||
// Check if we have write perms to the install directory...
|
|
||||||
try {
|
|
||||||
fs.accessSync(selected.location, fs.constants.W_OK);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("No write access to", selected.location);
|
|
||||||
console.error(
|
|
||||||
"Try running this script as an administrator:",
|
|
||||||
"sudo pnpm uninject"
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (fs.existsSync(dir)) {
|
|
||||||
fs.rmSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
"Successfully unpatched",
|
|
||||||
version.name
|
|
||||||
? `${selected.branch} ${version.name}`
|
|
||||||
: selected.branch
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
128
scripts/runInstaller.mjs
Normal file
128
scripts/runInstaller.mjs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./checkNodeVersion.js";
|
||||||
|
|
||||||
|
import { execFileSync, execSync } from "child_process";
|
||||||
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
|
import { dirname, join } from "path";
|
||||||
|
import { Readable } from "stream";
|
||||||
|
import { finished } from "stream/promises";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const BASE_URL = "https://github.com/Vencord/Installer/releases/latest/download/";
|
||||||
|
const INSTALLER_PATH_DARWIN = "VencordInstaller.app/Contents/MacOS/VencordInstaller";
|
||||||
|
|
||||||
|
const BASE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const FILE_DIR = join(BASE_DIR, "dist", "Installer");
|
||||||
|
const ETAG_FILE = join(FILE_DIR, "etag.txt");
|
||||||
|
|
||||||
|
function getFilename() {
|
||||||
|
switch (process.platform) {
|
||||||
|
case "win32":
|
||||||
|
return "VencordInstaller.exe";
|
||||||
|
case "darwin":
|
||||||
|
return "VencordInstaller.MacOS.zip";
|
||||||
|
case "linux":
|
||||||
|
return "VencordInstaller-" + (process.env.WAYLAND_DISPLAY ? "wayland" : "x11");
|
||||||
|
default:
|
||||||
|
throw new Error("Unsupported platform: " + process.platform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureBinary() {
|
||||||
|
const filename = getFilename();
|
||||||
|
console.log("Downloading " + filename);
|
||||||
|
|
||||||
|
mkdirSync(FILE_DIR, { recursive: true });
|
||||||
|
|
||||||
|
const downloadName = join(FILE_DIR, filename);
|
||||||
|
const outputFile = process.platform === "darwin"
|
||||||
|
? join(FILE_DIR, "VencordInstaller")
|
||||||
|
: downloadName;
|
||||||
|
|
||||||
|
const etag = existsSync(outputFile) && existsSync(ETAG_FILE)
|
||||||
|
? readFileSync(ETAG_FILE, "utf-8")
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const res = await fetch(BASE_URL + filename, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Vencord (https://github.com/Vendicated/Vencord)",
|
||||||
|
"If-None-Match": etag
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 304) {
|
||||||
|
console.log("Up to date, not redownloading!");
|
||||||
|
return outputFile;
|
||||||
|
}
|
||||||
|
if (!res.ok)
|
||||||
|
throw new Error(`Failed to download installer: ${res.status} ${res.statusText}`);
|
||||||
|
|
||||||
|
writeFileSync(ETAG_FILE, res.headers.get("etag"));
|
||||||
|
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
console.log("Unzipping...");
|
||||||
|
const zip = new Uint8Array(await res.arrayBuffer());
|
||||||
|
|
||||||
|
const ff = await import("fflate");
|
||||||
|
const bytes = ff.unzipSync(zip, {
|
||||||
|
filter: f => f.name === INSTALLER_PATH_DARWIN
|
||||||
|
})[INSTALLER_PATH_DARWIN];
|
||||||
|
|
||||||
|
writeFileSync(outputFile, bytes, { mode: 0o755 });
|
||||||
|
|
||||||
|
console.log("Overriding security policy for installer binary (this is required to run it)");
|
||||||
|
console.log("xattr might error, that's okay");
|
||||||
|
|
||||||
|
const logAndRun = cmd => {
|
||||||
|
console.log("Running", cmd);
|
||||||
|
try {
|
||||||
|
execSync(cmd);
|
||||||
|
} catch { }
|
||||||
|
};
|
||||||
|
logAndRun(`sudo spctl --add '${outputFile}' --label "Vencord Installer"`);
|
||||||
|
logAndRun(`sudo xattr -d com.apple.quarantine '${outputFile}'`);
|
||||||
|
} else {
|
||||||
|
// WHY DOES NODE FETCH RETURN A WEB STREAM OH MY GOD
|
||||||
|
const body = Readable.fromWeb(res.body);
|
||||||
|
await finished(body.pipe(createWriteStream(outputFile, {
|
||||||
|
mode: 0o755,
|
||||||
|
autoClose: true
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Finished downloading!");
|
||||||
|
|
||||||
|
return outputFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const installerBin = await ensureBinary();
|
||||||
|
|
||||||
|
console.log("Now running Installer...");
|
||||||
|
|
||||||
|
execFileSync(installerBin, {
|
||||||
|
stdio: "inherit",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
VENCORD_USER_DATA_DIR: BASE_DIR,
|
||||||
|
VENCORD_DEV_INSTALL: "1"
|
||||||
|
}
|
||||||
|
});
|
@ -22,19 +22,17 @@ export * as Util from "./utils";
|
|||||||
export * as QuickCss from "./utils/quickCss";
|
export * as QuickCss from "./utils/quickCss";
|
||||||
export * as Updater from "./utils/updater";
|
export * as Updater from "./utils/updater";
|
||||||
export * as Webpack from "./webpack";
|
export * as Webpack from "./webpack";
|
||||||
|
export { PlainSettings, Settings };
|
||||||
|
|
||||||
|
import "./utils/quickCss";
|
||||||
|
import "./webpack/patchWebpack";
|
||||||
|
|
||||||
import { popNotice, showNotice } from "./api/Notices";
|
import { popNotice, showNotice } from "./api/Notices";
|
||||||
import { PlainSettings,Settings } from "./api/settings";
|
import { PlainSettings, Settings } from "./api/settings";
|
||||||
import { startAllPlugins } from "./plugins";
|
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
||||||
|
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
|
||||||
export { PlainSettings,Settings };
|
|
||||||
|
|
||||||
import "./webpack/patchWebpack";
|
|
||||||
import "./utils/quickCss";
|
|
||||||
|
|
||||||
import { checkForUpdates, UpdateLogger } from "./utils/updater";
|
|
||||||
import { onceReady } from "./webpack";
|
import { onceReady } from "./webpack";
|
||||||
import { Router } from "./webpack/common";
|
import { SettingsRouter } from "./webpack/common";
|
||||||
|
|
||||||
export let Components: any;
|
export let Components: any;
|
||||||
|
|
||||||
@ -46,21 +44,54 @@ async function init() {
|
|||||||
if (!IS_WEB) {
|
if (!IS_WEB) {
|
||||||
try {
|
try {
|
||||||
const isOutdated = await checkForUpdates();
|
const isOutdated = await checkForUpdates();
|
||||||
if (isOutdated && Settings.notifyAboutUpdates)
|
if (!isOutdated) return;
|
||||||
|
|
||||||
|
if (Settings.autoUpdate) {
|
||||||
|
await update();
|
||||||
|
const needsFullRestart = await rebuild();
|
||||||
|
setTimeout(() => {
|
||||||
|
showNotice(
|
||||||
|
"Vencord has been updated!",
|
||||||
|
"Restart",
|
||||||
|
() => {
|
||||||
|
if (needsFullRestart)
|
||||||
|
window.DiscordNative.app.relaunch();
|
||||||
|
else
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, 10_000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.notifyAboutUpdates)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showNotice(
|
showNotice(
|
||||||
"A Vencord update is available!",
|
"A Vencord update is available!",
|
||||||
"View Update",
|
"View Update",
|
||||||
() => {
|
() => {
|
||||||
popNotice();
|
popNotice();
|
||||||
Router.open("VencordUpdater");
|
SettingsRouter.open("VencordUpdater");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, 10000);
|
}, 10_000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
UpdateLogger.error("Failed to check for updates", err);
|
UpdateLogger.error("Failed to check for updates", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IS_DEV) {
|
||||||
|
const pendingPatches = patches.filter(p => !p.all && p.predicate?.() !== false);
|
||||||
|
if (pendingPatches.length)
|
||||||
|
PMLogger.warn(
|
||||||
|
"Webpack has finished initialising, but some patches haven't been applied yet.",
|
||||||
|
"This might be expected since some Modules are lazy loaded, but please verify",
|
||||||
|
"that all plugins are working as intended.",
|
||||||
|
"You are seeing this warning because this is a Development build of Vencord.",
|
||||||
|
"\nThe following patches have not been applied:",
|
||||||
|
"\n\n" + pendingPatches.map(p => `${p.plugin}: ${p.find}`).join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
@ -16,10 +16,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import IPC_EVENTS from "@utils/IpcEvents";
|
||||||
import { IpcRenderer, ipcRenderer } from "electron";
|
import { IpcRenderer, ipcRenderer } from "electron";
|
||||||
|
|
||||||
import IPC_EVENTS from "./utils/IpcEvents";
|
|
||||||
|
|
||||||
function assertEventAllowed(event: string) {
|
function assertEventAllowed(event: string) {
|
||||||
if (!(event in IPC_EVENTS)) throw new Error(`Event ${event} not allowed.`);
|
if (!(event in IPC_EVENTS)) throw new Error(`Event ${event} not allowed.`);
|
||||||
}
|
}
|
||||||
|
107
src/api/Badges.ts
Normal file
107
src/api/Badges.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { User } from "discord-types/general";
|
||||||
|
import { ComponentType, HTMLProps } from "react";
|
||||||
|
|
||||||
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
|
export enum BadgePosition {
|
||||||
|
START,
|
||||||
|
END
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProfileBadge {
|
||||||
|
/** The tooltip to show on hover. Required for image badges */
|
||||||
|
tooltip?: string;
|
||||||
|
/** Custom component for the badge (tooltip not included) */
|
||||||
|
component?: ComponentType<ProfileBadge & BadgeUserArgs>;
|
||||||
|
/** The custom image to use */
|
||||||
|
image?: string;
|
||||||
|
/** Action to perform when you click the badge */
|
||||||
|
onClick?(): void;
|
||||||
|
/** Should the user display this badge? */
|
||||||
|
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
||||||
|
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
||||||
|
props?: HTMLProps<HTMLImageElement>;
|
||||||
|
/** Insert at start or end? */
|
||||||
|
position?: BadgePosition;
|
||||||
|
/** The badge name to display, Discord uses this. Required for component badges */
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Badges = new Set<ProfileBadge>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new badge with the Badges API
|
||||||
|
* @param badge The badge to register
|
||||||
|
*/
|
||||||
|
export function addBadge(badge: ProfileBadge) {
|
||||||
|
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
|
||||||
|
Badges.add(badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister a badge from the Badges API
|
||||||
|
* @param badge The badge to remove
|
||||||
|
*/
|
||||||
|
export function removeBadge(badge: ProfileBadge) {
|
||||||
|
return Badges.delete(badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject badges into the profile badges array.
|
||||||
|
* You probably don't need to use this.
|
||||||
|
*/
|
||||||
|
export function inject(badgeArray: ProfileBadge[], args: BadgeUserArgs) {
|
||||||
|
for (const badge of Badges) {
|
||||||
|
if (!badge.shouldShow || badge.shouldShow(args)) {
|
||||||
|
badge.position === BadgePosition.START
|
||||||
|
? badgeArray.unshift({ ...badge, ...args })
|
||||||
|
: badgeArray.push({ ...badge, ...args });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id);
|
||||||
|
|
||||||
|
return badgeArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BadgeUserArgs {
|
||||||
|
user: User;
|
||||||
|
profile: Profile;
|
||||||
|
premiumSince: Date;
|
||||||
|
premiumGuildSince?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConnectedAccount {
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
verified: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Profile {
|
||||||
|
connectedAccounts: ConnectedAccount[];
|
||||||
|
premiumType: number;
|
||||||
|
premiumSince: string;
|
||||||
|
premiumGuildSince?: any;
|
||||||
|
lastFetched: number;
|
||||||
|
profileFetchFailed: boolean;
|
||||||
|
application?: any;
|
||||||
|
}
|
@ -16,18 +16,16 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { mergeDefaults } from "@utils/misc";
|
||||||
|
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||||
|
import { SnowflakeUtils } from "@webpack/common";
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
import type { PartialDeep } from "type-fest";
|
import type { PartialDeep } from "type-fest";
|
||||||
|
|
||||||
import { lazyWebpack, mergeDefaults } from "../../utils/misc";
|
|
||||||
import { filters, waitFor } from "../../webpack";
|
|
||||||
import { Argument } from "./types";
|
import { Argument } from "./types";
|
||||||
|
|
||||||
const createBotMessage = lazyWebpack(filters.byCode('username:"Clyde"'));
|
const createBotMessage = findByCodeLazy('username:"Clyde"');
|
||||||
const MessageSender = lazyWebpack(filters.byProps(["receiveMessage"]));
|
const MessageSender = findByPropsLazy("receiveMessage");
|
||||||
|
|
||||||
let SnowflakeUtils: any;
|
|
||||||
waitFor("fromTimestamp", m => SnowflakeUtils = m);
|
|
||||||
|
|
||||||
export function generateId() {
|
export function generateId() {
|
||||||
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
|
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
|
||||||
|
@ -16,9 +16,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { makeCodeblock } from "../../utils/misc";
|
import { makeCodeblock } from "@utils/misc";
|
||||||
import { generateId, sendBotMessage } from "./commandHelpers";
|
|
||||||
import { ApplicationCommandInputType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
|
import { sendBotMessage } from "./commandHelpers";
|
||||||
|
import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
|
||||||
|
|
||||||
export * from "./commandHelpers";
|
export * from "./commandHelpers";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
@ -79,7 +80,12 @@ export const _handleCommand = function (cmd: Command, args: Argument[], ctx: Com
|
|||||||
}
|
}
|
||||||
} as never;
|
} as never;
|
||||||
|
|
||||||
function modifyOpt(opt: Option | Command) {
|
|
||||||
|
/**
|
||||||
|
* Prepare a Command Option for Discord by filling missing fields
|
||||||
|
* @param opt
|
||||||
|
*/
|
||||||
|
export function prepareOption<O extends Option | Command>(opt: O): O {
|
||||||
opt.displayName ||= opt.name;
|
opt.displayName ||= opt.name;
|
||||||
opt.displayDescription ||= opt.description;
|
opt.displayDescription ||= opt.description;
|
||||||
opt.options?.forEach((opt, i, opts) => {
|
opt.options?.forEach((opt, i, opts) => {
|
||||||
@ -88,11 +94,36 @@ function modifyOpt(opt: Option | Command) {
|
|||||||
else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;
|
else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;
|
||||||
opt.choices?.forEach(x => x.displayName ||= x.name);
|
opt.choices?.forEach(x => x.displayName ||= x.name);
|
||||||
|
|
||||||
modifyOpt(opts[i]);
|
prepareOption(opts[i]);
|
||||||
|
});
|
||||||
|
return opt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yes, Discord registers individual commands for each subcommand
|
||||||
|
// TODO: This probably doesn't support nested subcommands. If that is ever needed,
|
||||||
|
// investigate
|
||||||
|
function registerSubCommands(cmd: Command, plugin: string) {
|
||||||
|
cmd.options?.forEach(o => {
|
||||||
|
if (o.type !== ApplicationCommandOptionType.SUB_COMMAND)
|
||||||
|
throw new Error("When specifying sub-command options, all options must be sub-commands.");
|
||||||
|
const subCmd = {
|
||||||
|
...cmd,
|
||||||
|
...o,
|
||||||
|
type: ApplicationCommandType.CHAT_INPUT,
|
||||||
|
name: `${cmd.name} ${o.name}`,
|
||||||
|
displayName: `${cmd.name} ${o.name}`,
|
||||||
|
subCommandPath: [{
|
||||||
|
name: o.name,
|
||||||
|
type: o.type,
|
||||||
|
displayName: o.name
|
||||||
|
}],
|
||||||
|
rootCommand: cmd
|
||||||
|
};
|
||||||
|
registerCommand(subCmd as any, plugin);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerCommand(command: Command, plugin: string) {
|
export function registerCommand<C extends Command>(command: C, plugin: string) {
|
||||||
if (!BUILT_IN) {
|
if (!BUILT_IN) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[CommandsAPI]",
|
"[CommandsAPI]",
|
||||||
@ -106,13 +137,19 @@ export function registerCommand(command: Command, plugin: string) {
|
|||||||
throw new Error(`Command '${command.name}' already exists.`);
|
throw new Error(`Command '${command.name}' already exists.`);
|
||||||
|
|
||||||
command.isVencordCommand = true;
|
command.isVencordCommand = true;
|
||||||
command.id ??= generateId();
|
command.id ??= `-${BUILT_IN.length + 1}`;
|
||||||
command.applicationId ??= "-1"; // BUILT_IN;
|
command.applicationId ??= "-1"; // BUILT_IN;
|
||||||
command.type ??= ApplicationCommandType.CHAT_INPUT;
|
command.type ??= ApplicationCommandType.CHAT_INPUT;
|
||||||
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
|
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
|
||||||
command.plugin ||= plugin;
|
command.plugin ||= plugin;
|
||||||
|
|
||||||
modifyOpt(command);
|
prepareOption(command);
|
||||||
|
|
||||||
|
if (command.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND) {
|
||||||
|
registerSubCommands(command, plugin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
commands[command.name] = command;
|
commands[command.name] = command;
|
||||||
BUILT_IN.push(command);
|
BUILT_IN.push(command);
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,7 @@ export interface Argument {
|
|||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
focused: undefined;
|
focused: undefined;
|
||||||
|
options: Argument[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Command {
|
export interface Command {
|
||||||
|
65
src/api/MemberListDecorators.ts
Normal file
65
src/api/MemberListDecorators.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Channel, User } from "discord-types/general/index.js";
|
||||||
|
|
||||||
|
interface DecoratorProps {
|
||||||
|
activities: any[];
|
||||||
|
canUseAvatarDecorations: boolean;
|
||||||
|
channel: Channel;
|
||||||
|
/**
|
||||||
|
* Only for DM members
|
||||||
|
*/
|
||||||
|
channelName?: string;
|
||||||
|
/**
|
||||||
|
* Only for server members
|
||||||
|
*/
|
||||||
|
currentUser?: User;
|
||||||
|
guildId?: string;
|
||||||
|
isMobile: boolean;
|
||||||
|
isOwner?: boolean;
|
||||||
|
isTyping: boolean;
|
||||||
|
selected: boolean;
|
||||||
|
status: string;
|
||||||
|
user: User;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
export type Decorator = (props: DecoratorProps) => JSX.Element | null;
|
||||||
|
type OnlyIn = "guilds" | "dms";
|
||||||
|
|
||||||
|
export const decorators = new Map<string, { decorator: Decorator, onlyIn?: OnlyIn; }>();
|
||||||
|
|
||||||
|
export function addDecorator(identifier: string, decorator: Decorator, onlyIn?: OnlyIn) {
|
||||||
|
decorators.set(identifier, { decorator, onlyIn });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeDecorator(identifier: string) {
|
||||||
|
decorators.delete(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function __addDecoratorsToList(props: DecoratorProps): (JSX.Element | null)[] {
|
||||||
|
const isInGuild = !!(props.guildId);
|
||||||
|
return [...decorators.values()].map(decoratorObj => {
|
||||||
|
const { decorator, onlyIn } = decoratorObj;
|
||||||
|
// this can most likely be done cleaner
|
||||||
|
if (!onlyIn || (onlyIn === "guilds" && isInGuild) || (onlyIn === "dms" && !isInGuild)) {
|
||||||
|
return decorator(props);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type AccessoryCallback = (props: Record<string, any>) => JSX.Element;
|
export type AccessoryCallback = (props: Record<string, any>) => JSX.Element | null | Array<JSX.Element | null>;
|
||||||
export type Accessory = {
|
export type Accessory = {
|
||||||
callback: AccessoryCallback;
|
callback: AccessoryCallback;
|
||||||
position?: number;
|
position?: number;
|
||||||
@ -44,6 +44,15 @@ export function _modifyAccessories(
|
|||||||
props: Record<string, any>
|
props: Record<string, any>
|
||||||
) {
|
) {
|
||||||
for (const accessory of accessories.values()) {
|
for (const accessory of accessories.values()) {
|
||||||
|
let accessories = accessory.callback(props);
|
||||||
|
if (accessories == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!Array.isArray(accessories))
|
||||||
|
accessories = [accessories];
|
||||||
|
else if (accessories.length === 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
elements.splice(
|
elements.splice(
|
||||||
accessory.position != null
|
accessory.position != null
|
||||||
? accessory.position < 0
|
? accessory.position < 0
|
||||||
@ -51,7 +60,7 @@ export function _modifyAccessories(
|
|||||||
: accessory.position
|
: accessory.position
|
||||||
: elements.length,
|
: elements.length,
|
||||||
0,
|
0,
|
||||||
accessory.callback(props)
|
...accessories.filter(e => e != null) as JSX.Element[]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
63
src/api/MessageDecorations.ts
Normal file
63
src/api/MessageDecorations.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Channel, Message } from "discord-types/general/index.js";
|
||||||
|
|
||||||
|
interface DecorationProps {
|
||||||
|
author: {
|
||||||
|
/**
|
||||||
|
* Will be username if the user has no nickname
|
||||||
|
*/
|
||||||
|
nick: string;
|
||||||
|
iconRoleId: string;
|
||||||
|
guildMemberAvatar: string;
|
||||||
|
colorRoleName: string;
|
||||||
|
colorString: string;
|
||||||
|
};
|
||||||
|
channel: Channel;
|
||||||
|
compact: boolean;
|
||||||
|
decorations: {
|
||||||
|
/**
|
||||||
|
* Element for the [BOT] tag if there is one
|
||||||
|
*/
|
||||||
|
0: JSX.Element | null;
|
||||||
|
/**
|
||||||
|
* Other decorations (including ones added with this api)
|
||||||
|
*/
|
||||||
|
1: JSX.Element[];
|
||||||
|
};
|
||||||
|
message: Message;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
export type Decoration = (props: DecorationProps) => JSX.Element | null;
|
||||||
|
|
||||||
|
export const decorations = new Map<string, Decoration>();
|
||||||
|
|
||||||
|
export function addDecoration(identifier: string, decoration: Decoration) {
|
||||||
|
decorations.set(identifier, decoration);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeDecoration(identifier: string) {
|
||||||
|
decorations.delete(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function __addDecorationsToMessage(props: DecorationProps): (JSX.Element | null)[] {
|
||||||
|
return [...decorations.values()].map(decoration => {
|
||||||
|
return decoration(props);
|
||||||
|
});
|
||||||
|
}
|
@ -16,9 +16,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Channel,Message } from "discord-types/general";
|
import Logger from "@utils/Logger";
|
||||||
|
import { MessageStore } from "@webpack/common";
|
||||||
import Logger from "../utils/logger";
|
import type { Channel, Message } from "discord-types/general";
|
||||||
|
|
||||||
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
||||||
|
|
||||||
@ -37,25 +37,37 @@ export interface MessageObject {
|
|||||||
validNonShortcutEmojis: Emoji[];
|
validNonShortcutEmojis: Emoji[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: any) => void;
|
export interface MessageExtra {
|
||||||
|
stickerIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => void | { cancel: boolean; };
|
||||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
|
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
|
||||||
|
|
||||||
const sendListeners = new Set<SendListener>();
|
const sendListeners = new Set<SendListener>();
|
||||||
const editListeners = new Set<EditListener>();
|
const editListeners = new Set<EditListener>();
|
||||||
|
|
||||||
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: any) {
|
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
|
||||||
for (const listener of sendListeners) {
|
for (const listener of sendListeners) {
|
||||||
try {
|
try {
|
||||||
listener(channelId, messageObj, extra);
|
const result = listener(channelId, messageObj, extra);
|
||||||
} catch (e) { MessageEventsLogger.error(`MessageSendHandler: Listener encoutered an unknown error. (${e})`); }
|
if (result && result.cancel === true) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
MessageEventsLogger.error("MessageSendHandler: Listener encountered an unknown error\n", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _handlePreEdit(channeld: string, messageId: string, messageObj: MessageObject) {
|
export function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
|
||||||
for (const listener of editListeners) {
|
for (const listener of editListeners) {
|
||||||
try {
|
try {
|
||||||
listener(channeld, messageId, messageObj);
|
listener(channelId, messageId, messageObj);
|
||||||
} catch (e) { MessageEventsLogger.error(`MessageEditHandler: Listener encoutered an unknown error. (${e})`); }
|
} catch (e) {
|
||||||
|
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,10 +99,14 @@ type ClickListener = (message: Message, channel: Channel, event: MouseEvent) =>
|
|||||||
const listeners = new Set<ClickListener>();
|
const listeners = new Set<ClickListener>();
|
||||||
|
|
||||||
export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
|
export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
|
||||||
|
// message object may be outdated, so (try to) fetch latest one
|
||||||
|
message = MessageStore.getMessage(channel.id, message.id) ?? message;
|
||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
try {
|
try {
|
||||||
listener(message, channel, event);
|
listener(message, channel, event);
|
||||||
} catch (e) { MessageEventsLogger.error(`MessageClickHandler: Listener encoutered an unknown error. (${e})`); }
|
} catch (e) {
|
||||||
|
MessageEventsLogger.error("MessageClickHandler: Listener encountered an unknown error\n", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
69
src/api/MessagePopover.ts
Normal file
69
src/api/MessagePopover.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Logger from "@utils/Logger";
|
||||||
|
import { Channel, Message } from "discord-types/general";
|
||||||
|
import type { MouseEventHandler } from "react";
|
||||||
|
|
||||||
|
const logger = new Logger("MessagePopover");
|
||||||
|
|
||||||
|
export interface ButtonItem {
|
||||||
|
key?: string,
|
||||||
|
label: string,
|
||||||
|
icon: React.ComponentType<any>,
|
||||||
|
message: Message,
|
||||||
|
channel: Channel,
|
||||||
|
onClick?: MouseEventHandler<HTMLButtonElement>,
|
||||||
|
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type getButtonItem = (message: Message) => ButtonItem | null;
|
||||||
|
|
||||||
|
export const buttons = new Map<string, getButtonItem>();
|
||||||
|
|
||||||
|
export function addButton(
|
||||||
|
identifier: string,
|
||||||
|
item: getButtonItem,
|
||||||
|
) {
|
||||||
|
buttons.set(identifier, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeButton(identifier: string) {
|
||||||
|
buttons.delete(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function _buildPopoverElements(
|
||||||
|
msg: Message,
|
||||||
|
makeButton: (item: ButtonItem) => React.ComponentType
|
||||||
|
) {
|
||||||
|
const items = [] as React.ComponentType[];
|
||||||
|
|
||||||
|
for (const [identifier, getItem] of buttons.entries()) {
|
||||||
|
try {
|
||||||
|
const item = getItem(msg);
|
||||||
|
if (item) {
|
||||||
|
item.key ??= identifier;
|
||||||
|
items.push(makeButton(item));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`[${identifier}]`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { waitFor } from "../webpack";
|
import { waitFor } from "@webpack";
|
||||||
|
|
||||||
let NoticesModule: any;
|
let NoticesModule: any;
|
||||||
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
|
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
|
||||||
|
92
src/api/Notifications/NotificationComponent.tsx
Normal file
92
src/api/Notifications/NotificationComponent.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import { useSettings } from "@api/settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Forms, React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
|
||||||
|
|
||||||
|
import { NotificationData } from "./Notifications";
|
||||||
|
|
||||||
|
export default ErrorBoundary.wrap(function NotificationComponent({
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
richBody,
|
||||||
|
color,
|
||||||
|
icon,
|
||||||
|
onClick,
|
||||||
|
onClose,
|
||||||
|
image
|
||||||
|
}: NotificationData) {
|
||||||
|
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
|
||||||
|
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
|
||||||
|
|
||||||
|
const [isHover, setIsHover] = useState(false);
|
||||||
|
const [elapsed, setElapsed] = useState(0);
|
||||||
|
|
||||||
|
const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isHover || !hasFocus || timeout === 0) return void setElapsed(0);
|
||||||
|
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
if (elapsed >= timeout)
|
||||||
|
onClose!();
|
||||||
|
else
|
||||||
|
setElapsed(elapsed);
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [timeout, isHover, hasFocus]);
|
||||||
|
|
||||||
|
const timeoutProgress = elapsed / timeout;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="vc-notification-root"
|
||||||
|
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
|
||||||
|
onClick={onClick}
|
||||||
|
onContextMenu={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onClose!();
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setIsHover(true)}
|
||||||
|
onMouseLeave={() => setIsHover(false)}
|
||||||
|
>
|
||||||
|
<div className="vc-notification">
|
||||||
|
{icon && <img className="vc-notification-icon" src={icon} alt="" />}
|
||||||
|
<div className="vc-notification-content">
|
||||||
|
<Forms.FormTitle tag="h2">{title}</Forms.FormTitle>
|
||||||
|
<div>
|
||||||
|
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{image && <img className="vc-notification-img" src={image} alt="" />}
|
||||||
|
{timeout !== 0 && (
|
||||||
|
<div
|
||||||
|
className="vc-notification-progressbar"
|
||||||
|
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
99
src/api/Notifications/Notifications.tsx
Normal file
99
src/api/Notifications/Notifications.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Settings } from "@api/settings";
|
||||||
|
import { Queue } from "@utils/Queue";
|
||||||
|
import { ReactDOM } from "@webpack/common";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type { Root } from "react-dom/client";
|
||||||
|
|
||||||
|
import NotificationComponent from "./NotificationComponent";
|
||||||
|
|
||||||
|
const NotificationQueue = new Queue();
|
||||||
|
|
||||||
|
let reactRoot: Root;
|
||||||
|
let id = 42;
|
||||||
|
|
||||||
|
function getRoot() {
|
||||||
|
if (!reactRoot) {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.id = "vc-notification-container";
|
||||||
|
document.body.append(container);
|
||||||
|
reactRoot = ReactDOM.createRoot(container);
|
||||||
|
}
|
||||||
|
return reactRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationData {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
/**
|
||||||
|
* Same as body but can be a custom component.
|
||||||
|
* Will be used over body if present.
|
||||||
|
* Not supported on desktop notifications, those will fall back to body */
|
||||||
|
richBody?: ReactNode;
|
||||||
|
/** Small icon. This is for things like profile pictures and should be square */
|
||||||
|
icon?: string;
|
||||||
|
/** Large image. Optimally, this should be around 16x9 but it doesn't matter much. Desktop Notifications might not support this */
|
||||||
|
image?: string;
|
||||||
|
onClick?(): void;
|
||||||
|
onClose?(): void;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _showNotification(notification: NotificationData, id: number) {
|
||||||
|
const root = getRoot();
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
root.render(
|
||||||
|
<NotificationComponent key={id} {...notification} onClose={() => {
|
||||||
|
notification.onClose?.();
|
||||||
|
root.render(null);
|
||||||
|
resolve();
|
||||||
|
}} />,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldBeNative() {
|
||||||
|
const { useNative } = Settings.notifications;
|
||||||
|
if (useNative === "always") return true;
|
||||||
|
if (useNative === "not-focused") return !document.hasFocus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestPermission() {
|
||||||
|
return (
|
||||||
|
Notification.permission === "granted" ||
|
||||||
|
(Notification.permission !== "denied" && (await Notification.requestPermission()) === "granted")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showNotification(data: NotificationData) {
|
||||||
|
if (shouldBeNative() && await requestPermission()) {
|
||||||
|
const { title, body, icon, image, onClick = null, onClose = null } = data;
|
||||||
|
const n = new Notification(title, {
|
||||||
|
body,
|
||||||
|
icon,
|
||||||
|
image
|
||||||
|
});
|
||||||
|
n.onclick = onClick;
|
||||||
|
n.onclose = onClose;
|
||||||
|
} else {
|
||||||
|
NotificationQueue.push(() => _showNotification(data, id++));
|
||||||
|
}
|
||||||
|
}
|
19
src/api/Notifications/index.ts
Normal file
19
src/api/Notifications/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./Notifications";
|
49
src/api/Notifications/styles.css
Normal file
49
src/api/Notifications/styles.css
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
.vc-notification-root {
|
||||||
|
/* clear default button styles */
|
||||||
|
all: unset;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 25vw;
|
||||||
|
min-height: 10vh;
|
||||||
|
color: var(--text-normal);
|
||||||
|
background-color: var(--background-secondary-alt);
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2147483647;
|
||||||
|
right: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 1.25rem;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-icon {
|
||||||
|
height: 4rem;
|
||||||
|
width: 4rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Discord adding 3km margin to generic tags */
|
||||||
|
.vc-notification h2 {
|
||||||
|
margin: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-progressbar {
|
||||||
|
height: 0.25rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-p {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
line-height: 140%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
55
src/api/ServerList.ts
Normal file
55
src/api/ServerList.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Logger from "@utils/Logger";
|
||||||
|
|
||||||
|
const logger = new Logger("ServerListAPI");
|
||||||
|
|
||||||
|
export enum ServerListRenderPosition {
|
||||||
|
Above,
|
||||||
|
In,
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderFunctionsAbove = new Set<Function>();
|
||||||
|
const renderFunctionsIn = new Set<Function>();
|
||||||
|
|
||||||
|
function getRenderFunctions(position: ServerListRenderPosition) {
|
||||||
|
return position === ServerListRenderPosition.Above ? renderFunctionsAbove : renderFunctionsIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addServerListElement(position: ServerListRenderPosition, renderFunction: Function) {
|
||||||
|
getRenderFunctions(position).add(renderFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeServerListElement(position: ServerListRenderPosition, renderFunction: Function) {
|
||||||
|
getRenderFunctions(position).delete(renderFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderAll = (position: ServerListRenderPosition) => {
|
||||||
|
const ret: Array<JSX.Element> = [];
|
||||||
|
|
||||||
|
for (const renderFunction of getRenderFunctions(position)) {
|
||||||
|
try {
|
||||||
|
ret.unshift(renderFunction());
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to render server list element:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
};
|
162
src/api/Styles.ts
Normal file
162
src/api/Styles.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { MapValue } from "type-fest/source/entry";
|
||||||
|
|
||||||
|
export type Style = MapValue<typeof VencordStyles>;
|
||||||
|
|
||||||
|
export const styleMap = window.VencordStyles ??= new Map();
|
||||||
|
|
||||||
|
export function requireStyle(name: string) {
|
||||||
|
const style = styleMap.get(name);
|
||||||
|
if (!style) throw new Error(`Style "${name}" does not exist`);
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A style's name can be obtained from importing a stylesheet with `?managed` at the end of the import
|
||||||
|
* @param name The name of the style
|
||||||
|
* @returns `false` if the style was already enabled, `true` otherwise
|
||||||
|
* @example
|
||||||
|
* import pluginStyle from "./plugin.css?managed";
|
||||||
|
*
|
||||||
|
* // Inside some plugin method like "start()" or "[option].onChange()"
|
||||||
|
* enableStyle(pluginStyle);
|
||||||
|
*/
|
||||||
|
export function enableStyle(name: string) {
|
||||||
|
const style = requireStyle(name);
|
||||||
|
|
||||||
|
if (style.dom?.isConnected)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!style.dom) {
|
||||||
|
style.dom = document.createElement("style");
|
||||||
|
style.dom.dataset.vencordName = style.name;
|
||||||
|
}
|
||||||
|
compileStyle(style);
|
||||||
|
|
||||||
|
document.head.appendChild(style.dom);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param name The name of the style
|
||||||
|
* @returns `false` if the style was already disabled, `true` otherwise
|
||||||
|
* @see {@link enableStyle} for info on getting the name of an imported style
|
||||||
|
*/
|
||||||
|
export function disableStyle(name: string) {
|
||||||
|
const style = requireStyle(name);
|
||||||
|
if (!style.dom?.isConnected)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
style.dom.remove();
|
||||||
|
style.dom = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param name The name of the style
|
||||||
|
* @returns `true` in most cases, may return `false` in some edge cases
|
||||||
|
* @see {@link enableStyle} for info on getting the name of an imported style
|
||||||
|
*/
|
||||||
|
export const toggleStyle = (name: string) => isStyleEnabled(name) ? disableStyle(name) : enableStyle(name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param name The name of the style
|
||||||
|
* @returns Whether the style is enabled
|
||||||
|
* @see {@link enableStyle} for info on getting the name of an imported style
|
||||||
|
*/
|
||||||
|
export const isStyleEnabled = (name: string) => requireStyle(name).dom?.isConnected ?? false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the variables of a style
|
||||||
|
* ```ts
|
||||||
|
* // -- plugin.ts --
|
||||||
|
* import pluginStyle from "./plugin.css?managed";
|
||||||
|
* import { setStyleVars } from "@api/Styles";
|
||||||
|
* import { findByPropsLazy } from "@webpack";
|
||||||
|
* const classNames = findByPropsLazy("thin", "scrollerBase"); // { thin: "thin-31rlnD scrollerBase-_bVAAt", ... }
|
||||||
|
*
|
||||||
|
* // Inside some plugin method like "start()"
|
||||||
|
* setStyleClassNames(pluginStyle, classNames);
|
||||||
|
* enableStyle(pluginStyle);
|
||||||
|
* ```
|
||||||
|
* ```scss
|
||||||
|
* // -- plugin.css --
|
||||||
|
* .plugin-root [--thin]::-webkit-scrollbar { ... }
|
||||||
|
* ```
|
||||||
|
* ```scss
|
||||||
|
* // -- final stylesheet --
|
||||||
|
* .plugin-root .thin-31rlnD.scrollerBase-_bVAAt::-webkit-scrollbar { ... }
|
||||||
|
* ```
|
||||||
|
* @param name The name of the style
|
||||||
|
* @param classNames An object where the keys are the variable names and the values are the variable values
|
||||||
|
* @param recompile Whether to recompile the style after setting the variables, defaults to `true`
|
||||||
|
* @see {@link enableStyle} for info on getting the name of an imported style
|
||||||
|
*/
|
||||||
|
export const setStyleClassNames = (name: string, classNames: Record<string, string>, recompile = true) => {
|
||||||
|
const style = requireStyle(name);
|
||||||
|
style.classNames = classNames;
|
||||||
|
if (recompile && isStyleEnabled(style.name))
|
||||||
|
compileStyle(style);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the stylesheet after doing the following to the sourcecode:
|
||||||
|
* - Interpolate style classnames
|
||||||
|
* @param style **_Must_ be a style with a DOM element**
|
||||||
|
* @see {@link setStyleClassNames} for more info on style classnames
|
||||||
|
*/
|
||||||
|
export const compileStyle = (style: Style) => {
|
||||||
|
if (!style.dom) throw new Error("Style has no DOM element");
|
||||||
|
|
||||||
|
style.dom.textContent = style.source
|
||||||
|
.replace(/\[--(\w+)\]/g, (match, name) => {
|
||||||
|
const className = style.classNames[name];
|
||||||
|
return className ? classNameToSelector(className) : match;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param name The classname
|
||||||
|
* @param prefix A prefix to add each class, defaults to `""`
|
||||||
|
* @return A css selector for the classname
|
||||||
|
* @example
|
||||||
|
* classNameToSelector("foo bar") // => ".foo.bar"
|
||||||
|
*/
|
||||||
|
export const classNameToSelector = (name: string, prefix = "") => name.split(" ").map(n => `.${prefix}${n}`).join("");
|
||||||
|
|
||||||
|
type ClassNameFactoryArg = string | string[] | Record<string, unknown>;
|
||||||
|
/**
|
||||||
|
* @param prefix The prefix to add to each class, defaults to `""`
|
||||||
|
* @returns A classname generator function
|
||||||
|
* @example
|
||||||
|
* const cl = classNameFactory("plugin-");
|
||||||
|
*
|
||||||
|
* cl("base", ["item", "editable"], { selected: null, disabled: true })
|
||||||
|
* // => "plugin-base plugin-item plugin-editable plugin-disabled"
|
||||||
|
*/
|
||||||
|
export const classNameFactory = (prefix: string = "") => (...args: ClassNameFactoryArg[]) => {
|
||||||
|
const classNames = new Set<string>();
|
||||||
|
for (const arg of args) {
|
||||||
|
if (typeof arg === "string") classNames.add(arg);
|
||||||
|
else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name));
|
||||||
|
else if (typeof arg === "object") Object.entries(arg).forEach(([name, value]) => value && classNames.add(name));
|
||||||
|
}
|
||||||
|
return Array.from(classNames, name => prefix + name).join(" ");
|
||||||
|
};
|
@ -16,11 +16,18 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as $Badges from "./Badges";
|
||||||
import * as $Commands from "./Commands";
|
import * as $Commands from "./Commands";
|
||||||
import * as $DataStore from "./DataStore";
|
import * as $DataStore from "./DataStore";
|
||||||
|
import * as $MemberListDecorators from "./MemberListDecorators";
|
||||||
import * as $MessageAccessories from "./MessageAccessories";
|
import * as $MessageAccessories from "./MessageAccessories";
|
||||||
|
import * as $MessageDecorations from "./MessageDecorations";
|
||||||
import * as $MessageEventsAPI from "./MessageEvents";
|
import * as $MessageEventsAPI from "./MessageEvents";
|
||||||
|
import * as $MessagePopover from "./MessagePopover";
|
||||||
import * as $Notices from "./Notices";
|
import * as $Notices from "./Notices";
|
||||||
|
import * as $Notifications from "./Notifications";
|
||||||
|
import * as $ServerList from "./ServerList";
|
||||||
|
import * as $Styles from "./Styles";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An API allowing you to listen to Message Clicks or run your own logic
|
* An API allowing you to listen to Message Clicks or run your own logic
|
||||||
@ -28,16 +35,16 @@ import * as $Notices from "./Notices";
|
|||||||
*
|
*
|
||||||
* If your plugin uses this, you must add MessageEventsAPI to its dependencies
|
* If your plugin uses this, you must add MessageEventsAPI to its dependencies
|
||||||
*/
|
*/
|
||||||
const MessageEvents = $MessageEventsAPI;
|
export const MessageEvents = $MessageEventsAPI;
|
||||||
/**
|
/**
|
||||||
* An API allowing you to create custom notices
|
* An API allowing you to create custom notices
|
||||||
* (snackbars on the top, like the Update prompt)
|
* (snackbars on the top, like the Update prompt)
|
||||||
*/
|
*/
|
||||||
const Notices = $Notices;
|
export const Notices = $Notices;
|
||||||
/**
|
/**
|
||||||
* An API allowing you to register custom commands
|
* An API allowing you to register custom commands
|
||||||
*/
|
*/
|
||||||
const Commands = $Commands;
|
export const Commands = $Commands;
|
||||||
/**
|
/**
|
||||||
* A wrapper around IndexedDB. This can store arbitrarily
|
* A wrapper around IndexedDB. This can store arbitrarily
|
||||||
* large data and supports a lot of datatypes (Blob, Map, ...).
|
* large data and supports a lot of datatypes (Blob, Map, ...).
|
||||||
@ -52,10 +59,37 @@ const Commands = $Commands;
|
|||||||
* This is actually just idb-keyval, so if you're familiar with that, you're golden!
|
* This is actually just idb-keyval, so if you're familiar with that, you're golden!
|
||||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types}
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types}
|
||||||
*/
|
*/
|
||||||
const DataStore = $DataStore;
|
export const DataStore = $DataStore;
|
||||||
/**
|
/**
|
||||||
* An API allowing you to add custom components as message accessories
|
* An API allowing you to add custom components as message accessories
|
||||||
*/
|
*/
|
||||||
const MessageAccessories = $MessageAccessories;
|
export const MessageAccessories = $MessageAccessories;
|
||||||
|
/**
|
||||||
export { Commands,DataStore, MessageAccessories, MessageEvents, Notices };
|
* An API allowing you to add custom buttons in the message popover
|
||||||
|
*/
|
||||||
|
export const MessagePopover = $MessagePopover;
|
||||||
|
/**
|
||||||
|
* An API allowing you to add badges to user profiles
|
||||||
|
*/
|
||||||
|
export const Badges = $Badges;
|
||||||
|
/**
|
||||||
|
* An API allowing you to add custom elements to the server list
|
||||||
|
*/
|
||||||
|
export const ServerList = $ServerList;
|
||||||
|
/**
|
||||||
|
* An API allowing you to add components as message accessories
|
||||||
|
*/
|
||||||
|
export const MessageDecorations = $MessageDecorations;
|
||||||
|
/**
|
||||||
|
* An API allowing you to add components to member list users, in both DM's and servers
|
||||||
|
*/
|
||||||
|
export const MemberListDecorators = $MemberListDecorators;
|
||||||
|
/**
|
||||||
|
* An API allowing you to dynamically load styles
|
||||||
|
* a
|
||||||
|
*/
|
||||||
|
export const Styles = $Styles;
|
||||||
|
/**
|
||||||
|
* An API allowing you to display notifications
|
||||||
|
*/
|
||||||
|
export const Notifications = $Notifications;
|
||||||
|
@ -16,57 +16,83 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
|
import Logger from "@utils/Logger";
|
||||||
|
import { mergeDefaults } from "@utils/misc";
|
||||||
|
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||||
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
import plugins from "~plugins";
|
import plugins from "~plugins";
|
||||||
|
|
||||||
import IpcEvents from "../utils/IpcEvents";
|
const logger = new Logger("Settings");
|
||||||
import { mergeDefaults } from "../utils/misc";
|
|
||||||
import { OptionType } from "../utils/types";
|
|
||||||
import { React } from "../webpack/common";
|
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
notifyAboutUpdates: boolean;
|
notifyAboutUpdates: boolean;
|
||||||
|
autoUpdate: boolean;
|
||||||
useQuickCss: boolean;
|
useQuickCss: boolean;
|
||||||
enableReactDevtools: boolean;
|
enableReactDevtools: boolean;
|
||||||
|
themeLinks: string[];
|
||||||
|
frameless: boolean;
|
||||||
|
transparent: boolean;
|
||||||
|
winCtrlQ: boolean;
|
||||||
plugins: {
|
plugins: {
|
||||||
[plugin: string]: {
|
[plugin: string]: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
[setting: string]: any;
|
[setting: string]: any;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
notifications: {
|
||||||
|
timeout: number;
|
||||||
|
position: "top-right" | "bottom-right";
|
||||||
|
useNative: "always" | "never" | "not-focused";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultSettings: Settings = {
|
const DefaultSettings: Settings = {
|
||||||
notifyAboutUpdates: true,
|
notifyAboutUpdates: true,
|
||||||
|
autoUpdate: false,
|
||||||
useQuickCss: true,
|
useQuickCss: true,
|
||||||
|
themeLinks: [],
|
||||||
enableReactDevtools: false,
|
enableReactDevtools: false,
|
||||||
plugins: {}
|
frameless: false,
|
||||||
};
|
transparent: false,
|
||||||
|
winCtrlQ: false,
|
||||||
|
plugins: {},
|
||||||
|
|
||||||
for (const plugin in plugins) {
|
notifications: {
|
||||||
DefaultSettings.plugins[plugin] = {
|
timeout: 5000,
|
||||||
enabled: plugins[plugin].required ?? false
|
position: "bottom-right",
|
||||||
};
|
useNative: "not-focused"
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings;
|
var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings;
|
||||||
mergeDefaults(settings, DefaultSettings);
|
mergeDefaults(settings, DefaultSettings);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Corrupt settings file. ", err);
|
|
||||||
var settings = mergeDefaults({} as Settings, DefaultSettings);
|
var settings = mergeDefaults({} as Settings, DefaultSettings);
|
||||||
|
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
|
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
|
||||||
const subscriptions = new Set<SubscriptionCallback>();
|
const subscriptions = new Set<SubscriptionCallback>();
|
||||||
|
|
||||||
|
const proxyCache = {} as Record<string, any>;
|
||||||
|
|
||||||
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
|
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
|
||||||
function makeProxy(settings: Settings, root = settings, path = ""): Settings {
|
function makeProxy(settings: any, root = settings, path = ""): Settings {
|
||||||
return new Proxy(settings, {
|
return proxyCache[path] ??= new Proxy(settings, {
|
||||||
get(target, p: string) {
|
get(target, p: string) {
|
||||||
const v = target[p];
|
const v = target[p];
|
||||||
|
|
||||||
// using "in" is important in the following cases to properly handle falsy or nullish values
|
// using "in" is important in the following cases to properly handle falsy or nullish values
|
||||||
if (!(p in target)) {
|
if (!(p in target)) {
|
||||||
|
// Return empty for plugins with no settings
|
||||||
|
if (path === "plugins" && p in plugins)
|
||||||
|
return target[p] = makeProxy({
|
||||||
|
enabled: plugins[p].required ?? false
|
||||||
|
}, root, `plugins.${p}`);
|
||||||
|
|
||||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||||
// the default value.
|
// the default value.
|
||||||
if (path.startsWith("plugins.")) {
|
if (path.startsWith("plugins.")) {
|
||||||
@ -76,9 +102,13 @@ function makeProxy(settings: Settings, root = settings, path = ""): Settings {
|
|||||||
if (!setting) return v;
|
if (!setting) return v;
|
||||||
if ("default" in setting)
|
if ("default" in setting)
|
||||||
// normal setting with a default value
|
// normal setting with a default value
|
||||||
return setting.default;
|
return (target[p] = setting.default);
|
||||||
if (setting.type === OptionType.SELECT)
|
if (setting.type === OptionType.SELECT) {
|
||||||
return setting.options.find(o => o.default)?.value;
|
const def = setting.options.find(o => o.default);
|
||||||
|
if (def)
|
||||||
|
target[p] = def.value;
|
||||||
|
return def?.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
@ -131,14 +161,20 @@ export const Settings = makeProxy(settings);
|
|||||||
* Settings hook for React components. Returns a smart settings
|
* Settings hook for React components. Returns a smart settings
|
||||||
* object that automagically triggers a rerender if any properties
|
* object that automagically triggers a rerender if any properties
|
||||||
* are altered
|
* are altered
|
||||||
|
* @param paths An optional list of paths to whitelist for rerenders
|
||||||
* @returns Settings
|
* @returns Settings
|
||||||
*/
|
*/
|
||||||
export function useSettings() {
|
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
|
||||||
|
export function useSettings(paths?: string[]) {
|
||||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||||
|
|
||||||
|
const onUpdate: SubscriptionCallback = paths
|
||||||
|
? (value, path) => paths.includes(path) && forceUpdate()
|
||||||
|
: forceUpdate;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
subscriptions.add(forceUpdate);
|
subscriptions.add(onUpdate);
|
||||||
return () => void subscriptions.delete(forceUpdate);
|
return () => void subscriptions.delete(onUpdate);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return Settings;
|
return Settings;
|
||||||
@ -165,3 +201,37 @@ export function addSettingsListener(path: string, onUpdate: (newValue: any, path
|
|||||||
(onUpdate as SubscriptionCallback)._path = path;
|
(onUpdate as SubscriptionCallback)._path = path;
|
||||||
subscriptions.add(onUpdate);
|
subscriptions.add(onUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
||||||
|
const { plugins } = settings;
|
||||||
|
if (name in plugins) return;
|
||||||
|
|
||||||
|
for (const oldName of oldNames) {
|
||||||
|
if (oldName in plugins) {
|
||||||
|
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
|
||||||
|
plugins[name] = plugins[oldName];
|
||||||
|
delete plugins[oldName];
|
||||||
|
VencordNative.ipc.invoke(
|
||||||
|
IpcEvents.SET_SETTINGS,
|
||||||
|
JSON.stringify(settings, null, 4)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function definePluginSettings<D extends SettingsDefinition, C extends SettingsChecks<D>>(def: D, checks?: C) {
|
||||||
|
const definedSettings: DefinedSettings<D> = {
|
||||||
|
get store() {
|
||||||
|
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
|
||||||
|
return Settings.plugins[definedSettings.pluginName] as any;
|
||||||
|
},
|
||||||
|
use: settings => useSettings(
|
||||||
|
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`)
|
||||||
|
).plugins[definedSettings.pluginName] as any,
|
||||||
|
def,
|
||||||
|
checks: checks ?? {},
|
||||||
|
pluginName: "",
|
||||||
|
};
|
||||||
|
return definedSettings;
|
||||||
|
}
|
||||||
|
29
src/components/Badge.tsx
Normal file
29
src/components/Badge.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function Badge({ text, color }): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="vc-plugins-badge" style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
justifySelf: "flex-end",
|
||||||
|
marginLeft: "auto"
|
||||||
|
}}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
68
src/components/CheckedTextInput.tsx
Normal file
68
src/components/CheckedTextInput.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { React, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
|
// TODO: Refactor settings to use this as well
|
||||||
|
interface TextInputProps {
|
||||||
|
/**
|
||||||
|
* WARNING: Changing this between renders will have no effect!
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
/**
|
||||||
|
* This will only be called if the new value passed validate()
|
||||||
|
*/
|
||||||
|
onChange(newValue: string): void;
|
||||||
|
/**
|
||||||
|
* Optionally validate the user input
|
||||||
|
* Return true if the input is valid
|
||||||
|
* Otherwise, return a string containing the reason for this input being invalid
|
||||||
|
*/
|
||||||
|
validate(v: string): true | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A very simple wrapper around Discord's TextInput that validates input and shows
|
||||||
|
* the user an error message and only calls your onChange when the input is valid
|
||||||
|
*/
|
||||||
|
export function CheckedTextInput({ value: initialValue, onChange, validate }: TextInputProps) {
|
||||||
|
const [value, setValue] = React.useState(initialValue);
|
||||||
|
const [error, setError] = React.useState<string>();
|
||||||
|
|
||||||
|
function handleChange(v: string) {
|
||||||
|
setValue(v);
|
||||||
|
const res = validate(v);
|
||||||
|
if (res === true) {
|
||||||
|
setError(void 0);
|
||||||
|
onChange(v);
|
||||||
|
} else {
|
||||||
|
setError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
38
src/components/DonateButton.tsx
Normal file
38
src/components/DonateButton.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
|
import { Button } from "@webpack/common";
|
||||||
|
|
||||||
|
import { Heart } from "./Heart";
|
||||||
|
|
||||||
|
export default function DonateButton(props: any) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
look={Button.Looks.LINK}
|
||||||
|
color={Button.Colors.TRANSPARENT}
|
||||||
|
onClick={() =>
|
||||||
|
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/sponsors/Vendicated")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Heart />
|
||||||
|
Donate
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
@ -16,13 +16,20 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Logger from "../utils/logger";
|
import Logger from "@utils/Logger";
|
||||||
import { Margins, React } from "../webpack/common";
|
import { LazyComponent } from "@utils/misc";
|
||||||
|
import { Margins, React } from "@webpack/common";
|
||||||
|
|
||||||
import { ErrorCard } from "./ErrorCard";
|
import { ErrorCard } from "./ErrorCard";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
/** Render nothing if an error occurs */
|
||||||
|
noop?: boolean;
|
||||||
|
/** Fallback component to render if an error occurs */
|
||||||
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
||||||
|
/** called when an error occurs */
|
||||||
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
||||||
|
/** Custom error message */
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,15 +39,10 @@ const logger = new Logger("React ErrorBoundary", color);
|
|||||||
|
|
||||||
const NO_ERROR = {};
|
const NO_ERROR = {};
|
||||||
|
|
||||||
export default class ErrorBoundary extends React.Component<React.PropsWithChildren<Props>> {
|
// We might want to import this in a place where React isn't ready yet.
|
||||||
static wrap<T = any>(Component: React.ComponentType<T>): (props: T) => React.ReactElement {
|
// Thus, wrap in a LazyComponent
|
||||||
return props => (
|
const ErrorBoundary = LazyComponent(() => {
|
||||||
<ErrorBoundary>
|
return class ErrorBoundary extends React.PureComponent<React.PropsWithChildren<Props>> {
|
||||||
<Component {...props as any/* I hate react typings ??? */} />
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
error: NO_ERROR as any,
|
error: NO_ERROR as any,
|
||||||
stack: "",
|
stack: "",
|
||||||
@ -71,6 +73,8 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
|
|||||||
render() {
|
render() {
|
||||||
if (this.state.error === NO_ERROR) return this.props.children;
|
if (this.state.error === NO_ERROR) return this.props.children;
|
||||||
|
|
||||||
|
if (this.props.noop) return null;
|
||||||
|
|
||||||
if (this.props.fallback)
|
if (this.props.fallback)
|
||||||
return <this.props.fallback
|
return <this.props.fallback
|
||||||
children={this.props.children}
|
children={this.props.children}
|
||||||
@ -96,4 +100,16 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
|
|||||||
</ErrorCard>
|
</ErrorCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
}) as
|
||||||
|
React.ComponentType<React.PropsWithChildren<Props>> & {
|
||||||
|
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
||||||
|
<ErrorBoundary {...errorBoundaryProps}>
|
||||||
|
<Component {...props} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Card } from "../webpack/common";
|
import { Card } from "@webpack/common";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { React } from "../webpack/common";
|
import type { React } from "@webpack/common";
|
||||||
|
|
||||||
export function Flex(props: React.PropsWithChildren<{
|
export function Flex(props: React.PropsWithChildren<{
|
||||||
flexDirection?: React.CSSProperties["flexDirection"];
|
flexDirection?: React.CSSProperties["flexDirection"];
|
||||||
@ -24,9 +24,11 @@ export function Flex(props: React.PropsWithChildren<{
|
|||||||
className?: string;
|
className?: string;
|
||||||
} & React.HTMLProps<HTMLDivElement>>) {
|
} & React.HTMLProps<HTMLDivElement>>) {
|
||||||
props.style ??= {};
|
props.style ??= {};
|
||||||
props.style.flexDirection ||= props.flexDirection;
|
|
||||||
props.style.gap ??= "1em";
|
|
||||||
props.style.display = "flex";
|
props.style.display = "flex";
|
||||||
|
// TODO(ven): Remove me, what was I thinking??
|
||||||
|
props.style.gap ??= "1em";
|
||||||
|
props.style.flexDirection ||= props.flexDirection;
|
||||||
|
delete props.flexDirection;
|
||||||
return (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
35
src/components/Heart.tsx
Normal file
35
src/components/Heart.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function Heart() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="16"
|
||||||
|
style={{ marginRight: "0.5em", transform: "translateY(2px)" }}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#db61a2"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
@ -16,21 +16,20 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { React } from "../webpack/common";
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
interface Props {
|
interface Props extends React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> {
|
||||||
href: string;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
style?: React.CSSProperties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Link(props: React.PropsWithChildren<Props>) {
|
export function Link(props: React.PropsWithChildren<Props>) {
|
||||||
if (props.disabled) {
|
if (props.disabled) {
|
||||||
props.style ??= {};
|
props.style ??= {};
|
||||||
props.style.pointerEvents = "none";
|
props.style.pointerEvents = "none";
|
||||||
|
props["aria-disabled"] = true;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<a href={props.href} target="_blank" style={props.style}>
|
<a role="link" target="_blank" {...props}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
@ -16,27 +16,36 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import monacoHtml from "~fileContent/monacoWin.html";
|
import { debounce } from "@utils/debounce";
|
||||||
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
|
import { Queue } from "@utils/Queue";
|
||||||
|
import { find } from "@webpack";
|
||||||
|
|
||||||
import { IpcEvents } from "../utils";
|
import monacoHtml from "~fileContent/monacoWin.html";
|
||||||
import { debounce } from "../utils/debounce";
|
|
||||||
import { Queue } from "../utils/Queue";
|
|
||||||
import { find } from "../webpack/webpack";
|
|
||||||
|
|
||||||
const queue = new Queue();
|
const queue = new Queue();
|
||||||
const setCss = debounce((css: string) => {
|
const setCss = debounce((css: string) => {
|
||||||
queue.add(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css));
|
queue.push(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css));
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function launchMonacoEditor() {
|
export async function launchMonacoEditor() {
|
||||||
const win = open("about:blank", void 0, "popup,width=1000,height=1000")!;
|
const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
|
||||||
|
const win = open("about:blank", "VencordQuickCss", features);
|
||||||
|
if (!win) {
|
||||||
|
alert("Failed to open QuickCSS popup. Make sure to allow popups!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
win.setCss = setCss;
|
win.setCss = setCss;
|
||||||
win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
|
win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
|
||||||
win.getTheme = () => find(m => m.ProtoClass?.typeName.endsWith("PreloadedUserSettings"))
|
win.getTheme = () =>
|
||||||
.getCurrentValue().appearance.theme === 1
|
find(m =>
|
||||||
? "vs-dark"
|
m.ProtoClass?.typeName.endsWith("PreloadedUserSettings")
|
||||||
: "vs-light";
|
)?.getCurrentValue()?.appearance?.theme === 2
|
||||||
|
? "vs-light"
|
||||||
|
: "vs-dark";
|
||||||
|
|
||||||
win.document.write(monacoHtml);
|
win.document.write(monacoHtml);
|
||||||
|
|
||||||
|
window.__VENCORD_MONACO_WIN__ = new WeakRef(win);
|
||||||
}
|
}
|
||||||
|
308
src/components/PatchHelper.tsx
Normal file
308
src/components/PatchHelper.tsx
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { debounce } from "@utils/debounce";
|
||||||
|
import { makeCodeblock } from "@utils/misc";
|
||||||
|
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches";
|
||||||
|
import { search } from "@webpack";
|
||||||
|
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
|
import { CheckedTextInput } from "./CheckedTextInput";
|
||||||
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
|
|
||||||
|
// Do not include diff in non dev builds (side effects import)
|
||||||
|
if (IS_DEV) {
|
||||||
|
var differ = require("diff") as typeof import("diff");
|
||||||
|
}
|
||||||
|
|
||||||
|
const findCandidates = debounce(function ({ find, setModule, setError }) {
|
||||||
|
const candidates = search(find);
|
||||||
|
const keys = Object.keys(candidates);
|
||||||
|
const len = keys.length;
|
||||||
|
if (len === 0)
|
||||||
|
setError("No match. Perhaps that module is lazy loaded?");
|
||||||
|
else if (len !== 1)
|
||||||
|
setError("Multiple matches. Please refine your filter");
|
||||||
|
else
|
||||||
|
setModule([keys[0], candidates[keys[0]]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ReplacementComponentProps {
|
||||||
|
module: [id: number, factory: Function];
|
||||||
|
match: string | RegExp;
|
||||||
|
replacement: string | ReplaceFn;
|
||||||
|
setReplacementError(error: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReplacementComponent({ module, match, replacement, setReplacementError }: ReplacementComponentProps) {
|
||||||
|
const [id, fact] = module;
|
||||||
|
const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
|
||||||
|
|
||||||
|
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
||||||
|
const src: string = fact.toString().replaceAll("\n", "");
|
||||||
|
const canonicalMatch = canonicalizeMatch(match);
|
||||||
|
try {
|
||||||
|
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
||||||
|
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
||||||
|
setReplacementError(void 0);
|
||||||
|
} catch (e) {
|
||||||
|
setReplacementError((e as Error).message);
|
||||||
|
return ["", [], []];
|
||||||
|
}
|
||||||
|
const m = src.match(canonicalMatch);
|
||||||
|
return [patched, m, makeDiff(src, patched, m)];
|
||||||
|
}, [id, match, replacement]);
|
||||||
|
|
||||||
|
function makeDiff(original: string, patched: string, match: RegExpMatchArray | null) {
|
||||||
|
if (!match || original === patched) return null;
|
||||||
|
|
||||||
|
const changeSize = patched.length - original.length;
|
||||||
|
|
||||||
|
// Use 200 surrounding characters of context
|
||||||
|
const start = Math.max(0, match.index! - 200);
|
||||||
|
const end = Math.min(original.length, match.index! + match[0].length + 200);
|
||||||
|
// (changeSize may be negative)
|
||||||
|
const endPatched = end + changeSize;
|
||||||
|
|
||||||
|
const context = original.slice(start, end);
|
||||||
|
const patchedContext = patched.slice(start, endPatched);
|
||||||
|
|
||||||
|
return differ.diffWordsWithSpace(context, patchedContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMatch() {
|
||||||
|
if (!matchResult)
|
||||||
|
return <Forms.FormText>Regex doesn't match!</Forms.FormText>;
|
||||||
|
|
||||||
|
const fullMatch = matchResult[0] ? makeCodeblock(matchResult[0], "js") : "";
|
||||||
|
const groups = matchResult.length > 1
|
||||||
|
? makeCodeblock(matchResult.slice(1).map((g, i) => `Group ${i + 1}: ${g}`).join("\n"), "yml")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ userSelect: "text" }}>{Parser.parse(fullMatch)}</div>
|
||||||
|
<div style={{ userSelect: "text" }}>{Parser.parse(groups)}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDiff() {
|
||||||
|
return diff?.map(p => {
|
||||||
|
const color = p.added ? "lime" : p.removed ? "red" : "grey";
|
||||||
|
return <div style={{ color, userSelect: "text" }}>{p.value}</div>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle>Module {id}</Forms.FormTitle>
|
||||||
|
|
||||||
|
{!!matchResult?.[0]?.length && (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle>Match</Forms.FormTitle>
|
||||||
|
{renderMatch()}
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
|
||||||
|
{!!diff?.length && (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle>Diff</Forms.FormTitle>
|
||||||
|
{renderDiff()}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!diff?.length && (
|
||||||
|
<Button className={Margins.marginTop20} onClick={() => {
|
||||||
|
try {
|
||||||
|
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
||||||
|
setCompileResult([true, "Compiled successfully"]);
|
||||||
|
} catch (err) {
|
||||||
|
setCompileResult([false, (err as Error).message]);
|
||||||
|
}
|
||||||
|
}}>Compile</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{compileResult &&
|
||||||
|
<Forms.FormText style={{ color: compileResult[0] ? "var(--text-positive)" : "var(--text-danger)" }}>
|
||||||
|
{compileResult[1]}
|
||||||
|
</Forms.FormText>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
||||||
|
const [isFunc, setIsFunc] = React.useState(false);
|
||||||
|
const [error, setError] = React.useState<string>();
|
||||||
|
|
||||||
|
function onChange(v: string) {
|
||||||
|
setError(void 0);
|
||||||
|
|
||||||
|
if (isFunc) {
|
||||||
|
try {
|
||||||
|
const func = (0, eval)(v);
|
||||||
|
if (typeof func === "function")
|
||||||
|
setReplacement(() => func);
|
||||||
|
else
|
||||||
|
setError("Replacement must be a function");
|
||||||
|
} catch (e) {
|
||||||
|
setReplacement(v);
|
||||||
|
setError((e as Error).message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setReplacement(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(
|
||||||
|
() => void (isFunc ? onChange(replacement) : setError(void 0)),
|
||||||
|
[isFunc]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle>replacement</Forms.FormTitle>
|
||||||
|
<TextInput
|
||||||
|
value={replacement?.toString()}
|
||||||
|
onChange={onChange}
|
||||||
|
error={error ?? replacementError}
|
||||||
|
/>
|
||||||
|
{!isFunc && (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
|
||||||
|
{Object.entries({
|
||||||
|
"$$": "Insert a $",
|
||||||
|
"$&": "Insert the entire match",
|
||||||
|
"$`\u200b": "Insert the substring before the match",
|
||||||
|
"$'": "Insert the substring after the match",
|
||||||
|
"$n": "Insert the nth capturing group ($1, $2...)",
|
||||||
|
"$self": "Insert the plugin instance",
|
||||||
|
}).map(([placeholder, desc]) => (
|
||||||
|
<Forms.FormText key={placeholder}>
|
||||||
|
{Parser.parse("`" + placeholder + "`")}: {desc}
|
||||||
|
</Forms.FormText>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
className={Margins.marginTop8}
|
||||||
|
value={isFunc}
|
||||||
|
onChange={setIsFunc}
|
||||||
|
note="'replacement' will be evaled if this is toggled"
|
||||||
|
hideBorder={true}
|
||||||
|
>
|
||||||
|
Treat as Function
|
||||||
|
</Switch>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PatchHelper() {
|
||||||
|
const [find, setFind] = React.useState<string>("");
|
||||||
|
const [match, setMatch] = React.useState<string>("");
|
||||||
|
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
||||||
|
|
||||||
|
const [replacementError, setReplacementError] = React.useState<string>();
|
||||||
|
|
||||||
|
const [module, setModule] = React.useState<[number, Function]>();
|
||||||
|
const [findError, setFindError] = React.useState<string>();
|
||||||
|
|
||||||
|
const code = React.useMemo(() => {
|
||||||
|
return `
|
||||||
|
{
|
||||||
|
find: ${JSON.stringify(find)},
|
||||||
|
replacement: {
|
||||||
|
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
||||||
|
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`.trim();
|
||||||
|
}, [find, match, replacement]);
|
||||||
|
|
||||||
|
function onFindChange(v: string) {
|
||||||
|
setFindError(void 0);
|
||||||
|
setFind(v);
|
||||||
|
if (v.length) {
|
||||||
|
findCandidates({ find: v, setModule, setError: setFindError });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMatchChange(v: string) {
|
||||||
|
try {
|
||||||
|
new RegExp(v);
|
||||||
|
setFindError(void 0);
|
||||||
|
setMatch(v);
|
||||||
|
} catch (e: any) {
|
||||||
|
setFindError((e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Forms.FormSection>
|
||||||
|
<Text variant="heading-md/normal" tag="h2" className={Margins.marginBottom8}>Patch Helper</Text>
|
||||||
|
<Forms.FormTitle>find</Forms.FormTitle>
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
value={find}
|
||||||
|
onChange={onFindChange}
|
||||||
|
error={findError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Forms.FormTitle>match</Forms.FormTitle>
|
||||||
|
<CheckedTextInput
|
||||||
|
value={match}
|
||||||
|
onChange={onMatchChange}
|
||||||
|
validate={v => {
|
||||||
|
try {
|
||||||
|
return (new RegExp(v), true);
|
||||||
|
} catch (e) {
|
||||||
|
return (e as Error).message;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ReplacementInput
|
||||||
|
replacement={replacement}
|
||||||
|
setReplacement={setReplacement}
|
||||||
|
replacementError={replacementError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Forms.FormDivider />
|
||||||
|
{module && (
|
||||||
|
<ReplacementComponent
|
||||||
|
module={module}
|
||||||
|
match={new RegExp(match)}
|
||||||
|
replacement={replacement}
|
||||||
|
setReplacementError={setReplacementError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!(find && match && replacement) && (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle className={Margins.marginTop20}>Code</Forms.FormTitle>
|
||||||
|
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
||||||
|
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Forms.FormSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IS_DEV ? ErrorBoundary.wrap(PatchHelper) : null;
|
@ -16,29 +16,31 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Forms } from "@components";
|
import { generateId } from "@api/Commands";
|
||||||
|
import { useSettings } from "@api/settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Flex } from "@components/Flex";
|
||||||
|
import { LazyComponent } from "@utils/misc";
|
||||||
|
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||||
|
import { proxyLazy } from "@utils/proxyLazy";
|
||||||
|
import { OptionType, Plugin } from "@utils/types";
|
||||||
|
import { findByCode, findByPropsLazy } from "@webpack";
|
||||||
|
import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
import { Constructor } from "type-fest";
|
import { Constructor } from "type-fest";
|
||||||
|
|
||||||
import { generateId } from "../../api/Commands";
|
|
||||||
import { useSettings } from "../../api/settings";
|
|
||||||
import { lazyWebpack, proxyLazy } from "../../utils";
|
|
||||||
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "../../utils/modal";
|
|
||||||
import { OptionType, Plugin } from "../../utils/types";
|
|
||||||
import { filters } from "../../webpack";
|
|
||||||
import { Button, FluxDispatcher, React, Text, Tooltip, UserStore, UserUtils } from "../../webpack/common";
|
|
||||||
import ErrorBoundary from "../ErrorBoundary";
|
|
||||||
import { Flex } from "../Flex";
|
|
||||||
import {
|
import {
|
||||||
|
ISettingElementProps,
|
||||||
SettingBooleanComponent,
|
SettingBooleanComponent,
|
||||||
SettingInputComponent,
|
SettingCustomComponent,
|
||||||
SettingNumericComponent,
|
SettingNumericComponent,
|
||||||
SettingSelectComponent,
|
SettingSelectComponent,
|
||||||
SettingSliderComponent
|
SettingSliderComponent,
|
||||||
|
SettingTextComponent
|
||||||
} from "./components";
|
} from "./components";
|
||||||
|
|
||||||
const UserSummaryItem = lazyWebpack(filters.byCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
||||||
const AvatarStyles = lazyWebpack(filters.byProps(["moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"]));
|
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
||||||
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
||||||
|
|
||||||
interface PluginModalProps extends ModalProps {
|
interface PluginModalProps extends ModalProps {
|
||||||
@ -60,6 +62,16 @@ function makeDummyUser(user: { name: string, id: BigInt; }) {
|
|||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any>>> = {
|
||||||
|
[OptionType.STRING]: SettingTextComponent,
|
||||||
|
[OptionType.NUMBER]: SettingNumericComponent,
|
||||||
|
[OptionType.BIGINT]: SettingNumericComponent,
|
||||||
|
[OptionType.BOOLEAN]: SettingBooleanComponent,
|
||||||
|
[OptionType.SELECT]: SettingSelectComponent,
|
||||||
|
[OptionType.SLIDER]: SettingSliderComponent,
|
||||||
|
[OptionType.COMPONENT]: SettingCustomComponent
|
||||||
|
};
|
||||||
|
|
||||||
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
||||||
const [authors, setAuthors] = React.useState<Partial<User>[]>([]);
|
const [authors, setAuthors] = React.useState<Partial<User>[]>([]);
|
||||||
|
|
||||||
@ -68,23 +80,37 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
const [tempSettings, setTempSettings] = React.useState<Record<string, any>>({});
|
const [tempSettings, setTempSettings] = React.useState<Record<string, any>>({});
|
||||||
|
|
||||||
const [errors, setErrors] = React.useState<Record<string, boolean>>({});
|
const [errors, setErrors] = React.useState<Record<string, boolean>>({});
|
||||||
|
const [saveError, setSaveError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const canSubmit = () => Object.values(errors).every(e => !e);
|
const canSubmit = () => Object.values(errors).every(e => !e);
|
||||||
|
|
||||||
|
const hasSettings = Boolean(pluginSettings && plugin.options);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
for (const user of plugin.authors.slice(0, 6)) {
|
for (const user of plugin.authors.slice(0, 6)) {
|
||||||
const author = user.id ? await UserUtils.fetchUser(`${user.id}`).catch(() => null) : makeDummyUser(user);
|
const author = user.id
|
||||||
setAuthors(a => [...a, author || makeDummyUser(user)]);
|
? await UserUtils.fetchUser(`${user.id}`).catch(() => makeDummyUser(user))
|
||||||
|
: makeDummyUser(user);
|
||||||
|
setAuthors(a => [...a, author]);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function saveAndClose() {
|
async function saveAndClose() {
|
||||||
if (!plugin.options) {
|
if (!plugin.options) {
|
||||||
onClose();
|
onClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (plugin.beforeSave) {
|
||||||
|
const result = await Promise.resolve(plugin.beforeSave(tempSettings));
|
||||||
|
if (result !== true) {
|
||||||
|
setSaveError(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let restartNeeded = false;
|
let restartNeeded = false;
|
||||||
for (const [key, value] of Object.entries(tempSettings)) {
|
for (const [key, value] of Object.entries(tempSettings)) {
|
||||||
const option = plugin.options[key];
|
const option = plugin.options[key];
|
||||||
@ -97,13 +123,11 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSettings() {
|
function renderSettings() {
|
||||||
if (!pluginSettings || !plugin.options) {
|
if (!hasSettings || !plugin.options) {
|
||||||
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
||||||
}
|
} else {
|
||||||
|
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
||||||
const options: JSX.Element[] = [];
|
function onChange(newValue: any) {
|
||||||
for (const [key, setting] of Object.entries(plugin.options)) {
|
|
||||||
function onChange(newValue) {
|
|
||||||
setTempSettings(s => ({ ...s, [key]: newValue }));
|
setTempSettings(s => ({ ...s, [key]: newValue }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,33 +135,23 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
setErrors(e => ({ ...e, [key]: hasError }));
|
setErrors(e => ({ ...e, [key]: hasError }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = { onChange, pluginSettings, id: key, onError };
|
const Component = Components[setting.type];
|
||||||
switch (setting.type) {
|
return (
|
||||||
case OptionType.SELECT: {
|
<Component
|
||||||
options.push(<SettingSelectComponent key={key} option={setting} {...props} />);
|
id={key}
|
||||||
break;
|
key={key}
|
||||||
}
|
option={setting}
|
||||||
case OptionType.STRING: {
|
onChange={onChange}
|
||||||
options.push(<SettingInputComponent key={key} option={setting} {...props} />);
|
onError={onError}
|
||||||
break;
|
pluginSettings={pluginSettings}
|
||||||
}
|
definedSettings={plugin.settings}
|
||||||
case OptionType.NUMBER:
|
/>
|
||||||
case OptionType.BIGINT: {
|
);
|
||||||
options.push(<SettingNumericComponent key={key} option={setting} {...props} />);
|
});
|
||||||
break;
|
|
||||||
}
|
|
||||||
case OptionType.BOOLEAN: {
|
|
||||||
options.push(<SettingBooleanComponent key={key} option={setting} {...props} />);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case OptionType.SLIDER: {
|
|
||||||
options.push(<SettingSliderComponent key={key} option={setting} {...props} />);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
|
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderMoreUsers(_label: string, count: number) {
|
function renderMoreUsers(_label: string, count: number) {
|
||||||
const sliceCount = plugin.authors.length - count;
|
const sliceCount = plugin.authors.length - count;
|
||||||
@ -161,14 +175,16 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
|
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
|
||||||
<ModalHeader>
|
<ModalHeader separator={false}>
|
||||||
<Text variant="heading-md/bold">{plugin.name}</Text>
|
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
|
||||||
|
<ModalCloseButton onClick={onClose} />
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalContent style={{ marginBottom: 8, marginTop: 8 }}>
|
<ModalContent style={{ marginBottom: 8, marginTop: 8 }}>
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
|
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
|
||||||
<Forms.FormText>{plugin.description}</Forms.FormText>
|
<Forms.FormText>{plugin.description}</Forms.FormText>
|
||||||
<div style={{ marginTop: 8, marginBottom: 8, width: "fit-content" }}>
|
<Forms.FormTitle tag="h3" style={{ marginTop: 8, marginBottom: 0 }}>Authors</Forms.FormTitle>
|
||||||
|
<div style={{ width: "fit-content", marginBottom: 8 }}>
|
||||||
<UserSummaryItem
|
<UserSummaryItem
|
||||||
users={authors}
|
users={authors}
|
||||||
count={plugin.authors.length}
|
count={plugin.authors.length}
|
||||||
@ -185,7 +201,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
|
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
|
||||||
<plugin.settingsAboutComponent />
|
<plugin.settingsAboutComponent tempSettings={tempSettings} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
</div>
|
</div>
|
||||||
@ -195,14 +211,16 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
{renderSettings()}
|
{renderSettings()}
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
<ModalFooter>
|
{hasSettings && <ModalFooter>
|
||||||
<Flex>
|
<Flex flexDirection="column" style={{ width: "100%" }}>
|
||||||
|
<Flex style={{ marginLeft: "auto" }}>
|
||||||
<Button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
color={Button.Colors.RED}
|
color={Button.Colors.WHITE}
|
||||||
|
look={Button.Looks.LINK}
|
||||||
>
|
>
|
||||||
Exit Without Saving
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
|
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
|
||||||
{({ onMouseEnter, onMouseLeave }) => (
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
@ -214,12 +232,14 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
disabled={!canSubmit()}
|
disabled={!canSubmit()}
|
||||||
>
|
>
|
||||||
Save & Exit
|
Save & Close
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalFooter>
|
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>}
|
||||||
|
</Flex>
|
||||||
|
</ModalFooter>}
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,13 +16,12 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Forms } from "@components";
|
import { PluginOptionBoolean } from "@utils/types";
|
||||||
|
import { Forms, React, Select } from "@webpack/common";
|
||||||
|
|
||||||
import { PluginOptionBoolean } from "../../../utils/types";
|
|
||||||
import { React, Select } from "../../../webpack/common";
|
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function SettingBooleanComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
|
export function SettingBooleanComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
|
||||||
const def = pluginSettings[id] ?? option.default;
|
const def = pluginSettings[id] ?? option.default;
|
||||||
|
|
||||||
const [state, setState] = React.useState(def ?? false);
|
const [state, setState] = React.useState(def ?? false);
|
||||||
@ -38,7 +37,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange,
|
|||||||
];
|
];
|
||||||
|
|
||||||
function handleChange(newValue: boolean): void {
|
function handleChange(newValue: boolean): void {
|
||||||
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
@ -52,7 +51,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange,
|
|||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||||
<Select
|
<Select
|
||||||
isDisabled={option.disabled?.() ?? false}
|
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
options={options}
|
options={options}
|
||||||
placeholder={option.placeholder ?? "Select an option"}
|
placeholder={option.placeholder ?? "Select an option"}
|
||||||
maxVisibleItems={5}
|
maxVisibleItems={5}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PluginOptionComponent } from "@utils/types";
|
||||||
|
|
||||||
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
|
export function SettingCustomComponent({ option, onChange, onError }: ISettingElementProps<PluginOptionComponent>) {
|
||||||
|
return option.component({ setValue: onChange, setError: onError, option });
|
||||||
|
}
|
@ -16,15 +16,14 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Forms } from "@components";
|
import { OptionType, PluginOptionNumber } from "@utils/types";
|
||||||
|
import { Forms, React, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
import { OptionType, PluginOptionNumber } from "../../../utils/types";
|
|
||||||
import { React, TextInput } from "../../../webpack/common";
|
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
|
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||||
|
|
||||||
export function SettingNumericComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
|
export function SettingNumericComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
|
||||||
function serialize(value: any) {
|
function serialize(value: any) {
|
||||||
if (option.type === OptionType.BIGINT) return BigInt(value);
|
if (option.type === OptionType.BIGINT) return BigInt(value);
|
||||||
return Number(value);
|
return Number(value);
|
||||||
@ -38,7 +37,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
|
|||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue) {
|
function handleChange(newValue) {
|
||||||
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
||||||
@ -59,7 +58,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
|
|||||||
value={state}
|
value={state}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={option.placeholder ?? "Enter a number"}
|
placeholder={option.placeholder ?? "Enter a number"}
|
||||||
disabled={option.disabled?.() ?? false}
|
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||||
|
@ -16,14 +16,12 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FormSection, FormText, FormTitle } from "@components/Forms";
|
import { PluginOptionSelect } from "@utils/types";
|
||||||
import Select from "@components/Select";
|
import { Forms, React, Select } from "@webpack/common";
|
||||||
|
|
||||||
import { PluginOptionSelect } from "../../../utils/types";
|
|
||||||
import { React } from "../../../webpack/common";
|
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function SettingSelectComponent({ option, pluginSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
|
export function SettingSelectComponent({ option, pluginSettings, definedSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
|
||||||
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
|
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
|
||||||
|
|
||||||
const [state, setState] = React.useState<any>(def ?? null);
|
const [state, setState] = React.useState<any>(def ?? null);
|
||||||
@ -34,7 +32,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
|
|||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue) {
|
function handleChange(newValue) {
|
||||||
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
@ -44,10 +42,10 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormSection>
|
<Forms.FormSection>
|
||||||
<FormTitle>{option.description}</FormTitle>
|
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||||
<Select
|
<Select
|
||||||
isDisabled={option.disabled?.() ?? false}
|
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
options={option.options}
|
options={option.options}
|
||||||
placeholder={option.placeholder ?? "Select an option"}
|
placeholder={option.placeholder ?? "Select an option"}
|
||||||
maxVisibleItems={5}
|
maxVisibleItems={5}
|
||||||
@ -57,7 +55,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
|
|||||||
serialize={v => String(v)}
|
serialize={v => String(v)}
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
{error && <FormText style={{ color: "var(--text-danger)" }}>{error}</FormText>}
|
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||||
</FormSection>
|
</Forms.FormSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Forms } from "@components";
|
import { PluginOptionSlider } from "@utils/types";
|
||||||
|
import { Forms, React, Slider } from "@webpack/common";
|
||||||
|
|
||||||
import { PluginOptionSlider } from "../../../utils/types";
|
|
||||||
import { React, Slider } from "../../../webpack/common";
|
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function makeRange(start: number, end: number, step = 1) {
|
export function makeRange(start: number, end: number, step = 1) {
|
||||||
@ -30,7 +29,7 @@ export function makeRange(start: number, end: number, step = 1) {
|
|||||||
return ranges;
|
return ranges;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingSliderComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) {
|
export function SettingSliderComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) {
|
||||||
const def = pluginSettings[id] ?? option.default;
|
const def = pluginSettings[id] ?? option.default;
|
||||||
|
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
@ -40,7 +39,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o
|
|||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue: number): void {
|
function handleChange(newValue: number): void {
|
||||||
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
@ -53,7 +52,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o
|
|||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||||
<Slider
|
<Slider
|
||||||
disabled={option.disabled?.() ?? false}
|
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
markers={option.markers}
|
markers={option.markers}
|
||||||
minValue={option.markers[0]}
|
minValue={option.markers[0]}
|
||||||
maxValue={option.markers[option.markers.length - 1]}
|
maxValue={option.markers[option.markers.length - 1]}
|
||||||
|
@ -16,13 +16,12 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Forms } from "@components";
|
import { PluginOptionString } from "@utils/types";
|
||||||
|
import { Forms, React, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
import { PluginOptionString } from "../../../utils/types";
|
|
||||||
import { React, TextInput } from "../../../webpack/common";
|
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function SettingInputComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
|
export function SettingTextComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
|
||||||
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
|
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
@ -31,7 +30,7 @@ export function SettingInputComponent({ option, pluginSettings, id, onChange, on
|
|||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue) {
|
function handleChange(newValue) {
|
||||||
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
@ -48,7 +47,7 @@ export function SettingInputComponent({ option, pluginSettings, id, onChange, on
|
|||||||
value={state}
|
value={state}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={option.placeholder ?? "Enter a value"}
|
placeholder={option.placeholder ?? "Enter a value"}
|
||||||
disabled={option.disabled?.() ?? false}
|
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PluginOptionBase } from "../../../utils/types";
|
import { DefinedSettings, PluginOptionBase } from "@utils/types";
|
||||||
|
|
||||||
export interface ISettingElementProps<T extends PluginOptionBase> {
|
export interface ISettingElementProps<T extends PluginOptionBase> {
|
||||||
option: T;
|
option: T;
|
||||||
@ -27,10 +27,14 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
|
|||||||
};
|
};
|
||||||
id: string;
|
id: string;
|
||||||
onError(hasError: boolean): void;
|
onError(hasError: boolean): void;
|
||||||
|
definedSettings?: DefinedSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export * from "../../Badge";
|
||||||
export * from "./SettingBooleanComponent";
|
export * from "./SettingBooleanComponent";
|
||||||
|
export * from "./SettingCustomComponent";
|
||||||
export * from "./SettingNumericComponent";
|
export * from "./SettingNumericComponent";
|
||||||
export * from "./SettingSelectComponent";
|
export * from "./SettingSelectComponent";
|
||||||
export * from "./SettingSliderComponent";
|
export * from "./SettingSliderComponent";
|
||||||
export * from "./SettingTextComponent";
|
export * from "./SettingTextComponent";
|
||||||
|
|
||||||
|
@ -16,32 +16,38 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FormDivider, FormSection, FormText, FormTitle } from "@components/Forms";
|
import "./styles.css";
|
||||||
|
|
||||||
|
import * as DataStore from "@api/DataStore";
|
||||||
|
import { showNotice } from "@api/Notices";
|
||||||
|
import { useSettings } from "@api/settings";
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Flex } from "@components/Flex";
|
||||||
|
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||||
|
import { Badge } from "@components/PluginSettings/components";
|
||||||
|
import PluginModal from "@components/PluginSettings/PluginModal";
|
||||||
|
import { Switch } from "@components/Switch";
|
||||||
|
import { ChangeList } from "@utils/ChangeList";
|
||||||
|
import Logger from "@utils/Logger";
|
||||||
|
import { classes, LazyComponent, useAwaiter } from "@utils/misc";
|
||||||
|
import { openModalLazy } from "@utils/modal";
|
||||||
|
import { Plugin } from "@utils/types";
|
||||||
|
import { findByCode, findByPropsLazy } from "@webpack";
|
||||||
|
import { Alerts, Button, Card, Forms, Margins, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
import { showNotice } from "../../api/Notices";
|
|
||||||
import { Settings, useSettings } from "../../api/settings";
|
|
||||||
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
||||||
import { Logger, Modals } from "../../utils";
|
|
||||||
import { ChangeList } from "../../utils/ChangeList";
|
|
||||||
import { classes, lazyWebpack } from "../../utils/misc";
|
|
||||||
import { Plugin } from "../../utils/types";
|
|
||||||
import { filters } from "../../webpack";
|
|
||||||
import { Alerts, Button, Margins, Parser, React, Switch, Text, TextInput, Toasts, Tooltip } from "../../webpack/common";
|
|
||||||
import ErrorBoundary from "../ErrorBoundary";
|
|
||||||
import { ErrorCard } from "../ErrorCard";
|
|
||||||
import { Flex } from "../Flex";
|
|
||||||
import PluginModal from "./PluginModal";
|
|
||||||
import * as styles from "./styles";
|
|
||||||
|
|
||||||
|
|
||||||
|
const cl = classNameFactory("vc-plugins-");
|
||||||
const logger = new Logger("PluginSettings", "#a6d189");
|
const logger = new Logger("PluginSettings", "#a6d189");
|
||||||
|
|
||||||
const Select = lazyWebpack(filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
|
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
|
||||||
const InputStyles = lazyWebpack(filters.byProps(["inputDefault", "inputWrapper"]));
|
|
||||||
|
|
||||||
const CogWheel = lazyWebpack(filters.byCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
|
const CogWheel = LazyComponent(() => findByCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
|
||||||
const InfoIcon = lazyWebpack(filters.byCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
|
const InfoIcon = LazyComponent(() => findByCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
|
||||||
|
|
||||||
function showErrorToast(message: string) {
|
function showErrorToast(message: string) {
|
||||||
Toasts.show({
|
Toasts.show({
|
||||||
@ -54,23 +60,27 @@ function showErrorToast(message: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReloadRequiredCardProps extends React.HTMLProps<HTMLDivElement> {
|
function ReloadRequiredCard({ required }: { required: boolean; }) {
|
||||||
plugins: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function ReloadRequiredCard({ plugins, ...props }: ReloadRequiredCardProps) {
|
|
||||||
if (plugins.length === 0) return null;
|
|
||||||
|
|
||||||
const pluginPrefix = plugins.length === 1 ? "The plugin" : "The following plugins require a reload to apply changes:";
|
|
||||||
const pluginSuffix = plugins.length === 1 ? " requires a reload to apply changes." : ".";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorCard {...props} style={{ padding: "1em", display: "grid", gridTemplateColumns: "1fr auto", gap: 8, ...props.style }}>
|
<Card className={cl("info-card", { "restart-card": required })}>
|
||||||
<span style={{ margin: "auto 0" }}>
|
{required ? (
|
||||||
{pluginPrefix} <code>{plugins.join(", ")}</code>{pluginSuffix}
|
<>
|
||||||
</span>
|
<Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle>
|
||||||
<Button look={Button.Looks.INVERTED} onClick={() => location.reload()}>Reload</Button>
|
<Forms.FormText className={cl("dep-text")}>
|
||||||
</ErrorCard>
|
Restart now to apply new plugins and their settings
|
||||||
|
</Forms.FormText>
|
||||||
|
<Button color={Button.Colors.YELLOW} onClick={() => location.reload()}>
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle tag="h5">Plugin Management</Forms.FormTitle>
|
||||||
|
<Forms.FormText>Press the cog wheel or info icon to get more info on a plugin</Forms.FormText>
|
||||||
|
<Forms.FormText>Plugins with a cog wheel have settings you can modify!</Forms.FormText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,20 +88,16 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
|||||||
plugin: Plugin;
|
plugin: Plugin;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onRestartNeeded(name: string): void;
|
onRestartNeeded(name: string): void;
|
||||||
|
isNew?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave }: PluginCardProps) {
|
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
||||||
const settings = useSettings();
|
const settings = useSettings([`plugins.${plugin.name}`]).plugins[plugin.name];
|
||||||
const pluginSettings = settings.plugins[plugin.name];
|
|
||||||
|
|
||||||
const [iconHover, setIconHover] = React.useState(false);
|
const isEnabled = () => settings.enabled ?? false;
|
||||||
|
|
||||||
function isEnabled() {
|
|
||||||
return pluginSettings?.enabled || plugin.started;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openModal() {
|
function openModal() {
|
||||||
Modals.openModalLazy(async () => {
|
openModalLazy(async () => {
|
||||||
return modalProps => {
|
return modalProps => {
|
||||||
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
|
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
|
||||||
};
|
};
|
||||||
@ -110,7 +116,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||||||
return;
|
return;
|
||||||
} else if (restartNeeded) {
|
} else if (restartNeeded) {
|
||||||
// If any dependencies have patches, don't start the plugin yet.
|
// If any dependencies have patches, don't start the plugin yet.
|
||||||
pluginSettings.enabled = true;
|
settings.enabled = true;
|
||||||
onRestartNeeded(plugin.name);
|
onRestartNeeded(plugin.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -118,14 +124,14 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||||||
|
|
||||||
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
|
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
|
||||||
if (plugin.patches) {
|
if (plugin.patches) {
|
||||||
pluginSettings.enabled = !wasEnabled;
|
settings.enabled = !wasEnabled;
|
||||||
onRestartNeeded(plugin.name);
|
onRestartNeeded(plugin.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the plugin is enabled, but hasn't been started, then we can just toggle it off.
|
// If the plugin is enabled, but hasn't been started, then we can just toggle it off.
|
||||||
if (wasEnabled && !plugin.started) {
|
if (wasEnabled && !plugin.started) {
|
||||||
pluginSettings.enabled = !wasEnabled;
|
settings.enabled = !wasEnabled;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,41 +144,38 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginSettings.enabled = !wasEnabled;
|
settings.enabled = !wasEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex style={styles.PluginsGridItem} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
<Flex className={cl("card", { "card-disabled": disabled })} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||||
|
<div className={cl("card-header")}>
|
||||||
|
<Text variant="text-md/bold" className={cl("name")}>
|
||||||
|
{plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||||
|
</Text>
|
||||||
|
<button role="switch" onClick={() => openModal()} className={classes("button-12Fmur", cl("info-button"))}>
|
||||||
|
{plugin.options
|
||||||
|
? <CogWheel />
|
||||||
|
: <InfoIcon width="24" height="24" />}
|
||||||
|
</button>
|
||||||
<Switch
|
<Switch
|
||||||
|
checked={isEnabled()}
|
||||||
onChange={toggleEnabled}
|
onChange={toggleEnabled}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={isEnabled()}
|
|
||||||
note={<Text variant="text-md/normal" style={{ height: 40, overflow: "hidden" }}>{plugin.description}</Text>}
|
|
||||||
hideBorder={true}
|
|
||||||
>
|
|
||||||
<Flex style={{ marginTop: "auto", width: "100%", height: "100%", alignItems: "center" }}>
|
|
||||||
<Text variant="text-md/bold" style={{ flexGrow: "1" }}>{plugin.name}</Text>
|
|
||||||
<button role="switch" onClick={() => openModal()} style={styles.SettingsIcon} className="button-12Fmur">
|
|
||||||
{plugin.options
|
|
||||||
? <CogWheel
|
|
||||||
style={{ color: iconHover ? "" : "var(--text-muted)" }}
|
|
||||||
onMouseEnter={() => setIconHover(true)}
|
|
||||||
onMouseLeave={() => setIconHover(false)}
|
|
||||||
/>
|
/>
|
||||||
: <InfoIcon
|
</div>
|
||||||
width="24" height="24"
|
<Text className={cl("note")} variant="text-sm/normal">{plugin.description}</Text>
|
||||||
style={{ color: iconHover ? "" : "var(--text-muted)" }}
|
</Flex >
|
||||||
onMouseEnter={() => setIconHover(true)}
|
|
||||||
onMouseLeave={() => setIconHover(false)}
|
|
||||||
/>}
|
|
||||||
</button>
|
|
||||||
</Flex>
|
|
||||||
</Switch>
|
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ErrorBoundary.wrap(function Settings() {
|
enum SearchStatus {
|
||||||
|
ALL,
|
||||||
|
ENABLED,
|
||||||
|
DISABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary.wrap(function PluginSettings() {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
||||||
|
|
||||||
@ -210,51 +213,107 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||||||
return o;
|
return o;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function hasDependents(plugin: Plugin) {
|
|
||||||
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
|
||||||
return !!enabledDependants?.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
|
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
|
||||||
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = React.useState({ value: "", status: "all" });
|
const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL });
|
||||||
|
|
||||||
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
||||||
const onStatusChange = (status: string) => setSearchValue(prev => ({ ...prev, status }));
|
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
|
||||||
|
|
||||||
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
|
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
|
||||||
const showEnabled = searchValue.status === "enabled" || searchValue.status === "all";
|
const enabled = settings.plugins[plugin.name]?.enabled;
|
||||||
const showDisabled = searchValue.status === "disabled" || searchValue.status === "all";
|
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
|
||||||
const enabled = settings.plugins[plugin.name]?.enabled || plugin.started;
|
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
|
||||||
|
if (!searchValue.value.length) return true;
|
||||||
return (
|
return (
|
||||||
((showEnabled && enabled) || (showDisabled && !enabled)) &&
|
|
||||||
(
|
|
||||||
plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) ||
|
plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) ||
|
||||||
plugin.description.toLowerCase().includes(searchValue.value.toLowerCase())
|
plugin.description.toLowerCase().includes(searchValue.value.toLowerCase())
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [newPlugins] = useAwaiter(() => DataStore.get("Vencord_existingPlugins").then((cachedPlugins: Record<string, number> | undefined) => {
|
||||||
|
const now = Date.now() / 1000;
|
||||||
|
const existingTimestamps: Record<string, number> = {};
|
||||||
|
const sortedPluginNames = Object.values(sortedPlugins).map(plugin => plugin.name);
|
||||||
|
|
||||||
|
const newPlugins: string[] = [];
|
||||||
|
for (const { name: p } of sortedPlugins) {
|
||||||
|
const time = existingTimestamps[p] = cachedPlugins?.[p] ?? now;
|
||||||
|
if ((time + 60 * 60 * 24 * 2) > now) {
|
||||||
|
newPlugins.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DataStore.set("Vencord_existingPlugins", existingTimestamps);
|
||||||
|
|
||||||
|
return window._.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
|
||||||
|
}));
|
||||||
|
|
||||||
|
type P = JSX.Element | JSX.Element[];
|
||||||
|
let plugins: P, requiredPlugins: P;
|
||||||
|
if (sortedPlugins?.length) {
|
||||||
|
plugins = [];
|
||||||
|
requiredPlugins = [];
|
||||||
|
|
||||||
|
for (const p of sortedPlugins) {
|
||||||
|
if (!pluginFilter(p)) continue;
|
||||||
|
|
||||||
|
const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled);
|
||||||
|
|
||||||
|
if (isRequired) {
|
||||||
|
const tooltipText = p.required
|
||||||
|
? "This plugin is required for Vencord to function."
|
||||||
|
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
|
||||||
|
|
||||||
|
requiredPlugins.push(
|
||||||
|
<Tooltip text={tooltipText} key={p.name}>
|
||||||
|
{({ onMouseLeave, onMouseEnter }) => (
|
||||||
|
<PluginCard
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onRestartNeeded={name => changes.handleChange(name)}
|
||||||
|
disabled={true}
|
||||||
|
plugin={p}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
plugins.push(
|
||||||
|
<PluginCard
|
||||||
|
onRestartNeeded={name => changes.handleChange(name)}
|
||||||
|
disabled={false}
|
||||||
|
plugin={p}
|
||||||
|
isNew={newPlugins?.includes(p.name)}
|
||||||
|
key={p.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
plugins = requiredPlugins = <Text variant="text-md/normal">No plugins meet search criteria.</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormSection tag="h1" title="Vencord">
|
<Forms.FormSection className={Margins.marginTop16}>
|
||||||
<FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
<ReloadRequiredCard required={changes.hasChanges} />
|
||||||
Plugins
|
|
||||||
</FormTitle>
|
|
||||||
|
|
||||||
<ReloadRequiredCard plugins={[...changes.getChanges()]} style={{ marginBottom: 16 }} />
|
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
|
Filters
|
||||||
|
</Forms.FormTitle>
|
||||||
|
|
||||||
<div style={styles.FiltersBar}>
|
<div className={cl("filter-controls")}>
|
||||||
<TextInput value={searchValue.value} placeholder={"Search for a plugin..."} onChange={onSearch} style={{ marginBottom: 24 }} />
|
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.marginBottom20} />
|
||||||
<div className={InputStyles.inputWrapper}>
|
<div className={InputStyles.inputWrapper}>
|
||||||
<Select
|
<Select
|
||||||
className={InputStyles.inputDefault}
|
className={InputStyles.inputDefault}
|
||||||
options={[
|
options={[
|
||||||
{ label: "Show All", value: "all", default: true },
|
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||||
{ label: "Show Enabled", value: "enabled" },
|
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||||
{ label: "Show Disabled", value: "disabled" }
|
{ label: "Show Disabled", value: SearchStatus.DISABLED }
|
||||||
]}
|
]}
|
||||||
serialize={v => String(v)}
|
serialize={String}
|
||||||
select={onStatusChange}
|
select={onStatusChange}
|
||||||
isSelected={v => v === searchValue.status}
|
isSelected={v => v === searchValue.status}
|
||||||
closeOnSelect={true}
|
closeOnSelect={true}
|
||||||
@ -262,62 +321,32 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={styles.PluginsGrid}>
|
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
|
||||||
{sortedPlugins?.length ? sortedPlugins
|
|
||||||
.filter(a => !a.required && !dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
<div className={cl("grid")}>
|
||||||
.map(plugin => {
|
{plugins}
|
||||||
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
|
||||||
const dependency = enabledDependants?.length;
|
|
||||||
return <PluginCard
|
|
||||||
onRestartNeeded={name => changes.add(name)}
|
|
||||||
disabled={plugin.required || !!dependency}
|
|
||||||
plugin={plugin}
|
|
||||||
/>;
|
|
||||||
})
|
|
||||||
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<FormDivider />
|
|
||||||
<FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
<Forms.FormDivider className={Margins.marginTop20} />
|
||||||
|
|
||||||
|
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
Required Plugins
|
Required Plugins
|
||||||
</FormTitle>
|
</Forms.FormTitle>
|
||||||
<div style={styles.PluginsGrid}>
|
<div className={cl("grid")}>
|
||||||
{sortedPlugins?.length ? sortedPlugins
|
{requiredPlugins}
|
||||||
.filter(a => a.required || dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
|
||||||
.map(plugin => {
|
|
||||||
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
|
||||||
const dependency = enabledDependants?.length;
|
|
||||||
const tooltipText = plugin.required
|
|
||||||
? "This plugin is required for Vencord to function."
|
|
||||||
: makeDependencyList(dependencyCheck(plugin.name, depMap));
|
|
||||||
return <Tooltip text={tooltipText}>
|
|
||||||
{({ onMouseLeave, onMouseEnter }) => (
|
|
||||||
<PluginCard
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onRestartNeeded={name => changes.add(name)}
|
|
||||||
disabled={plugin.required || !!dependency}
|
|
||||||
plugin={plugin}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Tooltip>;
|
|
||||||
})
|
|
||||||
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</Forms.FormSection >
|
||||||
);
|
);
|
||||||
|
}, {
|
||||||
|
message: "Failed to render the Plugin Settings. If this persists, try using the installer to reinstall!",
|
||||||
|
onError: handleComponentFailed,
|
||||||
});
|
});
|
||||||
|
|
||||||
function makeDependencyList(deps: string[]) {
|
function makeDependencyList(deps: string[]) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<FormText>This plugin is required by:</FormText>
|
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
||||||
{deps.map((dep: string) => <FormText style={{ margin: "0 auto" }}>{dep}</FormText>)}
|
{deps.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function dependencyCheck(pluginName: string, depMap: Record<string, string[]>): string[] {
|
|
||||||
return depMap[pluginName]?.filter(d => Settings.plugins[d].enabled) || [];
|
|
||||||
}
|
|
||||||
|
138
src/components/PluginSettings/styles.css
Normal file
138
src/components/PluginSettings/styles.css
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.vc-plugins-grid {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 16px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-card {
|
||||||
|
background-color: var(--background-secondary-alt);
|
||||||
|
color: var(--interactive-active);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
width: 100%;
|
||||||
|
transition: 0.1s ease-out;
|
||||||
|
transition-property: box-shadow, transform, background, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-card-disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-card:hover {
|
||||||
|
background-color: var(--background-tertiary);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--elevation-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-card-header {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 1.5rem;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-info-button {
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-settings-button:hover {
|
||||||
|
color: var(--interactive-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-filter-controls {
|
||||||
|
display: grid;
|
||||||
|
height: 40px;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: 1fr 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-badge {
|
||||||
|
padding: 0 6px;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: var(--white-500);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-note {
|
||||||
|
height: 36px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
/* stylelint-disable-next-line property-no-unknown */
|
||||||
|
box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-name {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-dep-name {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-info-card {
|
||||||
|
padding: 1em;
|
||||||
|
height: 8em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-info-card div {
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-restart-card {
|
||||||
|
padding: 1em;
|
||||||
|
background: var(--info-warning-background);
|
||||||
|
border: 1px solid var(--info-warning-foreground);
|
||||||
|
color: var(--info-warning-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-restart-card button {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugins-info-button svg:not(:hover, :focus) {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const PluginsGrid: React.CSSProperties = {
|
|
||||||
marginTop: 16,
|
|
||||||
display: "grid",
|
|
||||||
gridGap: 16,
|
|
||||||
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PluginsGridItem: React.CSSProperties = {
|
|
||||||
backgroundColor: "var(--background-modifier-selected)",
|
|
||||||
color: "var(--interactive-active)",
|
|
||||||
borderRadius: 3,
|
|
||||||
cursor: "pointer",
|
|
||||||
display: "block",
|
|
||||||
height: "min-content",
|
|
||||||
padding: 10,
|
|
||||||
width: "100%",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FiltersBar: React.CSSProperties = {
|
|
||||||
gap: 10,
|
|
||||||
height: 40,
|
|
||||||
gridTemplateColumns: "1fr 150px",
|
|
||||||
display: "grid"
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SettingsIcon: React.CSSProperties = {
|
|
||||||
height: "24px",
|
|
||||||
width: "24px",
|
|
||||||
padding: "0",
|
|
||||||
background: "transparent",
|
|
||||||
marginRight: 8
|
|
||||||
};
|
|
@ -1,121 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { FormDivider, FormSection, FormText, FormTitle } from "@components/Forms";
|
|
||||||
|
|
||||||
import { useSettings } from "../api/settings";
|
|
||||||
import { ChangeList } from "../utils/ChangeList";
|
|
||||||
import IpcEvents from "../utils/IpcEvents";
|
|
||||||
import { useAwaiter } from "../utils/misc";
|
|
||||||
import { Alerts, Button, Margins, Parser, React, Switch } from "../webpack/common";
|
|
||||||
import ErrorBoundary from "./ErrorBoundary";
|
|
||||||
import { Flex } from "./Flex";
|
|
||||||
import { launchMonacoEditor } from "./Monaco";
|
|
||||||
|
|
||||||
export default ErrorBoundary.wrap(function Settings() {
|
|
||||||
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading...");
|
|
||||||
const settings = useSettings();
|
|
||||||
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
return () => void (changes.hasChanges && Alerts.show({
|
|
||||||
title: "Restart required",
|
|
||||||
body: (
|
|
||||||
<>
|
|
||||||
<p>The following plugins require a restart:</p>
|
|
||||||
<div>{changes.map((s, i) => (
|
|
||||||
<>
|
|
||||||
{i > 0 && ", "}
|
|
||||||
{Parser.parse("`" + s + "`")}
|
|
||||||
</>
|
|
||||||
))}</div>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
confirmText: "Restart now",
|
|
||||||
cancelText: "Later!",
|
|
||||||
onConfirm: () => location.reload()
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormSection tag="h1" title="Vencord">
|
|
||||||
<FormTitle tag="h5">
|
|
||||||
Settings
|
|
||||||
</FormTitle>
|
|
||||||
|
|
||||||
<FormText>
|
|
||||||
Settings Directory: <code style={{ userSelect: "text", cursor: "text" }}>{settingsDir}</code>
|
|
||||||
</FormText>
|
|
||||||
|
|
||||||
{!IS_WEB && <Flex className={Margins.marginBottom20} style={{ marginTop: 8 }}>
|
|
||||||
<Button
|
|
||||||
onClick={() => window.DiscordNative.app.relaunch()}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
color={Button.Colors.GREEN}
|
|
||||||
>
|
|
||||||
Reload
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
disabled={settingsDirPending}
|
|
||||||
>
|
|
||||||
Launch Directory
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_MONACO_EDITOR)}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
disabled={settingsDir === "Loading..."}
|
|
||||||
>
|
|
||||||
Open QuickCSS File
|
|
||||||
</Button>
|
|
||||||
</Flex>}
|
|
||||||
|
|
||||||
{IS_WEB && <Button
|
|
||||||
onClick={launchMonacoEditor}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
disabled={settingsDir === "Loading..."}
|
|
||||||
>
|
|
||||||
Open QuickCSS File
|
|
||||||
</Button>}
|
|
||||||
|
|
||||||
<FormDivider />
|
|
||||||
<Switch
|
|
||||||
value={settings.useQuickCss}
|
|
||||||
onChange={(v: boolean) => settings.useQuickCss = v}
|
|
||||||
note="Loads styles from your QuickCss file"
|
|
||||||
>
|
|
||||||
Use QuickCss
|
|
||||||
</Switch>
|
|
||||||
{!IS_WEB && <Switch
|
|
||||||
value={settings.enableReactDevtools}
|
|
||||||
onChange={(v: boolean) => settings.enableReactDevtools = v}
|
|
||||||
note="Requires a full restart"
|
|
||||||
>
|
|
||||||
Enable React Developer Tools
|
|
||||||
</Switch>}
|
|
||||||
{!IS_WEB && <Switch
|
|
||||||
value={settings.notifyAboutUpdates}
|
|
||||||
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
|
||||||
note="Shows a Toast on StartUp"
|
|
||||||
>
|
|
||||||
Get notified about new Updates
|
|
||||||
</Switch>}
|
|
||||||
</FormSection>
|
|
||||||
);
|
|
||||||
});
|
|
3
src/components/Switch.css
Normal file
3
src/components/Switch.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.vc-switch-slider {
|
||||||
|
transition: 100ms transform ease-in-out;
|
||||||
|
}
|
76
src/components/Switch.tsx
Normal file
76
src/components/Switch.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./Switch.css";
|
||||||
|
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
|
||||||
|
interface SwitchProps {
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SWITCH_ON = "var(--green-360)";
|
||||||
|
const SWITCH_OFF = "var(--primary-400)";
|
||||||
|
const SwitchClasses = findByPropsLazy("slider", "input", "container");
|
||||||
|
|
||||||
|
export function Switch({ checked, onChange, disabled }: SwitchProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={`${SwitchClasses.container} default-colors`} style={{
|
||||||
|
backgroundColor: checked ? SWITCH_ON : SWITCH_OFF,
|
||||||
|
opacity: disabled ? 0.3 : 1
|
||||||
|
}}>
|
||||||
|
<svg
|
||||||
|
className={SwitchClasses.slider + " vc-switch-slider"}
|
||||||
|
viewBox="0 0 28 20"
|
||||||
|
preserveAspectRatio="xMinYMid meet"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
transform: checked ? "translateX(12px)" : "translateX(-3px)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<rect fill="white" x="4" y="0" height="20" width="20" rx="10" />
|
||||||
|
<svg viewBox="0 0 20 20" fill="none">
|
||||||
|
{checked ? (
|
||||||
|
<>
|
||||||
|
<path fill={SWITCH_ON} d="M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z" />
|
||||||
|
<path fill={SWITCH_ON} d="M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<path fill={SWITCH_OFF} d="M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z" />
|
||||||
|
<path fill={SWITCH_OFF} d="M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
disabled={disabled}
|
||||||
|
type="checkbox"
|
||||||
|
className={SwitchClasses.input}
|
||||||
|
tabIndex={0}
|
||||||
|
checked={checked}
|
||||||
|
onChange={e => onChange(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
64
src/components/VencordSettings/BackupRestoreTab.tsx
Normal file
64
src/components/VencordSettings/BackupRestoreTab.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Flex } from "@components/Flex";
|
||||||
|
import { classes } from "@utils/misc";
|
||||||
|
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
||||||
|
import { Button, Card, Forms, Margins, Text } from "@webpack/common";
|
||||||
|
|
||||||
|
function BackupRestoreTab() {
|
||||||
|
return (
|
||||||
|
<Forms.FormSection title="Settings Sync" className={Margins.marginTop16}>
|
||||||
|
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
||||||
|
<Flex flexDirection="column">
|
||||||
|
<strong>Warning</strong>
|
||||||
|
<span>Importing a settings file will overwrite your current settings.</span>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
||||||
|
You can import and export your Vencord settings as a JSON file.
|
||||||
|
This allows you to easily transfer your settings to another device,
|
||||||
|
or recover your settings after reinstalling Vencord or Discord.
|
||||||
|
</Text>
|
||||||
|
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
||||||
|
Settings Export contains:
|
||||||
|
<ul>
|
||||||
|
<li>— Custom QuickCSS</li>
|
||||||
|
<li>— Plugin Settings</li>
|
||||||
|
</ul>
|
||||||
|
</Text>
|
||||||
|
<Flex>
|
||||||
|
<Button
|
||||||
|
onClick={() => uploadSettingsBackup()}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
>
|
||||||
|
Import Settings
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={downloadSettingsBackup}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
>
|
||||||
|
Export Settings
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Forms.FormSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary.wrap(BackupRestoreTab);
|
22
src/components/VencordSettings/PluginsTab.tsx
Normal file
22
src/components/VencordSettings/PluginsTab.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import PluginSettings from "@components/PluginSettings";
|
||||||
|
|
||||||
|
export default ErrorBoundary.wrap(PluginSettings);
|
132
src/components/VencordSettings/ThemesTab.tsx
Normal file
132
src/components/VencordSettings/ThemesTab.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useSettings } from "@api/settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Link } from "@components/Link";
|
||||||
|
import { useAwaiter } from "@utils/misc";
|
||||||
|
import { findLazy } from "@webpack";
|
||||||
|
import { Card, Forms, Margins, React, TextArea } from "@webpack/common";
|
||||||
|
|
||||||
|
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
||||||
|
|
||||||
|
function Validator({ link }: { link: string; }) {
|
||||||
|
const [res, err, pending] = useAwaiter(() => fetch(link).then(res => {
|
||||||
|
if (res.status > 300) throw `${res.status} ${res.statusText}`;
|
||||||
|
const contentType = res.headers.get("Content-Type");
|
||||||
|
if (!contentType?.startsWith("text/css") && !contentType?.startsWith("text/plain"))
|
||||||
|
throw "Not a CSS file. Remember to use the raw link!";
|
||||||
|
|
||||||
|
return "Okay!";
|
||||||
|
}));
|
||||||
|
|
||||||
|
const text = pending
|
||||||
|
? "Checking..."
|
||||||
|
: err
|
||||||
|
? `Error: ${err instanceof Error ? err.message : String(err)}`
|
||||||
|
: "Valid!";
|
||||||
|
|
||||||
|
return <Forms.FormText style={{
|
||||||
|
color: pending ? "var(--text-muted)" : err ? "var(--text-danger)" : "var(--text-positive)"
|
||||||
|
}}>{text}</Forms.FormText>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
||||||
|
if (!themeLinks.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle className={Margins.marginTop20} tag="h5">Validator</Forms.FormTitle>
|
||||||
|
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
||||||
|
<div>
|
||||||
|
{themeLinks.map(link => (
|
||||||
|
<Card style={{
|
||||||
|
padding: ".5em",
|
||||||
|
marginBottom: ".5em",
|
||||||
|
marginTop: ".5em"
|
||||||
|
}} key={link}>
|
||||||
|
<Forms.FormTitle tag="h5" style={{
|
||||||
|
overflowWrap: "break-word"
|
||||||
|
}}>
|
||||||
|
{link}
|
||||||
|
</Forms.FormTitle>
|
||||||
|
<Validator link={link} />
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary.wrap(function () {
|
||||||
|
const settings = useSettings();
|
||||||
|
const [themeText, setThemeText] = React.useState(settings.themeLinks.join("\n"));
|
||||||
|
|
||||||
|
function onBlur() {
|
||||||
|
settings.themeLinks = [...new Set(
|
||||||
|
themeText
|
||||||
|
.trim()
|
||||||
|
.split(/\n+/)
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card className="vc-settings-card">
|
||||||
|
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
|
||||||
|
<Forms.FormText>One link per line</Forms.FormText>
|
||||||
|
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
|
||||||
|
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
||||||
|
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
||||||
|
<div style={{ marginBottom: ".5em" }}>
|
||||||
|
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
||||||
|
BetterDiscord Themes
|
||||||
|
</Link>
|
||||||
|
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
|
||||||
|
</div>
|
||||||
|
<Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText>
|
||||||
|
<Forms.FormText>In the GitHub repository of your theme, find X.theme.css / X.css, click on it, then click the "Raw" button</Forms.FormText>
|
||||||
|
<Forms.FormText>
|
||||||
|
If the theme has configuration that requires you to edit the file:
|
||||||
|
<ul>
|
||||||
|
<li>• Make a <Link href="https://github.com/signup">GitHub</Link> account</li>
|
||||||
|
<li>• Click the fork button on the top right</li>
|
||||||
|
<li>• Edit the file</li>
|
||||||
|
<li>• Use the link to your own repository instead</li>
|
||||||
|
</ul>
|
||||||
|
</Forms.FormText>
|
||||||
|
</Card>
|
||||||
|
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
|
||||||
|
<TextArea
|
||||||
|
style={{
|
||||||
|
padding: ".5em",
|
||||||
|
border: "1px solid var(--background-modifier-accent)"
|
||||||
|
}}
|
||||||
|
value={themeText}
|
||||||
|
onChange={e => setThemeText(e.currentTarget.value)}
|
||||||
|
className={TextAreaProps.textarea}
|
||||||
|
placeholder="Theme Links"
|
||||||
|
spellCheck={false}
|
||||||
|
onBlur={onBlur}
|
||||||
|
/>
|
||||||
|
<Validators themeLinks={settings.themeLinks} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -16,18 +16,18 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Forms } from "@components";
|
import { useSettings } from "@api/settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
|
import { Flex } from "@components/Flex";
|
||||||
|
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||||
|
import { Link } from "@components/Link";
|
||||||
|
import { classes, useAwaiter } from "@utils/misc";
|
||||||
|
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
||||||
|
import { Alerts, Button, Card, Forms, Margins, Parser, React, Switch, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
import { classes, useAwaiter } from "../utils/misc";
|
|
||||||
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "../utils/updater";
|
|
||||||
import { Alerts, Button, Card, Margins, Parser, React, Toasts } from "../webpack/common";
|
|
||||||
import ErrorBoundary from "./ErrorBoundary";
|
|
||||||
import { ErrorCard } from "./ErrorCard";
|
|
||||||
import { Flex } from "./Flex";
|
|
||||||
import { Link } from "./Link";
|
|
||||||
|
|
||||||
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
||||||
return async () => {
|
return async () => {
|
||||||
dispatcher(true);
|
dispatcher(true);
|
||||||
@ -70,14 +70,18 @@ interface CommonProps {
|
|||||||
repoPending: boolean;
|
repoPending: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string, disabled?: boolean; }) {
|
||||||
|
return <Link href={`${repo}/commit/${hash}`} disabled={disabled}>
|
||||||
|
{hash}
|
||||||
|
</Link>;
|
||||||
|
}
|
||||||
|
|
||||||
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
|
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
|
||||||
return (
|
return (
|
||||||
<Card style={{ padding: ".5em" }}>
|
<Card style={{ padding: ".5em" }}>
|
||||||
{updates.map(({ hash, author, message }) => (
|
{updates.map(({ hash, author, message }) => (
|
||||||
<div>
|
<div>
|
||||||
<Link href={`${repo}/commit/${hash}`} disabled={repoPending}>
|
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
|
||||||
<code>{hash}</code>
|
|
||||||
</Link>
|
|
||||||
<span style={{
|
<span style={{
|
||||||
marginLeft: "0.5em",
|
marginLeft: "0.5em",
|
||||||
color: "var(--text-normal)"
|
color: "var(--text-normal)"
|
||||||
@ -180,7 +184,9 @@ function Newer(props: CommonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Updater() {
|
function Updater() {
|
||||||
const [repo, err, repoPending] = useAwaiter(getRepo, "Loading...");
|
const settings = useSettings(["notifyAboutUpdates", "autoUpdate"]);
|
||||||
|
|
||||||
|
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (err)
|
if (err)
|
||||||
@ -193,16 +199,33 @@ function Updater() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection tag="h1" title="Vencord Updater">
|
<Forms.FormSection className={Margins.marginTop16}>
|
||||||
|
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||||
|
<Switch
|
||||||
|
value={settings.notifyAboutUpdates}
|
||||||
|
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||||
|
note="Shows a toast on startup"
|
||||||
|
disabled={settings.autoUpdate}
|
||||||
|
>
|
||||||
|
Get notified about new updates
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
value={settings.autoUpdate}
|
||||||
|
onChange={(v: boolean) => settings.autoUpdate = v}
|
||||||
|
note="Automatically update Vencord without confirmation prompt"
|
||||||
|
>
|
||||||
|
Automatically update
|
||||||
|
</Switch>
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
|
||||||
|
|
||||||
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
|
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
|
||||||
<Link href={repo}>
|
<Link href={repo}>
|
||||||
{repo.split("/").slice(-2).join("/")}
|
{repo.split("/").slice(-2).join("/")}
|
||||||
</Link>
|
</Link>
|
||||||
)} ({gitHash})</Forms.FormText>
|
)} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText>
|
||||||
|
|
||||||
<Forms.FormDivider />
|
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
||||||
|
|
||||||
@ -211,4 +234,7 @@ function Updater() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IS_WEB ? null : ErrorBoundary.wrap(Updater);
|
export default IS_WEB ? null : ErrorBoundary.wrap(Updater, {
|
||||||
|
message: "Failed to render the Updater. If this persists, try using the installer to reinstall!",
|
||||||
|
onError: handleComponentFailed,
|
||||||
|
});
|
225
src/components/VencordSettings/VencordTab.tsx
Normal file
225
src/components/VencordSettings/VencordTab.tsx
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import { useSettings } from "@api/settings";
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import DonateButton from "@components/DonateButton";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { identity, useAwaiter } from "@utils/misc";
|
||||||
|
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
|
||||||
|
|
||||||
|
const cl = classNameFactory("vc-settings-");
|
||||||
|
|
||||||
|
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
|
||||||
|
const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png";
|
||||||
|
|
||||||
|
type KeysOfType<Object, Type> = {
|
||||||
|
[K in keyof Object]: Object[K] extends Type ? K : never;
|
||||||
|
}[keyof Object];
|
||||||
|
|
||||||
|
function VencordSettings() {
|
||||||
|
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
|
||||||
|
fallbackValue: "Loading..."
|
||||||
|
});
|
||||||
|
const settings = useSettings();
|
||||||
|
const notifSettings = settings.notifications;
|
||||||
|
|
||||||
|
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
|
||||||
|
|
||||||
|
const isWindows = navigator.platform.toLowerCase().startsWith("win");
|
||||||
|
|
||||||
|
const Switches: Array<false | {
|
||||||
|
key: KeysOfType<typeof settings, boolean>;
|
||||||
|
title: string;
|
||||||
|
note: string;
|
||||||
|
}> =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: "useQuickCss",
|
||||||
|
title: "Enable Custom CSS",
|
||||||
|
note: "Loads your Custom CSS"
|
||||||
|
},
|
||||||
|
!IS_WEB && {
|
||||||
|
key: "enableReactDevtools",
|
||||||
|
title: "Enable React Developer Tools",
|
||||||
|
note: "Requires a full restart"
|
||||||
|
},
|
||||||
|
!IS_WEB && !isWindows && {
|
||||||
|
key: "frameless",
|
||||||
|
title: "Disable the window frame",
|
||||||
|
note: "Requires a full restart"
|
||||||
|
},
|
||||||
|
!IS_WEB && {
|
||||||
|
key: "transparent",
|
||||||
|
title: "Enable window transparency",
|
||||||
|
note: "Requires a full restart"
|
||||||
|
},
|
||||||
|
!IS_WEB && isWindows && {
|
||||||
|
key: "winCtrlQ",
|
||||||
|
title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
|
||||||
|
note: "Requires a full restart"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<DonateCard image={donateImage} />
|
||||||
|
<Forms.FormSection title="Quick Actions">
|
||||||
|
<Card className={cl("quick-actions-card")}>
|
||||||
|
{IS_WEB ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => require("../Monaco").launchMonacoEditor()}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={settingsDir === "Loading..."}>
|
||||||
|
Open QuickCSS File
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<React.Fragment>
|
||||||
|
<Button
|
||||||
|
onClick={() => window.DiscordNative.app.relaunch()}
|
||||||
|
size={Button.Sizes.SMALL}>
|
||||||
|
Restart Client
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_MONACO_EDITOR)}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={settingsDir === "Loading..."}>
|
||||||
|
Open QuickCSS File
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={settingsDirPending}>
|
||||||
|
Open Settings Folder
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/Vendicated/Vencord")}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={settingsDirPending}>
|
||||||
|
Open in GitHub
|
||||||
|
</Button>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Forms.FormSection>
|
||||||
|
|
||||||
|
<Forms.FormDivider />
|
||||||
|
|
||||||
|
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
|
||||||
|
<Forms.FormText className={Margins.bottom20}>
|
||||||
|
Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
|
||||||
|
</Forms.FormText>
|
||||||
|
{Switches.map(s => s && (
|
||||||
|
<Switch
|
||||||
|
key={s.key}
|
||||||
|
value={settings[s.key]}
|
||||||
|
onChange={v => settings[s.key] = v}
|
||||||
|
note={s.note}
|
||||||
|
>
|
||||||
|
{s.title}
|
||||||
|
</Switch>
|
||||||
|
))}
|
||||||
|
</Forms.FormSection>
|
||||||
|
|
||||||
|
|
||||||
|
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
|
||||||
|
{notifSettings.useNative !== "never" && Notification.permission === "denied" && (
|
||||||
|
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
|
||||||
|
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
|
||||||
|
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
|
||||||
|
</ErrorCard>
|
||||||
|
)}
|
||||||
|
<Forms.FormText className={Margins.bottom8}>
|
||||||
|
Some plugins may show you notifications. These come in two styles:
|
||||||
|
<ul>
|
||||||
|
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
|
||||||
|
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
|
||||||
|
</ul>
|
||||||
|
</Forms.FormText>
|
||||||
|
<Select
|
||||||
|
placeholder="Notification Style"
|
||||||
|
options={[
|
||||||
|
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
|
||||||
|
{ label: "Always use Desktop notifications", value: "always" },
|
||||||
|
{ label: "Always use Vencord notifications", value: "never" },
|
||||||
|
]satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
|
||||||
|
closeOnSelect={true}
|
||||||
|
select={v => notifSettings.useNative = v}
|
||||||
|
isSelected={v => v === notifSettings.useNative}
|
||||||
|
serialize={identity}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
|
||||||
|
<Select
|
||||||
|
isDisabled={notifSettings.useNative === "always"}
|
||||||
|
placeholder="Notification Position"
|
||||||
|
options={[
|
||||||
|
{ label: "Bottom Right", value: "bottom-right", default: true },
|
||||||
|
{ label: "Top Right", value: "top-right" },
|
||||||
|
]satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
|
||||||
|
select={v => notifSettings.position = v}
|
||||||
|
isSelected={v => v === notifSettings.position}
|
||||||
|
serialize={identity}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
|
||||||
|
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
|
||||||
|
<Slider
|
||||||
|
disabled={notifSettings.useNative === "always"}
|
||||||
|
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
|
||||||
|
minValue={0}
|
||||||
|
maxValue={20_000}
|
||||||
|
initialValue={notifSettings.timeout}
|
||||||
|
onValueChange={v => notifSettings.timeout = v}
|
||||||
|
onValueRender={v => (v / 1000).toFixed(2) + "s"}
|
||||||
|
onMarkerRender={v => (v / 1000) + "s"}
|
||||||
|
stickToMarkers={false}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface DonateCardProps {
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DonateCard({ image }: DonateCardProps) {
|
||||||
|
return (
|
||||||
|
<Card className={cl("card", "donate")}>
|
||||||
|
<div>
|
||||||
|
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle>
|
||||||
|
<Forms.FormText>Please consider supporting the development of Vencord by donating!</Forms.FormText>
|
||||||
|
<DonateButton style={{ transform: "translateX(-1em)" }} />
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
role="presentation"
|
||||||
|
src={image}
|
||||||
|
alt=""
|
||||||
|
height={128}
|
||||||
|
style={{ marginLeft: "auto", transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : "" }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary.wrap(VencordSettings);
|
89
src/components/VencordSettings/index.tsx
Normal file
89
src/components/VencordSettings/index.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./settingsStyles.css";
|
||||||
|
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { findByCodeLazy } from "@webpack";
|
||||||
|
import { Forms, SettingsRouter, Text } from "@webpack/common";
|
||||||
|
|
||||||
|
import BackupRestoreTab from "./BackupRestoreTab";
|
||||||
|
import PluginsTab from "./PluginsTab";
|
||||||
|
import ThemesTab from "./ThemesTab";
|
||||||
|
import Updater from "./Updater";
|
||||||
|
import VencordSettings from "./VencordTab";
|
||||||
|
|
||||||
|
const cl = classNameFactory("vc-settings-");
|
||||||
|
|
||||||
|
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');
|
||||||
|
|
||||||
|
interface SettingsProps {
|
||||||
|
tab: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsTab {
|
||||||
|
name: string;
|
||||||
|
component?: React.ComponentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsTabs: Record<string, SettingsTab> = {
|
||||||
|
VencordSettings: { name: "Vencord", component: () => <VencordSettings /> },
|
||||||
|
VencordPlugins: { name: "Plugins", component: () => <PluginsTab /> },
|
||||||
|
VencordThemes: { name: "Themes", component: () => <ThemesTab /> },
|
||||||
|
VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false
|
||||||
|
VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater />;
|
||||||
|
|
||||||
|
function Settings(props: SettingsProps) {
|
||||||
|
const { tab = "VencordSettings" } = props;
|
||||||
|
|
||||||
|
const CurrentTab = SettingsTabs[tab]?.component;
|
||||||
|
|
||||||
|
return <Forms.FormSection>
|
||||||
|
<Text variant="heading-md/normal" tag="h2">Vencord Settings</Text>
|
||||||
|
|
||||||
|
<TabBar
|
||||||
|
type={TabBar.Types.TOP}
|
||||||
|
look={TabBar.Looks.BRAND}
|
||||||
|
className={cl("tab-bar")}
|
||||||
|
selectedItem={tab}
|
||||||
|
onItemSelect={SettingsRouter.open}
|
||||||
|
>
|
||||||
|
{Object.entries(SettingsTabs).map(([key, { name, component }]) => {
|
||||||
|
if (!component) return null;
|
||||||
|
return <TabBar.Item
|
||||||
|
id={key}
|
||||||
|
className={cl("tab-bar-item")}
|
||||||
|
key={key}>
|
||||||
|
{name}
|
||||||
|
</TabBar.Item>;
|
||||||
|
})}
|
||||||
|
</TabBar>
|
||||||
|
<Forms.FormDivider />
|
||||||
|
{CurrentTab && <CurrentTab />}
|
||||||
|
</Forms.FormSection >;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (props: SettingsProps) {
|
||||||
|
return <ErrorBoundary>
|
||||||
|
<Settings tab={props.tab} />
|
||||||
|
</ErrorBoundary>;
|
||||||
|
}
|
40
src/components/VencordSettings/settingsStyles.css
Normal file
40
src/components/VencordSettings/settingsStyles.css
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
.vc-settings-tab-bar {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
border-bottom: 2px solid var(--background-modifier-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-settings-tab-bar-item {
|
||||||
|
margin-right: 32px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-settings-quick-actions-card {
|
||||||
|
padding: 1em;
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-settings-donate {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-settings-card {
|
||||||
|
padding: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-backup-restore-card {
|
||||||
|
background-color: var(--info-warning-background);
|
||||||
|
border-color: var(--info-warning-foreground);
|
||||||
|
color: var(--info-warning-text);
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
44
src/components/handleComponentFailed.ts
Normal file
44
src/components/handleComponentFailed.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isOutdated, rebuild, update } from "@utils/updater";
|
||||||
|
|
||||||
|
export async function handleComponentFailed() {
|
||||||
|
if (isOutdated) {
|
||||||
|
setImmediate(async () => {
|
||||||
|
const wantsUpdate = confirm(
|
||||||
|
"Uh Oh! Failed to render this Page." +
|
||||||
|
" However, there is an update available that might fix it." +
|
||||||
|
" Would you like to update and restart now?"
|
||||||
|
);
|
||||||
|
if (wantsUpdate) {
|
||||||
|
try {
|
||||||
|
await update();
|
||||||
|
await rebuild();
|
||||||
|
if (IS_WEB)
|
||||||
|
location.reload();
|
||||||
|
else
|
||||||
|
DiscordNative.app.relaunch();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("That also failed :( Try updating or reinstalling with the installer!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,6 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export { default as PatchHelper } from "./PatchHelper";
|
||||||
export { default as PluginSettings } from "./PluginSettings";
|
export { default as PluginSettings } from "./PluginSettings";
|
||||||
export { default as Settings } from "./Settings";
|
export { default as VencordSettings } from "./VencordSettings";
|
||||||
export { default as Updater } from "./Updater";
|
|
||||||
|
64
src/debug/Tracer.ts
Normal file
64
src/debug/Tracer.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Logger from "@utils/Logger";
|
||||||
|
|
||||||
|
if (IS_DEV) {
|
||||||
|
var traces = {} as Record<string, [number, any[]]>;
|
||||||
|
var logger = new Logger("Tracer", "#FFD166");
|
||||||
|
}
|
||||||
|
|
||||||
|
const noop = function () { };
|
||||||
|
|
||||||
|
export const beginTrace = !IS_DEV ? noop :
|
||||||
|
function beginTrace(name: string, ...args: any[]) {
|
||||||
|
if (name in traces)
|
||||||
|
throw new Error(`Trace ${name} already exists!`);
|
||||||
|
|
||||||
|
traces[name] = [performance.now(), args];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const finishTrace = !IS_DEV ? noop : function finishTrace(name: string) {
|
||||||
|
const end = performance.now();
|
||||||
|
|
||||||
|
const [start, args] = traces[name];
|
||||||
|
delete traces[name];
|
||||||
|
|
||||||
|
logger.debug(`${name} took ${end - start}ms`, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
type Func = (...args: any[]) => any;
|
||||||
|
type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;
|
||||||
|
|
||||||
|
const noopTracer =
|
||||||
|
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
|
||||||
|
|
||||||
|
export const traceFunction = !IS_DEV
|
||||||
|
? noopTracer
|
||||||
|
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
|
||||||
|
return function (this: any, ...args: Parameters<F>) {
|
||||||
|
const traceName = mapper?.(...args) ?? name;
|
||||||
|
|
||||||
|
beginTrace(traceName, ...arguments);
|
||||||
|
try {
|
||||||
|
return f.apply(this, args);
|
||||||
|
} finally {
|
||||||
|
finishTrace(traceName);
|
||||||
|
}
|
||||||
|
} as F;
|
||||||
|
};
|
12
src/globals.d.ts
vendored
12
src/globals.d.ts
vendored
@ -16,11 +16,13 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { LoDashStatic } from "lodash";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
/**
|
/**
|
||||||
* This exists only at build time, so references to it in patches should insert it
|
* This exists only at build time, so references to it in patches should insert it
|
||||||
* via String interpolation OR use different replacement code based on this
|
* via String interpolation OR use different replacement code based on this
|
||||||
* but NEVER refrence it inside the patched code
|
* but NEVER reference it inside the patched code
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // BAD
|
* // BAD
|
||||||
@ -31,10 +33,17 @@ declare global {
|
|||||||
* replace: `${IS_WEB}?foo:bar`
|
* replace: `${IS_WEB}?foo:bar`
|
||||||
*/
|
*/
|
||||||
export var IS_WEB: boolean;
|
export var IS_WEB: boolean;
|
||||||
|
export var IS_DEV: boolean;
|
||||||
export var IS_STANDALONE: boolean;
|
export var IS_STANDALONE: boolean;
|
||||||
|
|
||||||
export var VencordNative: typeof import("./VencordNative").default;
|
export var VencordNative: typeof import("./VencordNative").default;
|
||||||
export var Vencord: typeof import("./Vencord");
|
export var Vencord: typeof import("./Vencord");
|
||||||
|
export var VencordStyles: Map<string, {
|
||||||
|
name: string;
|
||||||
|
source: string;
|
||||||
|
classNames: Record<string, string>;
|
||||||
|
dom: HTMLStyleElement | null;
|
||||||
|
}>;
|
||||||
export var appSettings: {
|
export var appSettings: {
|
||||||
set(setting: string, v: any): void;
|
set(setting: string, v: any): void;
|
||||||
};
|
};
|
||||||
@ -52,6 +61,7 @@ declare global {
|
|||||||
push(chunk: any): any;
|
push(chunk: any): any;
|
||||||
pop(): any;
|
pop(): any;
|
||||||
};
|
};
|
||||||
|
_: LoDashStatic;
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,9 +67,18 @@ export async function installExt(id: string) {
|
|||||||
try {
|
try {
|
||||||
await access(extDir, fsConstants.F_OK);
|
await access(extDir, fsConstants.F_OK);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`;
|
const url = id === "fmkadmapgofadopljbjfkapdkoienihi"
|
||||||
const buf = await get(url);
|
// React Devtools v4.25
|
||||||
await extract(crxToZip(buf), extDir);
|
// v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843
|
||||||
|
// Unfortunately, Google does not serve old versions, so this is the only way
|
||||||
|
? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip"
|
||||||
|
: `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`;
|
||||||
|
const buf = await get(url, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Vencord (https://github.com/Vendicated/Vencord)"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await extract(crxToZip(buf), extDir).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
session.defaultSession.loadExtension(extDir);
|
session.defaultSession.loadExtension(extDir);
|
||||||
|
@ -18,16 +18,16 @@
|
|||||||
|
|
||||||
import "./updater";
|
import "./updater";
|
||||||
|
|
||||||
import { BrowserWindow, desktopCapturer, ipcMain, shell } from "electron";
|
import { debounce } from "@utils/debounce";
|
||||||
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
|
import { Queue } from "@utils/Queue";
|
||||||
|
import { BrowserWindow, ipcMain, shell } from "electron";
|
||||||
import { mkdirSync, readFileSync, watch } from "fs";
|
import { mkdirSync, readFileSync, watch } from "fs";
|
||||||
import { open, readFile, writeFile } from "fs/promises";
|
import { open, readFile, writeFile } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
|
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
|
||||||
|
|
||||||
import { debounce } from "../utils/debounce";
|
|
||||||
import IpcEvents from "../utils/IpcEvents";
|
|
||||||
import { Queue } from "../utils/Queue";
|
|
||||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
|
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
|
||||||
|
|
||||||
mkdirSync(SETTINGS_DIR, { recursive: true });
|
mkdirSync(SETTINGS_DIR, { recursive: true });
|
||||||
@ -44,9 +44,6 @@ export function readSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix for screensharing in Electron >= 17
|
|
||||||
ipcMain.handle(IpcEvents.GET_DESKTOP_CAPTURE_SOURCES, (_, opts) => desktopCapturer.getSources(opts));
|
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
|
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
||||||
@ -66,21 +63,21 @@ const settingsWriteQueue = new Queue();
|
|||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
||||||
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
|
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
|
||||||
cssWriteQueue.add(() => writeFile(QUICKCSS_PATH, css))
|
cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css))
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
||||||
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
|
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
|
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
|
||||||
settingsWriteQueue.add(() => writeFile(SETTINGS_FILE, s));
|
settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export function initIpc(mainWindow: BrowserWindow) {
|
export function initIpc(mainWindow: BrowserWindow) {
|
||||||
open(QUICKCSS_PATH, "a+").then(fd => {
|
open(QUICKCSS_PATH, "a+").then(fd => {
|
||||||
fd.close();
|
fd.close();
|
||||||
watch(QUICKCSS_PATH, debounce(async () => {
|
watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
|
||||||
mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
|
mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
|
||||||
}, 50));
|
}, 50));
|
||||||
});
|
});
|
||||||
@ -89,8 +86,13 @@ export function initIpc(mainWindow: BrowserWindow) {
|
|||||||
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
title: "QuickCss Editor",
|
title: "QuickCss Editor",
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
darkTheme: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, "preload.js"),
|
preload: join(__dirname, "preload.js"),
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
sandbox: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
|
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user