Compare commits
259 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ee943c4284 | ||
|
337b3709d6 | ||
|
eb318c678f | ||
|
081df6beb7 | ||
|
ab911b48b5 | ||
|
8cb3491086 | ||
|
ee794d140f | ||
|
a00542b61b | ||
|
041a13c9d3 | ||
|
24aa90bd9c | ||
|
c574f53417 | ||
|
92b84a9e94 | ||
|
bbf3c74cb2 | ||
|
93cb51a975 | ||
|
0b4ae729a3 | ||
|
b90392576e | ||
|
e143260891 | ||
|
644c5c4faa | ||
|
8d8cedd72c | ||
|
082ac62eda | ||
|
7923a790e6 | ||
|
1368c25824 | ||
|
d0b3678ad6 | ||
|
cae8b1a93b | ||
|
a1c1fec8cb | ||
|
55a66dbb39 | ||
|
a2f0c912f0 | ||
|
e29bbf73aa | ||
|
0ba3e9f469 | ||
|
6f200e9218 | ||
|
586b26d2d4 | ||
|
d482d33d6f | ||
|
37c2a8a5de | ||
|
265547213c | ||
|
87e46f5a5a | ||
|
e36f4e5b0a | ||
|
4aff11421f | ||
|
ea642d9e90 | ||
|
17c3496542 | ||
|
0fb79b763d | ||
|
5873bde6a6 | ||
|
0b79387800 | ||
|
6b493bc7d9 | ||
|
de53bc7991 | ||
|
4c5a56a8a5 | ||
|
ed873ef9de | ||
|
d8a553feb0 | ||
|
4717612090 | ||
|
5d1283bd85 | ||
|
3b945b87b8 | ||
|
19c762f9c1 | ||
|
990adf7527 | ||
|
983414d024 | ||
|
d5c05d857f | ||
|
bff6788546 | ||
|
253183a16a | ||
|
0fb3901a18 | ||
|
1b199ec5d8 | ||
|
40395d562a | ||
|
7322c3af04 | ||
|
36c27f1111 | ||
|
95db6c32a3 | ||
|
bed5e98bb0 | ||
|
a5392e5c53 | ||
|
abbd298b31 | ||
|
e219aaa062 | ||
|
cab72e1be6 | ||
|
92372bde1d | ||
|
6747276a87 | ||
|
03915b7533 | ||
|
5e2ec368ad | ||
|
ab8c93fbac | ||
|
d6a3edefd9 | ||
|
727297ec4e | ||
|
eccc4b0be1 | ||
|
8465140bc4 | ||
|
e6ccb751a0 | ||
|
dfc7a15083 | ||
|
37003edae9 | ||
|
faa90eccd3 | ||
|
c91b0df607 | ||
|
f56d99e133 | ||
|
c690662802 | ||
|
4918d699d5 | ||
|
5ec517875e | ||
|
cf56ad985b | ||
|
c09d1558f7 | ||
|
eb190b660e | ||
|
d6f9068695 | ||
|
cb507babaa | ||
|
235d114193 | ||
|
9aba70dcb1 | ||
|
0b61d29c31 | ||
|
335a13a38a | ||
|
128ee41252 | ||
|
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 |
@ -37,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",
|
||||||
@ -82,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",
|
||||||
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
13
.github/FUNDING.yml
vendored
13
.github/FUNDING.yml
vendored
@ -1,13 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: Vendicated
|
|
||||||
patreon: Aliucord
|
|
||||||
open_collective: # Replace with a single Open Collective username
|
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
otechie: # Replace with a single Otechie username
|
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
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
|
38
.github/workflows/build.yml
vendored
38
.github/workflows/build.yml
vendored
@ -34,31 +34,45 @@ jobs:
|
|||||||
- name: Build web
|
- name: Build web
|
||||||
run: pnpm buildWeb --standalone
|
run: pnpm buildWeb --standalone
|
||||||
|
|
||||||
- name: Sign firefox extension
|
|
||||||
run: |
|
|
||||||
pnpx web-ext sign --api-key $WEBEXT_USER --api-secret $WEBEXT_SECRET --channel=unlisted
|
|
||||||
env:
|
|
||||||
WEBEXT_USER: ${{ secrets.WEBEXT_USER }}
|
|
||||||
WEBEXT_SECRET: ${{ secrets.WEBEXT_SECRET }}
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build --standalone
|
run: pnpm build --standalone
|
||||||
|
|
||||||
- name: Rename extensions for more user friendliness
|
- name: Generate plugin list
|
||||||
|
run: pnpm generatePluginJson dist/plugins.json
|
||||||
|
|
||||||
|
- name: Clean up obsolete files
|
||||||
run: |
|
run: |
|
||||||
mv dist/*.xpi dist/Vencord-for-Firefox.xpi
|
rm -rf dist/extension* Vencord.user.css
|
||||||
mv dist/extension-v3.zip dist/Vencord-for-Chrome-and-Edge.zip
|
|
||||||
rm -rf dist/extension-v2-unpacked
|
|
||||||
|
|
||||||
- name: Get some values needed for the release
|
- name: Get some values needed for the release
|
||||||
id: release_values
|
id: release_values
|
||||||
run: |
|
run: |
|
||||||
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Upload Devbuild
|
- name: Upload DevBuild as release
|
||||||
run: |
|
run: |
|
||||||
gh release upload devbuild --clobber dist/*
|
gh release upload devbuild --clobber dist/*
|
||||||
gh release edit devbuild --title "DevBuild $RELEASE_TAG"
|
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 }}
|
RELEASE_TAG: ${{ env.release_tag }}
|
||||||
|
|
||||||
|
- name: Upload DevBuild to builds repo
|
||||||
|
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:
|
||||||
|
API_TOKEN: ${{ secrets.BUILDS_TOKEN }}
|
||||||
|
GH_REPO: Vencord/builds
|
||||||
|
USERNAME: GitHub-Actions
|
||||||
|
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 }}
|
||||||
|
|
14
.github/workflows/reportBrokenPlugins.yml
vendored
14
.github/workflows/reportBrokenPlugins.yml
vendored
@ -36,6 +36,20 @@ jobs:
|
|||||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||||
export CHROMIUM_BIN=$(which chromium-browser)
|
export CHROMIUM_BIN=$(which chromium-browser)
|
||||||
|
|
||||||
|
esbuild scripts/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
|
esbuild test/generateReport.ts > dist/report.mjs
|
||||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||||
env:
|
env:
|
||||||
|
1
.gitignore
vendored
1
.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
|
||||||
|
6
.stylelintrc.json
Normal file
6
.stylelintrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "stylelint-config-standard",
|
||||||
|
"rules": {
|
||||||
|
"indentation": 4
|
||||||
|
}
|
||||||
|
}
|
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"EditorConfig.EditorConfig",
|
|
||||||
"pmneo.tsimporter",
|
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
|
"eamodio.gitlens",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"ExodiusStudios.comment-anchors",
|
||||||
"formulahendry.auto-rename-tag",
|
"formulahendry.auto-rename-tag",
|
||||||
"GregorBiswanger.json2ts",
|
"GregorBiswanger.json2ts",
|
||||||
"eamodio.gitlens",
|
"stylelint.vscode-stylelint"
|
||||||
"kamikillerto.vscode-colorize"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
20
CODE_OF_CONDUCT.md
Normal file
20
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
Our community is welcoming to everyone, regardless of their characteristics.
|
||||||
|
|
||||||
|
As such, we expect you to treat everyone with respect and contribute to an open and welcoming community.
|
||||||
|
|
||||||
|
DO
|
||||||
|
- have empathy and be nice to others
|
||||||
|
- be respectful of differing opinions, even if you disagree
|
||||||
|
- give and accept constructive criticism
|
||||||
|
|
||||||
|
DON'T
|
||||||
|
- use offensive or derogatory language
|
||||||
|
- troll or spam
|
||||||
|
- personally attack or harass others
|
||||||
|
|
||||||
|
Repetitive violations of these guidelines might get your access to the repository restricted.
|
||||||
|
|
||||||
|
|
||||||
|
If you feel like a user is violating these guidelines or feel treated unfairly, please refrain from publicly challenging them and instead contact a Moderator on our Discord server or send an email to vendicated+conduct@riseup.net!
|
48
README.md
48
README.md
@ -1,47 +1,32 @@
|
|||||||
# Vencord
|
# Vencord
|
||||||
|
|
||||||
A Discord client mod that does things differently
|
The cutest Discord client mod
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Super easy to install, no git or node or anything else required
|
- Super easy to install (Download Installer, open, click install button, done)
|
||||||
- Many plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
- 100+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
||||||
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, custom slash commands, ShowHiddenChannels
|
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
|
||||||
- Browser Support: Run Vencord in your Browser via extension or UserScript
|
- Fairly lightweight despite the many inbuilt plugins
|
||||||
|
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
||||||
|
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)
|
||||||
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
||||||
- Works in all Electron versions (Confirmed working on versions 13-23)
|
- Privacy friendly, blocks Discord analytics & crash reporting out of the box and has no telemetry
|
||||||
|
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
||||||
|
|
||||||
## Installing / Uninstalling
|
## Installing / Uninstalling
|
||||||
|
|
||||||
If you're just a normal user, use [our simple gui installer!](https://github.com/Vendicated/VencordInstaller#usage)
|
[![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)
|
||||||
|
|
||||||
If you're a power user who wants to contribute and make plugins or just want to build from source and install manually, read [Megu's Installation Guide!](docs/1_INSTALLING.md)
|
|
||||||
|
|
||||||
## Installing on Browser
|
## Installing on Browser
|
||||||
|
|
||||||
Install the browser extension for [![Chrome](https://img.shields.io/badge/chrome-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip), [![Firefox](https://img.shields.io/badge/firefox-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Firefox.xpi) or [UserScript](https://github.com/Vendicated/Vencord/releases/download/devbuild/Vencord.user.js). Please note that they aren't automatically updated for now, so you will regularely have to reinstall it.
|
[![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)
|
||||||
|
|
||||||
|
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that the CSS Editor, Themes loaded from remote sources and co. will not work in the UserScript. Use the extension if you need any of those
|
||||||
|
|
||||||
You may also build them from source, to do that do the same steps as in the manual regular install method,
|
## Building from Source
|
||||||
except run `pnpm buildWeb` instead of `pnpm build`, and your outputs will be in the dist folder
|
|
||||||
|
|
||||||
```sh
|
See the docs folder
|
||||||
pnpm buildWeb
|
|
||||||
```
|
|
||||||
|
|
||||||
You will find the built extension at dist/extension.zip. Now just install this extension in your Browser
|
|
||||||
|
|
||||||
## Installing Plugins
|
|
||||||
|
|
||||||
> **Note**
|
|
||||||
> You can only use 3rd party plugins in the manual Vencord install for now.
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@ -56,3 +41,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and [Megu's Plugin Guide!](docs/2_PLUGINS
|
|||||||
[join]: https://discord.gg/D9uwnFnqmd
|
[join]: https://discord.gg/D9uwnFnqmd
|
||||||
|
|
||||||
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
|
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
Discord is trademark of Discord Inc. and solely mentioned for the sake of descriptivity.
|
||||||
|
Mention of it does not imply any affiliation with or endorsement by Discord Inc.
|
||||||
|
108
browser/GMPolyfill.js
Normal file
108
browser/GMPolyfill.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* 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));
|
||||||
|
resp.headers = new Headers(parseHeaders(resp.responseHeaders));
|
||||||
|
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;
|
@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Linnea Gräf
|
|
||||||
*
|
|
||||||
* 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 setContentTypeOnStylesheets(details) {
|
|
||||||
if (details.type === "stylesheet") {
|
|
||||||
details.responseHeaders = details.responseHeaders.filter(it => it.name.toLowerCase() !== 'content-type');
|
|
||||||
details.responseHeaders.push({ name: "Content-Type", value: "text/css" });
|
|
||||||
}
|
|
||||||
return { responseHeaders: details.responseHeaders };
|
|
||||||
}
|
|
||||||
|
|
||||||
var cspHeaders = [
|
|
||||||
"content-security-policy",
|
|
||||||
"content-security-policy-report-only",
|
|
||||||
];
|
|
||||||
|
|
||||||
function removeCSPHeaders(details) {
|
|
||||||
return {
|
|
||||||
responseHeaders: details.responseHeaders.filter(header =>
|
|
||||||
!cspHeaders.includes(header.name.toLowerCase()))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
browser.webRequest.onHeadersReceived.addListener(
|
|
||||||
setContentTypeOnStylesheets, { urls: ["https://raw.githubusercontent.com/*"] }, ["blocking", "responseHeaders"]
|
|
||||||
);
|
|
||||||
|
|
||||||
browser.webRequest.onHeadersReceived.addListener(
|
|
||||||
removeCSPHeaders, { urls: ["https://raw.githubusercontent.com/*", "*://*.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,10 +1,14 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"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",
|
||||||
|
"icons": {
|
||||||
|
"128": "icon.png"
|
||||||
|
},
|
||||||
|
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"*://*.discord.com/*",
|
"*://*.discord.com/*",
|
||||||
@ -23,7 +27,7 @@
|
|||||||
|
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": ["dist/Vencord.js"],
|
"resources": ["dist/Vencord.js", "dist/Vencord.css"],
|
||||||
"matches": ["*://*.discord.com/*"]
|
"matches": ["*://*.discord.com/*"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -36,5 +40,12 @@
|
|||||||
"path": "modifyResponseHeaders.json"
|
"path": "modifyResponseHeaders.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "vencord-firefox@vendicated.dev",
|
||||||
|
"strict_min_version": "109.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 2,
|
|
||||||
"name": "Vencord Web",
|
|
||||||
"description": "The Vencord Client Mod for Discord Web.",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Vendicated",
|
|
||||||
"homepage_url": "https://github.com/Vendicated/Vencord",
|
|
||||||
"permissions": [
|
|
||||||
"webRequest",
|
|
||||||
"webRequestBlocking",
|
|
||||||
"*://*.discord.com/*",
|
|
||||||
"https://raw.githubusercontent.com/*"
|
|
||||||
],
|
|
||||||
"content_scripts": [
|
|
||||||
{
|
|
||||||
"run_at": "document_start",
|
|
||||||
"matches": ["*://*.discord.com/*"],
|
|
||||||
"js": ["content.js"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"web_accessible_resources": ["dist/Vencord.js"],
|
|
||||||
"background": {
|
|
||||||
"scripts": ["background.js"]
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,5 +1,5 @@
|
|||||||
> **Warning**
|
> **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.
|
> 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
|
||||||
|
|
||||||
@ -31,12 +31,14 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
|
|||||||
|
|
||||||
Install `pnpm`:
|
Install `pnpm`:
|
||||||
|
|
||||||
> :exclamation: this may need to be run as admin depending on your system, and you may need to close and reopen your terminal.
|
> :exclamation: This next command may need to be run as admin/sudo depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm i -g pnpm
|
npm i -g pnpm
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> :exclamation: **IMPORTANT** Make sure you aren't using an admin/root terminal from here onwards. It **will** mess up your Discord/Vencord instance and you **will** most likely have to reinstall.
|
||||||
|
|
||||||
Clone Vencord:
|
Clone Vencord:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@ -183,7 +185,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`:
|
||||||
|
@ -26,6 +26,10 @@ export default definePlugin({
|
|||||||
name: "Your Name",
|
name: "Your Name",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
// Delete `patches` if you are not using code patches, as it will make
|
||||||
|
// your plugin require restarts, and your stop() method will not be
|
||||||
|
// invoked at all. The presence of the key in the object alone is
|
||||||
|
// enough to trigger this behavior, even if the value is an empty array.
|
||||||
patches: [],
|
patches: [],
|
||||||
// Delete these two below if you are only using code patches
|
// Delete these two below if you are only using code patches
|
||||||
start() {},
|
start() {},
|
||||||
|
59
package.json
59
package.json
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.0.1",
|
"version": "1.1.3",
|
||||||
"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": {
|
||||||
"url": "https://github.com/Vendicated/Vencord/issues"
|
"url": "https://github.com/Vendicated/Vencord/issues"
|
||||||
@ -20,32 +20,34 @@
|
|||||||
"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",
|
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
||||||
|
"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",
|
"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": {
|
||||||
|
"@vap/core": "0.0.12",
|
||||||
|
"@vap/shiki": "0.10.3",
|
||||||
"fflate": "^0.7.4"
|
"fflate": "^0.7.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/diff": "^5.0.2",
|
"@types/diff": "^5.0.2",
|
||||||
"@types/node": "^18.11.9",
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/react": "^18.0.25",
|
"@types/node": "^18.11.18",
|
||||||
"@types/react-dom": "^18.0.9",
|
"@types/react": "^18.0.27",
|
||||||
|
"@types/react-dom": "^18.0.10",
|
||||||
"@types/yazl": "^2.4.2",
|
"@types/yazl": "^2.4.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||||
"@typescript-eslint/parser": "^5.44.0",
|
"@typescript-eslint/parser": "^5.49.0",
|
||||||
"@vap/core": "0.0.12",
|
|
||||||
"@vap/shiki": "0.10.3",
|
|
||||||
"console-menu": "^0.1.0",
|
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
"discord-types": "^1.3.26",
|
"discord-types": "^1.3.26",
|
||||||
"esbuild": "^0.15.16",
|
"esbuild": "^0.15.18",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"eslint-plugin-header": "^3.1.1",
|
"eslint-plugin-header": "^3.1.1",
|
||||||
@ -54,15 +56,31 @@
|
|||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"highlight.js": "10.6.0",
|
"highlight.js": "10.6.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"puppeteer-core": "^19.3.0",
|
"puppeteer-core": "^19.6.0",
|
||||||
"standalone-electron-types": "^1.0.0",
|
"standalone-electron-types": "^1.0.0",
|
||||||
"type-fest": "^3.3.0",
|
"stylelint": "^14.16.1",
|
||||||
"typescript": "^4.9.3"
|
"stylelint-config-standard": "^29.0.0",
|
||||||
|
"tsx": "^3.12.6",
|
||||||
|
"type-fest": "^3.5.3",
|
||||||
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@7.13.4",
|
"packageManager": "pnpm@7.13.4",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch"
|
"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": {
|
"webExt": {
|
||||||
@ -71,5 +89,8 @@
|
|||||||
"overwriteDest": true
|
"overwriteDest": true
|
||||||
},
|
},
|
||||||
"sourceDir": "./dist/extension-v2-unpacked"
|
"sourceDir": "./dist/extension-v2-unpacked"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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));
|
1439
pnpm-lock.yaml
generated
1439
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
99
scripts/build/buildWeb.mjs
Executable file → Normal file
99
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 { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { readFile } from "fs/promises";
|
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
||||||
import { join, resolve } 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, watch } from "./common.mjs";
|
import { commonOpts, globPlugins, watch } from "./common.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.BuildOptions}
|
* @type {esbuild.BuildOptions}
|
||||||
@ -39,9 +39,7 @@ 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: {
|
||||||
@ -60,51 +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}.${new Date().getTime()}`)
|
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});"
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {(target: string, files: string[], shouldZip: boolean) => Promise<void>}
|
||||||
|
*/
|
||||||
async function buildPluginZip(target, files, shouldZip) {
|
async function buildPluginZip(target, files, shouldZip) {
|
||||||
const entries = {
|
const entries = {
|
||||||
"dist/Vencord.js": readFileSync("dist/browser.js"),
|
"dist/Vencord.js": await readFile("dist/browser.js"),
|
||||||
...Object.fromEntries(await Promise.all(files.map(async f => [
|
"dist/Vencord.css": await readFile("dist/browser.css"),
|
||||||
(f.startsWith("manifest") ? "manifest.json" : f),
|
...Object.fromEntries(await Promise.all(files.map(async f => {
|
||||||
await readFile(join("browser", f))
|
let content = await readFile(join("browser", f));
|
||||||
]))),
|
if (f.startsWith("manifest")) {
|
||||||
|
const json = JSON.parse(content.toString("utf-8"));
|
||||||
|
json.version = PackageJSON.version;
|
||||||
|
content = new TextEncoder().encode(JSON.stringify(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
f.startsWith("manifest") ? "manifest.json" : f,
|
||||||
|
content
|
||||||
|
];
|
||||||
|
}))),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (shouldZip) {
|
if (shouldZip) {
|
||||||
zip(entries, {}, (err, data) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (err) {
|
zip(entries, {}, (err, data) => {
|
||||||
console.error(err);
|
if (err) {
|
||||||
process.exitCode = 1;
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
writeFileSync("dist/" + target, data);
|
const out = join("dist", target);
|
||||||
console.info("Extension written to dist/" + target);
|
writeFile(out, data).then(() => {
|
||||||
}
|
console.info("Extension written to " + out);
|
||||||
|
resolve();
|
||||||
|
}).catch(reject);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (existsSync(target))
|
await rm(target, { recursive: true, force: true });
|
||||||
rmSync(target, { recursive: true });
|
await Promise.all(Object.entries(entries).map(async ([file, content]) => {
|
||||||
for (const entry in entries) {
|
const dest = join("dist", target, file);
|
||||||
const destination = "dist/" + target + "/" + entry;
|
const parentDirectory = join(dest, "..");
|
||||||
const parentDirectory = resolve(destination, "..");
|
await mkdir(parentDirectory, { recursive: true });
|
||||||
mkdirSync(parentDirectory, { recursive: true });
|
await writeFile(dest, content);
|
||||||
writeFileSync(destination, entries[entry]);
|
}));
|
||||||
}
|
|
||||||
console.info("Unpacked Extension written to dist/" + target);
|
console.info("Unpacked Extension written to dist/" + target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await buildPluginZip("extension-v3.zip", ["modifyResponseHeaders.json", "content.js", "manifestv3.json"], true);
|
const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => {
|
||||||
await buildPluginZip("extension-v2.zip", ["background.js", "content.js", "manifestv2.json"], true);
|
const cssRuntime = `
|
||||||
await buildPluginZip("extension-v2-unpacked", ["background.js", "content.js", "manifestv2.json"], false);
|
;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,9 +17,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { exec, execSync } from "child_process";
|
import { exec, execSync } from "child_process";
|
||||||
import { existsSync } from "fs";
|
import { existsSync, readFileSync } 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");
|
||||||
@ -33,9 +33,11 @@ export const banner = {
|
|||||||
`.trim()
|
`.trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
|
||||||
|
|
||||||
// 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",
|
||||||
@ -46,7 +48,7 @@ export const makeAllPackagesExternalPlugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
export const globPlugins = {
|
export const globPlugins = {
|
||||||
name: "glob-plugins",
|
name: "glob-plugins",
|
||||||
@ -68,11 +70,18 @@ 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 === "index.ts") {
|
if (file.startsWith(".")) continue;
|
||||||
continue;
|
if (file === "index.ts") continue;
|
||||||
|
const fileBits = file.split(".");
|
||||||
|
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1))) {
|
||||||
|
const mod = fileBits.at(-2);
|
||||||
|
if (mod === "dev" && !watch) continue;
|
||||||
|
if (mod === "web" && !isWeb) continue;
|
||||||
|
if (mod === "desktop" && isWeb) 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++;
|
||||||
}
|
}
|
||||||
@ -87,7 +96,7 @@ export const globPlugins = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
export const gitHashPlugin = {
|
export const gitHashPlugin = {
|
||||||
name: "git-hash-plugin",
|
name: "git-hash-plugin",
|
||||||
@ -103,7 +112,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",
|
||||||
@ -125,7 +134,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",
|
||||||
@ -147,6 +156,31 @@ export const fileIncludePlugin = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styleModule = readFileSync("./scripts/build/module/style.js", "utf-8");
|
||||||
|
/**
|
||||||
|
* @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}
|
* @type {import("esbuild").BuildOptions}
|
||||||
*/
|
*/
|
||||||
@ -158,7 +192,7 @@ export const commonOpts = {
|
|||||||
sourcemap: watch ? "inline" : "",
|
sourcemap: watch ? "inline" : "",
|
||||||
legalComments: "linked",
|
legalComments: "linked",
|
||||||
banner,
|
banner,
|
||||||
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin],
|
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
||||||
external: ["~plugins", "~git-hash", "~git-remote"],
|
external: ["~plugins", "~git-hash", "~git-remote"],
|
||||||
inject: ["./scripts/build/inject/react.mjs"],
|
inject: ["./scripts/build/inject/react.mjs"],
|
||||||
jsxFactory: "VencordCreateElement",
|
jsxFactory: "VencordCreateElement",
|
||||||
|
@ -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 const VencordFragment = Symbol.for("react.fragment");
|
export const VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment");
|
||||||
export let VencordCreateElement =
|
export let VencordCreateElement =
|
||||||
(...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);
|
(...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);
|
||||||
|
@ -16,12 +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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface Review {
|
(window.VencordStyles ??= new Map()).set(STYLE_NAME, {
|
||||||
comment: string,
|
name: STYLE_NAME,
|
||||||
id: number,
|
source: STYLE_SOURCE,
|
||||||
senderdiscordid: string,
|
classNames: {},
|
||||||
senderuserid: number,
|
dom: null,
|
||||||
star: number,
|
});
|
||||||
username: string,
|
|
||||||
profile_photo: string;
|
export default STYLE_NAME;
|
||||||
}
|
|
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/`;
|
191
scripts/generatePluginList.ts
Normal file
191
scripts/generatePluginList.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Dirent, readdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
|
import { access, readFile } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
|
||||||
|
|
||||||
|
interface Dev {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginData {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
authors: Dev[];
|
||||||
|
dependencies: string[];
|
||||||
|
hasPatches: boolean;
|
||||||
|
hasCommands: boolean;
|
||||||
|
required: boolean;
|
||||||
|
enabledByDefault: boolean;
|
||||||
|
target: "desktop" | "web" | "dev";
|
||||||
|
}
|
||||||
|
|
||||||
|
const devs = {} as Record<string, Dev>;
|
||||||
|
|
||||||
|
function getName(node: NamedDeclaration) {
|
||||||
|
return node.name && isIdentifier(node.name) ? node.name.text : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasName(node: NamedDeclaration, name: string) {
|
||||||
|
return getName(node) === name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getObjectProp(node: ObjectLiteralExpression, name: string) {
|
||||||
|
const prop = node.properties.find(p => hasName(p, name));
|
||||||
|
if (prop && isPropertyAssignment(prop)) return prop.initializer;
|
||||||
|
return prop;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDevs() {
|
||||||
|
const file = createSourceFile("constants.ts", readFileSync("src/utils/constants.ts", "utf8"), ScriptTarget.Latest);
|
||||||
|
|
||||||
|
for (const child of file.getChildAt(0).getChildren()) {
|
||||||
|
if (!isVariableStatement(child)) continue;
|
||||||
|
|
||||||
|
const devsDeclaration = child.declarationList.declarations.find(d => hasName(d, "Devs"));
|
||||||
|
if (!devsDeclaration?.initializer || !isCallExpression(devsDeclaration.initializer)) continue;
|
||||||
|
|
||||||
|
const value = devsDeclaration.initializer.arguments[0];
|
||||||
|
|
||||||
|
if (!isObjectLiteralExpression(value)) return;
|
||||||
|
|
||||||
|
for (const prop of value.properties) {
|
||||||
|
const name = (prop.name as Identifier).text;
|
||||||
|
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
|
||||||
|
|
||||||
|
if (!isObjectLiteralExpression(value)) throw new Error(`Failed to parse devs: ${name} is not an object literal`);
|
||||||
|
|
||||||
|
devs[name] = {
|
||||||
|
name: (getObjectProp(value, "name") as StringLiteral).text,
|
||||||
|
id: (getObjectProp(value, "id") as BigIntLiteral).text.slice(0, -1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Could not find Devs constant");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseFile(fileName: string) {
|
||||||
|
const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest);
|
||||||
|
|
||||||
|
const fail = (reason: string) => {
|
||||||
|
return new Error(`Invalid plugin ${fileName}, because ${reason}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const node of file.getChildAt(0).getChildren()) {
|
||||||
|
if (!isExportAssignment(node) || !isCallExpression(node.expression)) continue;
|
||||||
|
|
||||||
|
const call = node.expression;
|
||||||
|
if (!isIdentifier(call.expression) || call.expression.text !== "definePlugin") continue;
|
||||||
|
|
||||||
|
const pluginObj = node.expression.arguments[0];
|
||||||
|
if (!isObjectLiteralExpression(pluginObj)) throw fail("no object literal passed to definePlugin");
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
hasPatches: false,
|
||||||
|
hasCommands: false,
|
||||||
|
enabledByDefault: false,
|
||||||
|
required: false,
|
||||||
|
} as PluginData;
|
||||||
|
|
||||||
|
for (const prop of pluginObj.properties) {
|
||||||
|
const key = getName(prop);
|
||||||
|
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "name":
|
||||||
|
case "description":
|
||||||
|
if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);
|
||||||
|
data[key] = value.text;
|
||||||
|
break;
|
||||||
|
case "patches":
|
||||||
|
data.hasPatches = true;
|
||||||
|
break;
|
||||||
|
case "commands":
|
||||||
|
data.hasCommands = true;
|
||||||
|
break;
|
||||||
|
case "authors":
|
||||||
|
if (!isArrayLiteralExpression(value)) throw fail("authors is not an array literal");
|
||||||
|
data.authors = value.elements.map(e => {
|
||||||
|
if (!isPropertyAccessExpression(e)) throw fail("authors array contains non-property access expressions");
|
||||||
|
return devs[getName(e)!];
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "dependencies":
|
||||||
|
if (!isArrayLiteralExpression(value)) throw fail("dependencies is not an array literal");
|
||||||
|
const { elements } = value;
|
||||||
|
if (elements.some(e => !isStringLiteral(e))) throw fail("dependencies array contains non-string elements");
|
||||||
|
data.dependencies = (elements as NodeArray<StringLiteral>).map(e => e.text);
|
||||||
|
break;
|
||||||
|
case "required":
|
||||||
|
case "enabledByDefault":
|
||||||
|
data[key] = value.kind === SyntaxKind.TrueKeyword;
|
||||||
|
if (!data[key] && value.kind !== SyntaxKind.FalseKeyword) throw fail(`${key} is not a boolean literal`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.name || !data.description || !data.authors) throw fail("name, description or authors are missing");
|
||||||
|
|
||||||
|
const fileBits = fileName.split(".");
|
||||||
|
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1)!)) {
|
||||||
|
const mod = fileBits.at(-2)!;
|
||||||
|
if (!["web", "desktop", "dev"].includes(mod)) throw fail(`invalid target ${fileBits.at(-2)}`);
|
||||||
|
data.target = mod as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw fail("no default export called 'definePlugin' found");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEntryPoint(dirent: Dirent) {
|
||||||
|
const base = join("./src/plugins", dirent.name);
|
||||||
|
if (!dirent.isDirectory()) return base;
|
||||||
|
|
||||||
|
for (const name of ["index.ts", "index.tsx"]) {
|
||||||
|
const full = join(base, name);
|
||||||
|
try {
|
||||||
|
await access(full);
|
||||||
|
return full;
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`${dirent.name}: Couldn't find entry point`);
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
parseDevs();
|
||||||
|
const plugins = readdirSync("./src/plugins", { withFileTypes: true }).filter(d => d.name !== "index.ts");
|
||||||
|
|
||||||
|
const promises = plugins.map(async dirent => parseFile(await getEntryPoint(dirent)));
|
||||||
|
|
||||||
|
const data = JSON.stringify(await Promise.all(promises));
|
||||||
|
|
||||||
|
if (process.argv.length > 2) {
|
||||||
|
writeFileSync(process.argv[2], data);
|
||||||
|
} else {
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
})();
|
@ -31,13 +31,15 @@ for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CANARY = process.env.USE_CANARY === "true";
|
||||||
|
|
||||||
const browser = await pup.launch({
|
const browser = await pup.launch({
|
||||||
headless: true,
|
headless: true,
|
||||||
executablePath: process.env.CHROMIUM_BIN
|
executablePath: process.env.CHROMIUM_BIN
|
||||||
});
|
});
|
||||||
|
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.setUserAgent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36");
|
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
|
||||||
|
|
||||||
function maybeGetError(handle: JSHandle) {
|
function maybeGetError(handle: JSHandle) {
|
||||||
return (handle as JSHandle<Error>)?.getProperty("message")
|
return (handle as JSHandle<Error>)?.getProperty("message")
|
||||||
@ -65,7 +67,7 @@ function toCodeBlock(s: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function printReport() {
|
async function printReport() {
|
||||||
console.log("# Vencord Report");
|
console.log("# Vencord Report" + (CANARY ? " (Canary)" : ""));
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
console.log("## Bad Patches");
|
console.log("## Bad Patches");
|
||||||
@ -98,7 +100,7 @@ async function printReport() {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
description: "Here's the latest Vencord Report!",
|
description: "Here's the latest Vencord Report!",
|
||||||
username: "Vencord Reporter",
|
username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""),
|
||||||
avatar_url: "https://cdn.discordapp.com/icons/1015060230222131221/f0204a918c6c9c9a43195997e97d8adf.webp",
|
avatar_url: "https://cdn.discordapp.com/icons/1015060230222131221/f0204a918c6c9c9a43195997e97d8adf.webp",
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
@ -184,8 +186,11 @@ page.on("console", async e => {
|
|||||||
} else if (isDebug) {
|
} else if (isDebug) {
|
||||||
console.error(e.text());
|
console.error(e.text());
|
||||||
} else if (level === "error") {
|
} else if (level === "error") {
|
||||||
console.error("Got unexpected error", e.text());
|
const text = e.text();
|
||||||
report.otherErrors.push(e.text());
|
if (!text.startsWith("Failed to load resource: the server responded with a status of")) {
|
||||||
|
console.error("Got unexpected error", text);
|
||||||
|
report.otherErrors.push(text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -207,6 +212,7 @@ function runTime(token: string) {
|
|||||||
|
|
||||||
|
|
||||||
// Monkey patch Logger to not log with custom css
|
// Monkey patch Logger to not log with custom css
|
||||||
|
// @ts-ignore
|
||||||
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
|
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
|
||||||
if (level === "warn" || level === "error")
|
if (level === "warn" || level === "error")
|
||||||
console[level]("[Vencord]", this.name + ":", ...args);
|
console[level]("[Vencord]", this.name + ":", ...args);
|
||||||
@ -215,6 +221,9 @@ function runTime(token: string) {
|
|||||||
// force enable all plugins and patches
|
// force enable all plugins and patches
|
||||||
Vencord.Plugins.patches.length = 0;
|
Vencord.Plugins.patches.length = 0;
|
||||||
Object.values(Vencord.Plugins.plugins).forEach(p => {
|
Object.values(Vencord.Plugins.plugins).forEach(p => {
|
||||||
|
// Needs native server to run
|
||||||
|
if (p.name === "WebRichPresence (arRPC)") return;
|
||||||
|
|
||||||
p.required = true;
|
p.required = true;
|
||||||
p.patches?.forEach(patch => {
|
p.patches?.forEach(patch => {
|
||||||
patch.plugin = p.name;
|
patch.plugin = p.name;
|
||||||
@ -248,6 +257,8 @@ function runTime(token: string) {
|
|||||||
|
|
||||||
if (!isWasm)
|
if (!isWasm)
|
||||||
await wreq.e(id as any);
|
await wreq.e(id as any);
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
}
|
}
|
||||||
console.error("[PUP_DEBUG]", "Finished loading chunks!");
|
console.error("[PUP_DEBUG]", "Finished loading chunks!");
|
||||||
|
|
||||||
@ -271,4 +282,4 @@ await page.evaluateOnNewDocument(`
|
|||||||
;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await page.goto("https://discord.com/login");
|
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
|
@ -1,342 +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`,
|
|
||||||
`${process.env.HOME}/.dvm`,
|
|
||||||
"/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,153 +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,
|
|
||||||
question
|
|
||||||
} = 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 cwd = process.cwd();
|
|
||||||
const globalCmd = `flatpak override ${selected.branch} --filesystem=${cwd}`;
|
|
||||||
const userCmd = `flatpak override --user ${selected.branch} --filesystem=${cwd}`;
|
|
||||||
const cmd = selected.location.startsWith("/home")
|
|
||||||
? userCmd
|
|
||||||
: globalCmd;
|
|
||||||
execSync(cmd);
|
|
||||||
console.log("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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const answer = await question(
|
|
||||||
`Would you like to allow ${selected.branch} to talk to org.freedesktop.Flatpak?\n` +
|
|
||||||
"This is essentially full host access but necessary to spawn git. Without it, the updater will not work\n" +
|
|
||||||
"Consider using the http based updater (using the gui installer) instead if you want to maintain the sandbox.\n" +
|
|
||||||
"[y/N]: "
|
|
||||||
);
|
|
||||||
|
|
||||||
if (["y", "yes", "yeah"].includes(answer.toLowerCase())) {
|
|
||||||
try {
|
|
||||||
const globalCmd = `flatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
|
|
||||||
const userCmd = `flatpak override --user ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
|
|
||||||
const cmd = selected.location.startsWith("/home")
|
|
||||||
? userCmd
|
|
||||||
: globalCmd;
|
|
||||||
execSync(cmd);
|
|
||||||
console.log("Sucessfully gave talk permission");
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to give talk permission\n", err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`Not giving full host access. If you change your mind later, you can run:\nflatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
||||||
|
}
|
||||||
|
});
|
@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
export * as Api from "./api";
|
export * as Api from "./api";
|
||||||
export * as Plugins from "./plugins";
|
export * as Plugins from "./plugins";
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
||||||
export * as Util from "./utils";
|
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";
|
||||||
@ -28,12 +27,12 @@ export { PlainSettings, Settings };
|
|||||||
import "./utils/quickCss";
|
import "./utils/quickCss";
|
||||||
import "./webpack/patchWebpack";
|
import "./webpack/patchWebpack";
|
||||||
|
|
||||||
import { popNotice, showNotice } from "./api/Notices";
|
import { showNotification } from "./api/Notifications";
|
||||||
import { PlainSettings, Settings } from "./api/settings";
|
import { PlainSettings, Settings } from "./api/settings";
|
||||||
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
||||||
import { checkForUpdates, UpdateLogger } from "./utils/updater";
|
import { checkForUpdates, rebuild, update, 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;
|
||||||
|
|
||||||
@ -45,17 +44,35 @@ 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;
|
||||||
setTimeout(() => {
|
|
||||||
showNotice(
|
if (Settings.autoUpdate) {
|
||||||
"A Vencord update is available!",
|
await update();
|
||||||
"View Update",
|
const needsFullRestart = await rebuild();
|
||||||
() => {
|
if (Settings.autoUpdateNotification)
|
||||||
popNotice();
|
setTimeout(() => showNotification({
|
||||||
Router.open("VencordUpdater");
|
title: "Vencord has been updated!",
|
||||||
|
body: "Click here to restart",
|
||||||
|
permanent: true,
|
||||||
|
onClick() {
|
||||||
|
if (needsFullRestart)
|
||||||
|
window.DiscordNative.app.relaunch();
|
||||||
|
else
|
||||||
|
location.reload();
|
||||||
}
|
}
|
||||||
);
|
}), 10_000);
|
||||||
}, 10000);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.notifyAboutUpdates)
|
||||||
|
setTimeout(() => showNotification({
|
||||||
|
title: "A Vencord update is available!",
|
||||||
|
body: "Click here to view the update",
|
||||||
|
permanent: true,
|
||||||
|
onClick() {
|
||||||
|
SettingsRouter.open("VencordUpdater");
|
||||||
|
}
|
||||||
|
}), 10_000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
UpdateLogger.error("Failed to check for updates", err);
|
UpdateLogger.error("Failed to check for updates", err);
|
||||||
}
|
}
|
||||||
@ -76,3 +93,12 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
|
if (!IS_WEB && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.head.append(Object.assign(document.createElement("style"), {
|
||||||
|
id: "vencord-native-titlebar-style",
|
||||||
|
textContent: "[class*=titleBar-]{display: none!important}"
|
||||||
|
}));
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
@ -16,8 +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 ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
import { HTMLProps } from "react";
|
import { ComponentType, HTMLProps } from "react";
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
@ -27,20 +28,21 @@ export enum BadgePosition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ProfileBadge {
|
export interface ProfileBadge {
|
||||||
/** The tooltip to show on hover */
|
/** The tooltip to show on hover. Required for image badges */
|
||||||
tooltip: string;
|
tooltip?: string;
|
||||||
|
/** Custom component for the badge (tooltip not included) */
|
||||||
|
component?: ComponentType<ProfileBadge & BadgeUserArgs>;
|
||||||
/** The custom image to use */
|
/** The custom image to use */
|
||||||
image?: string;
|
image?: string;
|
||||||
/** Action to perform when you click the badge */
|
/** Action to perform when you click the badge */
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
/** Should the user display this badge? */
|
/** Should the user display this badge? */
|
||||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
||||||
/** Optional props (e.g. style) for the badge */
|
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
||||||
props?: HTMLProps<HTMLImageElement>;
|
props?: HTMLProps<HTMLImageElement>;
|
||||||
/** Insert at start or end? */
|
/** Insert at start or end? */
|
||||||
position?: BadgePosition;
|
position?: BadgePosition;
|
||||||
|
/** The badge name to display, Discord uses this. Required for component badges */
|
||||||
/** The badge name to display. Discord uses this, but we don't. */
|
|
||||||
key?: string;
|
key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +53,7 @@ const Badges = new Set<ProfileBadge>();
|
|||||||
* @param badge The badge to register
|
* @param badge The badge to register
|
||||||
*/
|
*/
|
||||||
export function addBadge(badge: ProfileBadge) {
|
export function addBadge(badge: ProfileBadge) {
|
||||||
|
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
|
||||||
Badges.add(badge);
|
Badges.add(badge);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,8 +73,8 @@ export function inject(badgeArray: ProfileBadge[], args: BadgeUserArgs) {
|
|||||||
for (const badge of Badges) {
|
for (const badge of Badges) {
|
||||||
if (!badge.shouldShow || badge.shouldShow(args)) {
|
if (!badge.shouldShow || badge.shouldShow(args)) {
|
||||||
badge.position === BadgePosition.START
|
badge.position === BadgePosition.START
|
||||||
? badgeArray.unshift(badge)
|
? badgeArray.unshift({ ...badge, ...args })
|
||||||
: badgeArray.push(badge);
|
: badgeArray.push({ ...badge, ...args });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id);
|
(Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id);
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { mergeDefaults } from "@utils/misc";
|
import { mergeDefaults } from "@utils/misc";
|
||||||
import { findByCodeLazy, findByPropsLazy, waitFor } from "@webpack";
|
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";
|
||||||
|
|
||||||
@ -26,9 +27,6 @@ import { Argument } from "./types";
|
|||||||
const createBotMessage = findByCodeLazy('username:"Clyde"');
|
const createBotMessage = findByCodeLazy('username:"Clyde"');
|
||||||
const MessageSender = findByPropsLazy("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())}`;
|
||||||
}
|
}
|
||||||
|
144
src/api/ContextMenu.ts
Normal file
144
src/api/ContextMenu.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* 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 Logger from "@utils/Logger";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param children The rendered context menu elements
|
||||||
|
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
||||||
|
*/
|
||||||
|
export type NavContextMenuPatchCallback = (children: Array<React.ReactElement>, ...args: Array<any>) => void;
|
||||||
|
/**
|
||||||
|
* @param The navId of the context menu being patched
|
||||||
|
* @param children The rendered context menu elements
|
||||||
|
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
||||||
|
*/
|
||||||
|
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<React.ReactElement>, ...args: Array<any>) => void;
|
||||||
|
|
||||||
|
const ContextMenuLogger = new Logger("ContextMenu");
|
||||||
|
|
||||||
|
export const navPatches = new Map<string, Set<NavContextMenuPatchCallback>>();
|
||||||
|
export const globalPatches = new Set<GlobalContextMenuPatchCallback>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a context menu patch
|
||||||
|
* @param navId The navId(s) for the context menu(s) to patch
|
||||||
|
* @param patch The patch to be applied
|
||||||
|
*/
|
||||||
|
export function addContextMenuPatch(navId: string | Array<string>, patch: NavContextMenuPatchCallback) {
|
||||||
|
if (!Array.isArray(navId)) navId = [navId];
|
||||||
|
for (const id of navId) {
|
||||||
|
let contextMenuPatches = navPatches.get(id);
|
||||||
|
if (!contextMenuPatches) {
|
||||||
|
contextMenuPatches = new Set();
|
||||||
|
navPatches.set(id, contextMenuPatches);
|
||||||
|
}
|
||||||
|
|
||||||
|
contextMenuPatches.add(patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a global context menu patch that fires the patch for all context menus
|
||||||
|
* @param patch The patch to be applied
|
||||||
|
*/
|
||||||
|
export function addGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback) {
|
||||||
|
globalPatches.add(patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a context menu patch
|
||||||
|
* @param navId The navId(s) for the context menu(s) to remove the patch
|
||||||
|
* @param patch The patch to be removed
|
||||||
|
* @returns Wheter the patch was sucessfully removed from the context menu(s)
|
||||||
|
*/
|
||||||
|
export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> {
|
||||||
|
const navIds = Array.isArray(navId) ? navId : [navId as string];
|
||||||
|
|
||||||
|
const results = navIds.map(id => navPatches.get(id)?.delete(patch) ?? false);
|
||||||
|
|
||||||
|
return (Array.isArray(navId) ? results : results[0]) as T extends string ? boolean : Array<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a global context menu patch
|
||||||
|
* @returns Wheter the patch was sucessfully removed
|
||||||
|
*/
|
||||||
|
export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean {
|
||||||
|
return globalPatches.delete(patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper function for finding the children array of a group nested inside a context menu based on the id of one of its childs
|
||||||
|
* @param id The id of the child
|
||||||
|
*/
|
||||||
|
export function findGroupChildrenByChildId(id: string, children: Array<React.ReactElement>, itemsArray?: Array<React.ReactElement>): Array<React.ReactElement> | null {
|
||||||
|
for (const child of children) {
|
||||||
|
if (child == null) continue;
|
||||||
|
|
||||||
|
if (child.props?.id === id) return itemsArray ?? null;
|
||||||
|
|
||||||
|
let nextChildren = child.props?.children;
|
||||||
|
if (nextChildren) {
|
||||||
|
if (!Array.isArray(nextChildren)) {
|
||||||
|
nextChildren = [nextChildren];
|
||||||
|
child.props.children = nextChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
const found = findGroupChildrenByChildId(id, nextChildren, nextChildren);
|
||||||
|
if (found !== null) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContextMenuProps {
|
||||||
|
contextMenuApiArguments?: Array<any>;
|
||||||
|
navId: string;
|
||||||
|
children: Array<ReactElement>;
|
||||||
|
"aria-label": string;
|
||||||
|
onSelect: (() => void) | undefined;
|
||||||
|
onClose: (callback: (...args: Array<any>) => any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function _patchContextMenu(props: ContextMenuProps) {
|
||||||
|
props.contextMenuApiArguments ??= [];
|
||||||
|
const contextMenuPatches = navPatches.get(props.navId);
|
||||||
|
|
||||||
|
if (!Array.isArray(props.children)) props.children = [props.children];
|
||||||
|
|
||||||
|
if (contextMenuPatches) {
|
||||||
|
for (const patch of contextMenuPatches) {
|
||||||
|
try {
|
||||||
|
patch(props.children, ...props.contextMenuApiArguments);
|
||||||
|
} catch (err) {
|
||||||
|
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const patch of globalPatches) {
|
||||||
|
try {
|
||||||
|
patch(props.navId, props.children, ...props.contextMenuApiArguments);
|
||||||
|
} catch (err) {
|
||||||
|
ContextMenuLogger.error("Global patch errored,", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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);
|
||||||
|
});
|
||||||
|
}
|
@ -19,6 +19,7 @@
|
|||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { MessageStore } from "@webpack/common";
|
import { MessageStore } from "@webpack/common";
|
||||||
import type { Channel, Message } from "discord-types/general";
|
import type { Channel, Message } from "discord-types/general";
|
||||||
|
import type { Promisable } from "type-fest";
|
||||||
|
|
||||||
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
||||||
|
|
||||||
@ -41,16 +42,16 @@ export interface MessageExtra {
|
|||||||
stickerIds?: string[];
|
stickerIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => void | { cancel: boolean; };
|
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
|
||||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
|
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<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: MessageExtra) {
|
export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
|
||||||
for (const listener of sendListeners) {
|
for (const listener of sendListeners) {
|
||||||
try {
|
try {
|
||||||
const result = listener(channelId, messageObj, extra);
|
const result = await listener(channelId, messageObj, extra);
|
||||||
if (result && result.cancel === true) {
|
if (result && result.cancel === true) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -61,10 +62,10 @@ export function _handlePreSend(channelId: string, messageObj: MessageObject, ext
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
|
export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
|
||||||
for (const listener of editListeners) {
|
for (const listener of editListeners) {
|
||||||
try {
|
try {
|
||||||
listener(channelId, messageId, messageObj);
|
await listener(channelId, messageId, messageObj);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
|
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
|
||||||
}
|
}
|
||||||
|
119
src/api/Notifications/NotificationComponent.tsx
Normal file
119
src/api/Notifications/NotificationComponent.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* 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 { 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,
|
||||||
|
permanent
|
||||||
|
}: 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 || permanent) 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={() => {
|
||||||
|
onClose!();
|
||||||
|
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">
|
||||||
|
<div className="vc-notification-header">
|
||||||
|
<h2 className="vc-notification-title">{title}</h2>
|
||||||
|
<button
|
||||||
|
className="vc-notification-close-btn"
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onClose!();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
role="img"
|
||||||
|
aria-labelledby="vc-notification-dismiss-title"
|
||||||
|
>
|
||||||
|
<title id="vc-notification-dismiss-title">Dismiss Notification</title>
|
||||||
|
<path fill="currentColor" d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{image && <img className="vc-notification-img" src={image} alt="" />}
|
||||||
|
{timeout !== 0 && !permanent && (
|
||||||
|
<div
|
||||||
|
className="vc-notification-progressbar"
|
||||||
|
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
onError: ({ props }) => props.onClose!()
|
||||||
|
});
|
101
src/api/Notifications/Notifications.tsx
Normal file
101
src/api/Notifications/Notifications.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
/** Whether this notification should not have a timeout */
|
||||||
|
permanent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
74
src/api/Notifications/styles.css
Normal file
74
src/api/Notifications/styles.css
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
.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-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-title {
|
||||||
|
color: var(--header-primary);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-close-btn {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--interactive-normal);
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-close-btn:hover {
|
||||||
|
color: var(--interactive-hover);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-icon {
|
||||||
|
height: 4rem;
|
||||||
|
width: 4rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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%;
|
||||||
|
}
|
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(" ");
|
||||||
|
};
|
@ -18,12 +18,17 @@
|
|||||||
|
|
||||||
import * as $Badges from "./Badges";
|
import * as $Badges from "./Badges";
|
||||||
import * as $Commands from "./Commands";
|
import * as $Commands from "./Commands";
|
||||||
|
import * as $ContextMenu from "./ContextMenu";
|
||||||
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 $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 $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
|
||||||
@ -31,16 +36,16 @@ import * as $ServerList from "./ServerList";
|
|||||||
*
|
*
|
||||||
* 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, ...).
|
||||||
@ -55,22 +60,42 @@ 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;
|
||||||
/**
|
/**
|
||||||
* An API allowing you to add custom buttons in the message popover
|
* An API allowing you to add custom buttons in the message popover
|
||||||
*/
|
*/
|
||||||
const MessagePopover = $MessagePopover;
|
export const MessagePopover = $MessagePopover;
|
||||||
/**
|
/**
|
||||||
* An API allowing you to add badges to user profiles
|
* An API allowing you to add badges to user profiles
|
||||||
*/
|
*/
|
||||||
const Badges = $Badges;
|
export const Badges = $Badges;
|
||||||
/**
|
/**
|
||||||
* An API allowing you to add custom elements to the server list
|
* An API allowing you to add custom elements to the server list
|
||||||
*/
|
*/
|
||||||
const ServerList = $ServerList;
|
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;
|
||||||
|
|
||||||
export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, MessagePopover, Notices, ServerList };
|
/**
|
||||||
|
* An api allowing you to patch and add/remove items to/from context menus
|
||||||
|
*/
|
||||||
|
export const ContextMenu = $ContextMenu;
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
import IpcEvents from "@utils/IpcEvents";
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { mergeDefaults } from "@utils/misc";
|
import { mergeDefaults } from "@utils/misc";
|
||||||
import { OptionType } from "@utils/types";
|
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||||
import { React } from "@webpack/common";
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
import plugins from "~plugins";
|
import plugins from "~plugins";
|
||||||
@ -27,23 +27,47 @@ import plugins from "~plugins";
|
|||||||
const logger = new Logger("Settings");
|
const logger = new Logger("Settings");
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
notifyAboutUpdates: boolean;
|
notifyAboutUpdates: boolean;
|
||||||
|
autoUpdate: boolean;
|
||||||
|
autoUpdateNotification: boolean,
|
||||||
useQuickCss: boolean;
|
useQuickCss: boolean;
|
||||||
enableReactDevtools: boolean;
|
enableReactDevtools: boolean;
|
||||||
themeLinks: string[];
|
themeLinks: string[];
|
||||||
|
frameless: boolean;
|
||||||
|
transparent: boolean;
|
||||||
|
winCtrlQ: boolean;
|
||||||
|
winNativeTitleBar: 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,
|
||||||
|
autoUpdateNotification: true,
|
||||||
useQuickCss: true,
|
useQuickCss: true,
|
||||||
themeLinks: [],
|
themeLinks: [],
|
||||||
enableReactDevtools: false,
|
enableReactDevtools: false,
|
||||||
plugins: {}
|
frameless: false,
|
||||||
|
transparent: false,
|
||||||
|
winCtrlQ: false,
|
||||||
|
winNativeTitleBar: false,
|
||||||
|
plugins: {},
|
||||||
|
|
||||||
|
notifications: {
|
||||||
|
timeout: 5000,
|
||||||
|
position: "bottom-right",
|
||||||
|
useNative: "not-focused"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -70,7 +94,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
|||||||
// Return empty for plugins with no settings
|
// Return empty for plugins with no settings
|
||||||
if (path === "plugins" && p in plugins)
|
if (path === "plugins" && p in plugins)
|
||||||
return target[p] = makeProxy({
|
return target[p] = makeProxy({
|
||||||
enabled: plugins[p].required ?? false
|
enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
|
||||||
}, root, `plugins.${p}`);
|
}, 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
|
||||||
@ -144,11 +168,12 @@ export const Settings = makeProxy(settings);
|
|||||||
* @param paths An optional list of paths to whitelist for rerenders
|
* @param paths An optional list of paths to whitelist for rerenders
|
||||||
* @returns Settings
|
* @returns Settings
|
||||||
*/
|
*/
|
||||||
export function useSettings(paths?: string[]) {
|
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
|
||||||
|
export function useSettings(paths?: UseSettings<Settings>[]) {
|
||||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||||
|
|
||||||
const onUpdate: SubscriptionCallback = paths
|
const onUpdate: SubscriptionCallback = paths
|
||||||
? (value, path) => paths.includes(path) && forceUpdate()
|
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
|
||||||
: forceUpdate;
|
: forceUpdate;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -198,3 +223,31 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}`) as UseSettings<Settings>[]
|
||||||
|
).plugins[definedSettings.pluginName] as any,
|
||||||
|
def,
|
||||||
|
checks: checks ?? {},
|
||||||
|
pluginName: "",
|
||||||
|
};
|
||||||
|
return definedSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
type UseSettings<T extends object> = ResolveUseSettings<T>[keyof T];
|
||||||
|
|
||||||
|
type ResolveUseSettings<T extends object> = {
|
||||||
|
[Key in keyof T]:
|
||||||
|
Key extends string
|
||||||
|
? T[Key] extends Record<string, unknown>
|
||||||
|
// @ts-ignore "Type instantiation is excessively deep and possibly infinite"
|
||||||
|
? UseSettings<T[Key]> extends string ? `${Key}.${UseSettings<T[Key]>}` : never
|
||||||
|
: Key
|
||||||
|
: never;
|
||||||
|
};
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -17,20 +17,24 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { LazyComponent } from "@utils/misc";
|
import { LazyComponent } from "@utils/misc";
|
||||||
import { Margins, React } from "@webpack/common";
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
import { ErrorCard } from "./ErrorCard";
|
import { ErrorCard } from "./ErrorCard";
|
||||||
|
|
||||||
interface Props {
|
interface Props<T = any> {
|
||||||
/** Render nothing if an error occurs */
|
/** Render nothing if an error occurs */
|
||||||
noop?: boolean;
|
noop?: boolean;
|
||||||
/** Fallback component to render if an error occurs */
|
/** 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 */
|
/** called when an error occurs. The props property is only available if using .wrap */
|
||||||
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void;
|
||||||
/** Custom error message */
|
/** Custom error message */
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|
||||||
|
/** The props passed to the wrapped component. Only used by wrap */
|
||||||
|
wrappedProps?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = "#e78284";
|
const color = "#e78284";
|
||||||
@ -65,7 +69,7 @@ const ErrorBoundary = LazyComponent(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
this.props.onError?.(error, errorInfo);
|
this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps });
|
||||||
logger.error("A component threw an Error\n", error);
|
logger.error("A component threw an Error\n", error);
|
||||||
logger.error("Component Stack", errorInfo.componentStack);
|
logger.error("Component Stack", errorInfo.componentStack);
|
||||||
}
|
}
|
||||||
@ -84,15 +88,13 @@ const ErrorBoundary = LazyComponent(() => {
|
|||||||
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorCard style={{
|
<ErrorCard style={{ overflow: "hidden" }}>
|
||||||
overflow: "hidden",
|
|
||||||
}}>
|
|
||||||
<h1>Oh no!</h1>
|
<h1>Oh no!</h1>
|
||||||
<p>{msg}</p>
|
<p>{msg}</p>
|
||||||
<code>
|
<code>
|
||||||
{this.state.message}
|
{this.state.message}
|
||||||
{!!this.state.stack && (
|
{!!this.state.stack && (
|
||||||
<pre className={Margins.marginTop8}>
|
<pre className={Margins.top8}>
|
||||||
{this.state.stack}
|
{this.state.stack}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
@ -103,11 +105,11 @@ const ErrorBoundary = LazyComponent(() => {
|
|||||||
};
|
};
|
||||||
}) as
|
}) as
|
||||||
React.ComponentType<React.PropsWithChildren<Props>> & {
|
React.ComponentType<React.PropsWithChildren<Props>> & {
|
||||||
wrap<T extends JSX.IntrinsicAttributes = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
|
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, "wrappedProps">): React.FunctionComponent<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
||||||
<ErrorBoundary {...errorBoundaryProps}>
|
<ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>
|
||||||
<Component {...props} />
|
<Component {...props} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
7
src/components/ErrorCard.css
Normal file
7
src/components/ErrorCard.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.vc-error-card {
|
||||||
|
padding: 2em;
|
||||||
|
background-color: #e7828430;
|
||||||
|
border: 1px solid #e78284;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: var(--text-normal, white);
|
||||||
|
}
|
@ -16,24 +16,15 @@
|
|||||||
* 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 "./ErrorCard.css";
|
||||||
|
|
||||||
interface Props {
|
import { classes } from "@utils/misc";
|
||||||
style?: React.CSSProperties;
|
import type { HTMLProps } from "react";
|
||||||
className?: string;
|
|
||||||
}
|
export function ErrorCard(props: React.PropsWithChildren<HTMLProps<HTMLDivElement>>) {
|
||||||
export function ErrorCard(props: React.PropsWithChildren<Props>) {
|
|
||||||
return (
|
return (
|
||||||
<Card className={props.className} style={
|
<div {...props} className={classes(props.className, "vc-error-card")}>
|
||||||
{
|
|
||||||
padding: "2em",
|
|
||||||
backgroundColor: "#e7828430",
|
|
||||||
borderColor: "#e78284",
|
|
||||||
color: "var(--text-normal)",
|
|
||||||
...props.style
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,12 @@ const setCss = debounce((css: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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);
|
||||||
@ -41,4 +46,6 @@ export async function launchMonacoEditor() {
|
|||||||
: "vs-dark";
|
: "vs-dark";
|
||||||
|
|
||||||
win.document.write(monacoHtml);
|
win.document.write(monacoHtml);
|
||||||
|
|
||||||
|
window.__VENCORD_MONACO_WIN__ = new WeakRef(win);
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { makeCodeblock } from "@utils/misc";
|
import { makeCodeblock } from "@utils/misc";
|
||||||
|
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||||
|
import { ReplaceFn } from "@utils/types";
|
||||||
import { search } from "@webpack";
|
import { search } from "@webpack";
|
||||||
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
import { Button, Clipboard, Forms, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
import { CheckedTextInput } from "./CheckedTextInput";
|
import { CheckedTextInput } from "./CheckedTextInput";
|
||||||
import ErrorBoundary from "./ErrorBoundary";
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
@ -41,20 +44,29 @@ const findCandidates = debounce(function ({ find, setModule, setError }) {
|
|||||||
setModule([keys[0], candidates[keys[0]]]);
|
setModule([keys[0], candidates[keys[0]]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
function ReplacementComponent({ module, match, replacement, setReplacementError }) {
|
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 [id, fact] = module;
|
||||||
const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
|
const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
|
||||||
|
|
||||||
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
||||||
const src: string = fact.toString().replaceAll("\n", "");
|
const src: string = fact.toString().replaceAll("\n", "");
|
||||||
|
const canonicalMatch = canonicalizeMatch(match);
|
||||||
try {
|
try {
|
||||||
var patched = src.replace(match, replacement);
|
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
||||||
|
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
||||||
setReplacementError(void 0);
|
setReplacementError(void 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setReplacementError((e as Error).message);
|
setReplacementError((e as Error).message);
|
||||||
return ["", [], []];
|
return ["", [], []];
|
||||||
}
|
}
|
||||||
const m = src.match(match);
|
const m = src.match(canonicalMatch);
|
||||||
return [patched, m, makeDiff(src, patched, m)];
|
return [patched, m, makeDiff(src, patched, m)];
|
||||||
}, [id, match, replacement]);
|
}, [id, match, replacement]);
|
||||||
|
|
||||||
@ -118,7 +130,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!!diff?.length && (
|
{!!diff?.length && (
|
||||||
<Button className={Margins.marginTop20} onClick={() => {
|
<Button className={Margins.top20} onClick={() => {
|
||||||
try {
|
try {
|
||||||
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
||||||
setCompileResult([true, "Compiled successfully"]);
|
setCompileResult([true, "Compiled successfully"]);
|
||||||
@ -179,9 +191,10 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||||||
{Object.entries({
|
{Object.entries({
|
||||||
"$$": "Insert a $",
|
"$$": "Insert a $",
|
||||||
"$&": "Insert the entire match",
|
"$&": "Insert the entire match",
|
||||||
"$`": "Insert the substring before the match",
|
"$`\u200b": "Insert the substring before the match",
|
||||||
"$'": "Insert the substring after the match",
|
"$'": "Insert the substring after the match",
|
||||||
"$n": "Insert the nth capturing group ($1, $2...)"
|
"$n": "Insert the nth capturing group ($1, $2...)",
|
||||||
|
"$self": "Insert the plugin instance",
|
||||||
}).map(([placeholder, desc]) => (
|
}).map(([placeholder, desc]) => (
|
||||||
<Forms.FormText key={placeholder}>
|
<Forms.FormText key={placeholder}>
|
||||||
{Parser.parse("`" + placeholder + "`")}: {desc}
|
{Parser.parse("`" + placeholder + "`")}: {desc}
|
||||||
@ -191,7 +204,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
className={Margins.marginTop8}
|
className={Margins.top8}
|
||||||
value={isFunc}
|
value={isFunc}
|
||||||
onChange={setIsFunc}
|
onChange={setIsFunc}
|
||||||
note="'replacement' will be evaled if this is toggled"
|
note="'replacement' will be evaled if this is toggled"
|
||||||
@ -206,7 +219,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||||||
function PatchHelper() {
|
function PatchHelper() {
|
||||||
const [find, setFind] = React.useState<string>("");
|
const [find, setFind] = React.useState<string>("");
|
||||||
const [match, setMatch] = React.useState<string>("");
|
const [match, setMatch] = React.useState<string>("");
|
||||||
const [replacement, setReplacement] = React.useState<string | Function>("");
|
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
||||||
|
|
||||||
const [replacementError, setReplacementError] = React.useState<string>();
|
const [replacementError, setReplacementError] = React.useState<string>();
|
||||||
|
|
||||||
@ -245,7 +258,7 @@ function PatchHelper() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Text variant="heading-md/normal" tag="h2" className={Margins.marginBottom8}>Patch Helper</Text>
|
<Text variant="heading-md/normal" tag="h2" className={Margins.bottom8}>Patch Helper</Text>
|
||||||
<Forms.FormTitle>find</Forms.FormTitle>
|
<Forms.FormTitle>find</Forms.FormTitle>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
@ -285,7 +298,7 @@ function PatchHelper() {
|
|||||||
|
|
||||||
{!!(find && match && replacement) && (
|
{!!(find && match && replacement) && (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.marginTop20}>Code</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
|
||||||
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
||||||
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
||||||
</>
|
</>
|
||||||
|
@ -21,7 +21,7 @@ import { useSettings } from "@api/settings";
|
|||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { LazyComponent } from "@utils/misc";
|
import { LazyComponent } from "@utils/misc";
|
||||||
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||||
import { proxyLazy } from "@utils/proxyLazy";
|
import { proxyLazy } from "@utils/proxyLazy";
|
||||||
import { OptionType, Plugin } from "@utils/types";
|
import { OptionType, Plugin } from "@utils/types";
|
||||||
import { findByCode, findByPropsLazy } from "@webpack";
|
import { findByCode, findByPropsLazy } from "@webpack";
|
||||||
@ -84,6 +84,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
|
|
||||||
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)) {
|
||||||
@ -121,33 +123,34 @@ 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]) => {
|
||||||
|
function onChange(newValue: any) {
|
||||||
|
setTempSettings(s => ({ ...s, [key]: newValue }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(hasError: boolean) {
|
||||||
|
setErrors(e => ({ ...e, [key]: hasError }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component = Components[setting.type];
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
id={key}
|
||||||
|
key={key}
|
||||||
|
option={setting}
|
||||||
|
onChange={onChange}
|
||||||
|
onError={onError}
|
||||||
|
pluginSettings={pluginSettings}
|
||||||
|
definedSettings={plugin.settings}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
|
||||||
function onChange(newValue: any) {
|
|
||||||
setTempSettings(s => ({ ...s, [key]: newValue }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError(hasError: boolean) {
|
|
||||||
setErrors(e => ({ ...e, [key]: hasError }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const Component = Components[setting.type];
|
|
||||||
return (
|
|
||||||
<Component
|
|
||||||
id={key}
|
|
||||||
key={key}
|
|
||||||
option={setting}
|
|
||||||
onChange={onChange}
|
|
||||||
onError={onError}
|
|
||||||
pluginSettings={pluginSettings}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMoreUsers(_label: string, count: number) {
|
function renderMoreUsers(_label: string, count: number) {
|
||||||
@ -172,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}
|
||||||
@ -196,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>
|
||||||
@ -206,13 +211,14 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
{renderSettings()}
|
{renderSettings()}
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
<ModalFooter>
|
{hasSettings && <ModalFooter>
|
||||||
<Flex flexDirection="column" style={{ width: "100%" }}>
|
<Flex flexDirection="column" style={{ width: "100%" }}>
|
||||||
<Flex style={{ marginLeft: "auto" }}>
|
<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}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@ -233,7 +239,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
</Flex>
|
</Flex>
|
||||||
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>}
|
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>}
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalFooter>
|
</ModalFooter>}
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import { Forms, 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);
|
||||||
@ -37,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 {
|
||||||
@ -51,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}
|
||||||
|
@ -23,7 +23,7 @@ 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);
|
||||||
@ -37,10 +37,13 @@ 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;
|
||||||
|
|
||||||
|
setError(null);
|
||||||
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) {
|
|
||||||
|
if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
||||||
setState(`${Number.MAX_SAFE_INTEGER}`);
|
setState(`${Number.MAX_SAFE_INTEGER}`);
|
||||||
onChange(serialize(newValue));
|
onChange(serialize(newValue));
|
||||||
} else {
|
} else {
|
||||||
@ -58,7 +61,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>}
|
||||||
|
@ -21,7 +21,7 @@ import { Forms, React, Select } 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);
|
||||||
@ -32,10 +32,11 @@ 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 {
|
||||||
|
setError(null);
|
||||||
setState(newValue);
|
setState(newValue);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
@ -45,7 +46,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
|
|||||||
<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={option.options}
|
options={option.options}
|
||||||
placeholder={option.placeholder ?? "Select an option"}
|
placeholder={option.placeholder ?? "Select an option"}
|
||||||
maxVisibleItems={5}
|
maxVisibleItems={5}
|
||||||
|
@ -29,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);
|
||||||
@ -39,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 {
|
||||||
@ -52,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]}
|
||||||
|
@ -21,7 +21,7 @@ import { Forms, React, TextInput } from "@webpack/common";
|
|||||||
|
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function SettingTextComponent({ 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);
|
||||||
|
|
||||||
@ -30,10 +30,11 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE
|
|||||||
}, [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 {
|
||||||
|
setError(null);
|
||||||
setState(newValue);
|
setState(newValue);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
@ -47,7 +48,7 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE
|
|||||||
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,8 +27,10 @@ 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 "./SettingCustomComponent";
|
||||||
export * from "./SettingNumericComponent";
|
export * from "./SettingNumericComponent";
|
||||||
|
@ -16,26 +16,33 @@
|
|||||||
* 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 "./styles.css";
|
||||||
|
|
||||||
|
import * as DataStore from "@api/DataStore";
|
||||||
import { showNotice } from "@api/Notices";
|
import { showNotice } from "@api/Notices";
|
||||||
import { Settings, useSettings } from "@api/settings";
|
import { useSettings } from "@api/settings";
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { ErrorCard } from "@components/ErrorCard";
|
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
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 { ChangeList } from "@utils/ChangeList";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
import { classes, LazyComponent } from "@utils/misc";
|
import { Margins } from "@utils/margins";
|
||||||
|
import { classes, LazyComponent, useAwaiter } from "@utils/misc";
|
||||||
import { openModalLazy } from "@utils/modal";
|
import { openModalLazy } from "@utils/modal";
|
||||||
import { Plugin } from "@utils/types";
|
import { Plugin } from "@utils/types";
|
||||||
import { findByCode, findByPropsLazy } from "@webpack";
|
import { findByCode, findByPropsLazy } from "@webpack";
|
||||||
import { Alerts, Button, Forms, Margins, Parser, React, Select, Switch, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
import { Alerts, Button, Card, Forms, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
||||||
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 InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
|
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
|
||||||
@ -54,23 +61,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,17 +89,13 @@ 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}.enabled`]).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() {
|
||||||
openModalLazy(async () => {
|
openModalLazy(async () => {
|
||||||
@ -110,7 +117,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 +125,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,53 +145,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}>
|
||||||
<Switch
|
<div className={cl("card-header")}>
|
||||||
onChange={toggleEnabled}
|
<Text variant="text-md/bold" className={cl("name")}>
|
||||||
disabled={disabled}
|
{plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||||
value={isEnabled()}
|
</Text>
|
||||||
note={<Text variant="text-md/normal" style={{
|
<button role="switch" onClick={() => openModal()} className={classes("button-12Fmur", cl("info-button"))}>
|
||||||
height: 40,
|
{plugin.options
|
||||||
overflow: "hidden",
|
? <CogWheel />
|
||||||
// mfw css is so bad you need whatever this is to get multi line overflow ellipsis to work
|
: <InfoIcon width="24" height="24" />}
|
||||||
textOverflow: "ellipsis",
|
</button>
|
||||||
display: "-webkit-box", // firefox users will cope (it doesn't support it)
|
<Switch
|
||||||
WebkitLineClamp: 2,
|
checked={isEnabled()}
|
||||||
lineClamp: 2,
|
onChange={toggleEnabled}
|
||||||
WebkitBoxOrient: "vertical",
|
disabled={disabled}
|
||||||
boxOrient: "vertical"
|
/>
|
||||||
}}>
|
</div>
|
||||||
{plugin.description}
|
<Text className={cl("note")} variant="text-sm/normal">{plugin.description}</Text>
|
||||||
</Text>}
|
</Flex >
|
||||||
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
|
|
||||||
width="24" height="24"
|
|
||||||
style={{ color: iconHover ? "" : "var(--text-muted)" }}
|
|
||||||
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>(), []);
|
||||||
|
|
||||||
@ -225,41 +217,102 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||||||
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.description.toLowerCase().includes(searchValue.value.toLowerCase())
|
||||||
plugin.name.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 (
|
||||||
<Forms.FormSection>
|
<Forms.FormSection className={Margins.top16}>
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
<ReloadRequiredCard required={changes.hasChanges} />
|
||||||
|
|
||||||
|
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
||||||
Filters
|
Filters
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
|
|
||||||
<ReloadRequiredCard plugins={[...changes.getChanges()]} style={{ marginBottom: 16 }} />
|
<div className={cl("filter-controls")}>
|
||||||
|
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
|
||||||
<div style={styles.FiltersBar}>
|
|
||||||
<TextInput value={searchValue.value} placeholder={"Search for a plugin..."} onChange={onSearch} style={{ marginBottom: 24 }} />
|
|
||||||
<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={String}
|
serialize={String}
|
||||||
select={onStatusChange}
|
select={onStatusChange}
|
||||||
@ -269,51 +322,19 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
|
||||||
|
|
||||||
<div style={styles.PluginsGrid}>
|
<div className={cl("grid")}>
|
||||||
{sortedPlugins?.length ? sortedPlugins
|
{plugins}
|
||||||
.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;
|
|
||||||
return <PluginCard
|
|
||||||
onRestartNeeded={name => changes.add(name)}
|
|
||||||
disabled={plugin.required || !!dependency}
|
|
||||||
plugin={plugin}
|
|
||||||
key={plugin.name}
|
|
||||||
/>;
|
|
||||||
})
|
|
||||||
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<Forms.FormDivider />
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
<Forms.FormDivider className={Margins.top20} />
|
||||||
|
|
||||||
|
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
||||||
Required Plugins
|
Required Plugins
|
||||||
</Forms.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} key={plugin.name}>
|
|
||||||
{({ onMouseLeave, onMouseEnter }) => (
|
|
||||||
<PluginCard
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onRestartNeeded={name => changes.handleChange(name)}
|
|
||||||
disabled={plugin.required || !!dependency}
|
|
||||||
plugin={plugin}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Tooltip>;
|
|
||||||
})
|
|
||||||
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</Forms.FormSection >
|
</Forms.FormSection >
|
||||||
);
|
);
|
||||||
@ -326,11 +347,7 @@ function makeDependencyList(deps: string[]) {
|
|||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
||||||
{deps.map((dep: string) => <Forms.FormText style={{ margin: "0 auto" }}>{dep}</Forms.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
|
|
||||||
};
|
|
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;
|
||||||
|
}
|
77
src/components/Switch.tsx
Normal file
77
src/components/Switch.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* 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 { classes } from "@utils/misc";
|
||||||
|
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={classes(SwitchClasses.container, "default-colors", checked ? SwitchClasses.checked : void 0)} 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>
|
||||||
|
);
|
||||||
|
}
|
@ -18,30 +18,26 @@
|
|||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { classes } from "@utils/misc";
|
||||||
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
||||||
import { Button, Card, Forms, Margins, Text } from "@webpack/common";
|
import { Button, Card, Forms, Text } from "@webpack/common";
|
||||||
|
|
||||||
function BackupRestoreTab() {
|
function BackupRestoreTab() {
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection title="Settings Sync">
|
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
|
||||||
<Card style={{
|
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
||||||
backgroundColor: "var(--info-warning-background)",
|
|
||||||
borderColor: "var(--info-warning-foreground)",
|
|
||||||
color: "var(--info-warning-text)",
|
|
||||||
padding: "1em",
|
|
||||||
marginBottom: "0.5em",
|
|
||||||
}}>
|
|
||||||
<Flex flexDirection="column">
|
<Flex flexDirection="column">
|
||||||
<strong>Warning</strong>
|
<strong>Warning</strong>
|
||||||
<span>Importing a settings file will overwrite your current settings.</span>
|
<span>Importing a settings file will overwrite your current settings.</span>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
<Text variant="text-md/normal" className={Margins.bottom8}>
|
||||||
You can import and export your Vencord settings as a JSON file.
|
You can import and export your Vencord settings as a JSON file.
|
||||||
This allows you to easily transfer your settings to another device,
|
This allows you to easily transfer your settings to another device,
|
||||||
or recover your settings after reinstalling Vencord or Discord.
|
or recover your settings after reinstalling Vencord or Discord.
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
<Text variant="text-md/normal" className={Margins.bottom8}>
|
||||||
Settings Export contains:
|
Settings Export contains:
|
||||||
<ul>
|
<ul>
|
||||||
<li>— Custom QuickCSS</li>
|
<li>— Custom QuickCSS</li>
|
||||||
@ -50,7 +46,7 @@ function BackupRestoreTab() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Flex>
|
<Flex>
|
||||||
<Button
|
<Button
|
||||||
onClick={uploadSettingsBackup}
|
onClick={() => uploadSettingsBackup()}
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
>
|
>
|
||||||
Import Settings
|
Import Settings
|
||||||
|
@ -19,9 +19,10 @@
|
|||||||
import { useSettings } from "@api/settings";
|
import { useSettings } from "@api/settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { useAwaiter } from "@utils/misc";
|
import { useAwaiter } from "@utils/misc";
|
||||||
import { findLazy } from "@webpack";
|
import { findLazy } from "@webpack";
|
||||||
import { Card, Forms, Margins, React, TextArea } from "@webpack/common";
|
import { Card, Forms, React, TextArea } from "@webpack/common";
|
||||||
|
|
||||||
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
||||||
|
|
||||||
@ -51,13 +52,14 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle className={Margins.marginTop20} tag="h5">Validator</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
|
||||||
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
||||||
<div>
|
<div>
|
||||||
{themeLinks.map(link => (
|
{themeLinks.map(link => (
|
||||||
<Card style={{
|
<Card style={{
|
||||||
padding: ".5em",
|
padding: ".5em",
|
||||||
marginBottom: ".5em"
|
marginBottom: ".5em",
|
||||||
|
marginTop: ".5em"
|
||||||
}} key={link}>
|
}} key={link}>
|
||||||
<Forms.FormTitle tag="h5" style={{
|
<Forms.FormTitle tag="h5" style={{
|
||||||
overflowWrap: "break-word"
|
overflowWrap: "break-word"
|
||||||
@ -74,11 +76,11 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
|||||||
|
|
||||||
export default ErrorBoundary.wrap(function () {
|
export default ErrorBoundary.wrap(function () {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const ref = React.useRef<HTMLTextAreaElement>();
|
const [themeText, setThemeText] = React.useState(settings.themeLinks.join("\n"));
|
||||||
|
|
||||||
function onBlur() {
|
function onBlur() {
|
||||||
settings.themeLinks = [...new Set(
|
settings.themeLinks = [...new Set(
|
||||||
ref.current!.value
|
themeText
|
||||||
.trim()
|
.trim()
|
||||||
.split(/\n+/)
|
.split(/\n+/)
|
||||||
.map(s => s.trim())
|
.map(s => s.trim())
|
||||||
@ -88,28 +90,24 @@ export default ErrorBoundary.wrap(function () {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card style={{
|
<Card className="vc-settings-card">
|
||||||
padding: "1em",
|
|
||||||
marginBottom: "1em",
|
|
||||||
marginTop: "1em"
|
|
||||||
}}>
|
|
||||||
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
|
||||||
<Forms.FormText>One link per line</Forms.FormText>
|
<Forms.FormText>One link per line</Forms.FormText>
|
||||||
<Forms.FormText>Be careful to use the raw links or github.io links!</Forms.FormText>
|
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
|
||||||
<Forms.FormDivider />
|
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
|
||||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
||||||
<div>
|
<div style={{ marginBottom: ".5em" }}>
|
||||||
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
||||||
BetterDiscord Themes
|
BetterDiscord Themes
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="https://github.com/search?q=discord+theme">Github</Link>
|
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
|
||||||
</div>
|
</div>
|
||||||
<Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText>
|
<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>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>
|
<Forms.FormText>
|
||||||
If the theme has configuration that requires you to edit the file:
|
If the theme has configuration that requires you to edit the file:
|
||||||
<ul>
|
<ul>
|
||||||
<li>• Make a github account</li>
|
<li>• Make a <Link href="https://github.com/signup">GitHub</Link> account</li>
|
||||||
<li>• Click the fork button on the top right</li>
|
<li>• Click the fork button on the top right</li>
|
||||||
<li>• Edit the file</li>
|
<li>• Edit the file</li>
|
||||||
<li>• Use the link to your own repository instead</li>
|
<li>• Use the link to your own repository instead</li>
|
||||||
@ -118,13 +116,9 @@ export default ErrorBoundary.wrap(function () {
|
|||||||
</Card>
|
</Card>
|
||||||
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
|
||||||
<TextArea
|
<TextArea
|
||||||
style={{
|
value={themeText}
|
||||||
padding: ".5em",
|
onChange={e => setThemeText(e.currentTarget.value)}
|
||||||
border: "1px solid var(--background-modifier-accent)"
|
className={`${TextAreaProps.textarea} vc-settings-theme-links`}
|
||||||
}}
|
|
||||||
ref={ref}
|
|
||||||
defaultValue={settings.themeLinks.join("\n")}
|
|
||||||
className={TextAreaProps.textarea}
|
|
||||||
placeholder="Theme Links"
|
placeholder="Theme Links"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
|
@ -16,14 +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 { useSettings } from "@api/settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { ErrorCard } from "@components/ErrorCard";
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { classes, useAwaiter } from "@utils/misc";
|
import { classes, useAwaiter } from "@utils/misc";
|
||||||
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
||||||
import { Alerts, Button, Card, Forms, Margins, Parser, React, Toasts } from "@webpack/common";
|
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
@ -69,14 +71,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)"
|
||||||
@ -104,14 +110,14 @@ function Updatable(props: CommonProps) {
|
|||||||
</ErrorCard>
|
</ErrorCard>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Forms.FormText className={Margins.marginBottom8}>
|
<Forms.FormText className={Margins.bottom8}>
|
||||||
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
|
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isOutdated && <Changes updates={updates} {...props} />}
|
{isOutdated && <Changes updates={updates} {...props} />}
|
||||||
|
|
||||||
<Flex className={classes(Margins.marginBottom8, Margins.marginTop8)}>
|
<Flex className={classes(Margins.bottom8, Margins.top8)}>
|
||||||
{isOutdated && <Button
|
{isOutdated && <Button
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
disabled={isUpdating || isChecking}
|
disabled={isUpdating || isChecking}
|
||||||
@ -170,7 +176,7 @@ function Updatable(props: CommonProps) {
|
|||||||
function Newer(props: CommonProps) {
|
function Newer(props: CommonProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Forms.FormText className={Margins.marginBottom8}>
|
<Forms.FormText className={Margins.bottom8}>
|
||||||
Your local copy has more recent commits. Please stash or reset them.
|
Your local copy has more recent commits. Please stash or reset them.
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Changes {...props} updates={changes} />
|
<Changes {...props} updates={changes} />
|
||||||
@ -179,6 +185,8 @@ function Newer(props: CommonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Updater() {
|
function Updater() {
|
||||||
|
const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
|
||||||
|
|
||||||
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -192,16 +200,41 @@ function Updater() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection>
|
<Forms.FormSection className={Margins.top16}>
|
||||||
|
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||||
|
<Switch
|
||||||
|
value={settings.notifyAboutUpdates}
|
||||||
|
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||||
|
note="Shows a notification 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>
|
||||||
|
<Switch
|
||||||
|
value={settings.autoUpdateNotification}
|
||||||
|
onChange={(v: boolean) => settings.autoUpdateNotification = v}
|
||||||
|
note="Shows a notification when Vencord automatically updates"
|
||||||
|
disabled={!settings.autoUpdate}
|
||||||
|
>
|
||||||
|
Get notified when an automatic update completes
|
||||||
|
</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.top8 + " " + Margins.bottom8} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
||||||
|
|
||||||
|
@ -18,31 +18,77 @@
|
|||||||
|
|
||||||
|
|
||||||
import { useSettings } from "@api/settings";
|
import { useSettings } from "@api/settings";
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
import DonateButton from "@components/DonateButton";
|
import DonateButton from "@components/DonateButton";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
import { useAwaiter } from "@utils/misc";
|
import { Margins } from "@utils/margins";
|
||||||
import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common";
|
import { identity, useAwaiter } from "@utils/misc";
|
||||||
|
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
|
||||||
|
|
||||||
const st = (style: string) => `vcSettings${style}`;
|
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() {
|
function VencordSettings() {
|
||||||
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
|
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
|
||||||
fallbackValue: "Loading..."
|
fallbackValue: "Loading..."
|
||||||
});
|
});
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
const notifSettings = settings.notifications;
|
||||||
|
|
||||||
const [donateImage] = React.useState(
|
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
|
||||||
Math.random() > 0.5
|
|
||||||
? "https://cdn.discordapp.com/emojis/1026533090627174460.png"
|
const isWindows = navigator.platform.toLowerCase().startsWith("win");
|
||||||
: "https://media.discordapp.net/stickers/1039992459209490513.png"
|
|
||||||
);
|
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"
|
||||||
|
} : {
|
||||||
|
key: "winNativeTitleBar",
|
||||||
|
title: "Use Windows' native title bar instead of Discord's custom one",
|
||||||
|
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 (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<DonateCard image={donateImage} />
|
<DonateCard image={donateImage} />
|
||||||
<Forms.FormSection title="Quick Actions">
|
<Forms.FormSection title="Quick Actions">
|
||||||
<Card className={st("QuickActionCard")}>
|
<Card className={cl("quick-actions-card")}>
|
||||||
{IS_WEB ? (
|
{IS_WEB ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => require("../Monaco").launchMonacoEditor()}
|
onClick={() => require("../Monaco").launchMonacoEditor()}
|
||||||
@ -82,34 +128,76 @@ function VencordSettings() {
|
|||||||
|
|
||||||
<Forms.FormDivider />
|
<Forms.FormDivider />
|
||||||
|
|
||||||
<Forms.FormSection title="Settings">
|
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
|
||||||
<Forms.FormText className={Margins.marginBottom20}>
|
<Forms.FormText className={Margins.bottom20}>
|
||||||
Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
|
Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Switch
|
{Switches.map(s => s && (
|
||||||
value={settings.useQuickCss}
|
<Switch
|
||||||
onChange={(v: boolean) => settings.useQuickCss = v}
|
key={s.key}
|
||||||
note="Loads styles from your QuickCss file">
|
value={settings[s.key]}
|
||||||
Use QuickCss
|
onChange={v => settings[s.key] = v}
|
||||||
</Switch>
|
note={s.note}
|
||||||
{!IS_WEB && (
|
>
|
||||||
<React.Fragment>
|
{s.title}
|
||||||
<Switch
|
</Switch>
|
||||||
value={settings.enableReactDevtools}
|
))}
|
||||||
onChange={(v: boolean) => settings.enableReactDevtools = v}
|
|
||||||
note="Requires a full restart">
|
|
||||||
Enable React Developer Tools
|
|
||||||
</Switch>
|
|
||||||
<Switch
|
|
||||||
value={settings.notifyAboutUpdates}
|
|
||||||
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
|
||||||
note="Shows a Toast on StartUp">
|
|
||||||
Get notified about new Updates
|
|
||||||
</Switch>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</Forms.FormSection>
|
</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>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -121,18 +209,10 @@ interface DonateCardProps {
|
|||||||
|
|
||||||
function DonateCard({ image }: DonateCardProps) {
|
function DonateCard({ image }: DonateCardProps) {
|
||||||
return (
|
return (
|
||||||
<Card style={{
|
<Card className={cl("card", "donate")}>
|
||||||
padding: "1em",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
marginBottom: "1em",
|
|
||||||
marginTop: "1em"
|
|
||||||
}}>
|
|
||||||
<div>
|
<div>
|
||||||
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle>
|
||||||
<Forms.FormText>
|
<Forms.FormText>Please consider supporting the development of Vencord by donating!</Forms.FormText>
|
||||||
Please consider supporting the Development of Vencord by donating!
|
|
||||||
</Forms.FormText>
|
|
||||||
<DonateButton style={{ transform: "translateX(-1em)" }} />
|
<DonateButton style={{ transform: "translateX(-1em)" }} />
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
@ -140,7 +220,7 @@ function DonateCard({ image }: DonateCardProps) {
|
|||||||
src={image}
|
src={image}
|
||||||
alt=""
|
alt=""
|
||||||
height={128}
|
height={128}
|
||||||
style={{ marginLeft: "auto", transform: "rotate(10deg)" }}
|
style={{ marginLeft: "auto", transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : "" }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
@ -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 ErrorBoundary from "@components/ErrorBoundary";
|
import "./settingsStyles.css";
|
||||||
import { findByCodeLazy } from "@webpack";
|
|
||||||
import { Forms, Router, Text } from "@webpack/common";
|
|
||||||
|
|
||||||
import cssText from "~fileContent/settingsStyles.css";
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||||
|
import { findByCodeLazy } from "@webpack";
|
||||||
|
import { Forms, SettingsRouter, Text } from "@webpack/common";
|
||||||
|
|
||||||
import BackupRestoreTab from "./BackupRestoreTab";
|
import BackupRestoreTab from "./BackupRestoreTab";
|
||||||
import PluginsTab from "./PluginsTab";
|
import PluginsTab from "./PluginsTab";
|
||||||
@ -28,11 +30,7 @@ import ThemesTab from "./ThemesTab";
|
|||||||
import Updater from "./Updater";
|
import Updater from "./Updater";
|
||||||
import VencordSettings from "./VencordTab";
|
import VencordSettings from "./VencordTab";
|
||||||
|
|
||||||
const style = document.createElement("style");
|
const cl = classNameFactory("vc-settings-");
|
||||||
style.textContent = cssText;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
|
|
||||||
const st = (style: string) => `vcSettings${style}`;
|
|
||||||
|
|
||||||
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');
|
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');
|
||||||
|
|
||||||
@ -61,20 +59,20 @@ function Settings(props: SettingsProps) {
|
|||||||
const CurrentTab = SettingsTabs[tab]?.component;
|
const CurrentTab = SettingsTabs[tab]?.component;
|
||||||
|
|
||||||
return <Forms.FormSection>
|
return <Forms.FormSection>
|
||||||
<Text variant="heading-md/normal" tag="h2">Vencord Settings</Text>
|
<Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">Vencord Settings</Text>
|
||||||
|
|
||||||
<TabBar
|
<TabBar
|
||||||
type={TabBar.Types.TOP}
|
type="top"
|
||||||
look={TabBar.Looks.BRAND}
|
look="brand"
|
||||||
className={st("TabBar")}
|
className={cl("tab-bar")}
|
||||||
selectedItem={tab}
|
selectedItem={tab}
|
||||||
onItemSelect={Router.open}
|
onItemSelect={SettingsRouter.open}
|
||||||
>
|
>
|
||||||
{Object.entries(SettingsTabs).map(([key, { name, component }]) => {
|
{Object.entries(SettingsTabs).map(([key, { name, component }]) => {
|
||||||
if (!component) return null;
|
if (!component) return null;
|
||||||
return <TabBar.Item
|
return <TabBar.Item
|
||||||
id={key}
|
id={key}
|
||||||
className={st("TabBarItem")}
|
className={cl("tab-bar-item")}
|
||||||
key={key}>
|
key={key}>
|
||||||
{name}
|
{name}
|
||||||
</TabBar.Item>;
|
</TabBar.Item>;
|
||||||
@ -86,7 +84,7 @@ function Settings(props: SettingsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function (props: SettingsProps) {
|
export default function (props: SettingsProps) {
|
||||||
return <ErrorBoundary>
|
return <ErrorBoundary onError={handleComponentFailed}>
|
||||||
<Settings tab={props.tab} />
|
<Settings tab={props.tab} />
|
||||||
</ErrorBoundary>;
|
</ErrorBoundary>;
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,48 @@
|
|||||||
.vcSettingsTabBar {
|
.vc-settings-tab-bar {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
margin-bottom: -2px;
|
margin-bottom: -2px;
|
||||||
border-bottom: 2px solid var(--background-modifier-accent);
|
border-bottom: 2px solid var(--background-modifier-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vcSettingsTabBarItem {
|
.vc-settings-tab-bar-item {
|
||||||
margin-right: 32px;
|
margin-right: 32px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
margin-bottom: -2px;
|
margin-bottom: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vcSettingsQuickActionCard {
|
.vc-settings-quick-actions-card {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-direction: row;
|
flex-flow: row wrap;
|
||||||
margin-bottom: 1em;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-settings-theme-links {
|
||||||
|
/* Needed to fix bad themes that hide certain textarea elements for whatever eldritch reason */
|
||||||
|
display: inline-block !important;
|
||||||
|
color: var(--text-normal) !important;
|
||||||
|
padding: 0.5em;
|
||||||
|
border: 1px solid var(--background-modifier-accent);
|
||||||
|
}
|
||||||
|
@ -16,29 +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 { isOutdated, rebuild, update } from "@utils/updater";
|
import { maybePromptToUpdate } from "@utils/updater";
|
||||||
|
|
||||||
export async function handleComponentFailed() {
|
export function handleComponentFailed() {
|
||||||
if (isOutdated) {
|
maybePromptToUpdate(
|
||||||
setImmediate(async () => {
|
"Uh Oh! Failed to render this Page." +
|
||||||
const wantsUpdate = confirm(
|
" However, there is an update available that might fix it." +
|
||||||
"Uh Oh! Failed to render this Page." +
|
" Would you like to update and restart now?"
|
||||||
" 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!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
11
src/globals.d.ts
vendored
11
src/globals.d.ts
vendored
@ -16,6 +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 { LoDashStatic } from "lodash";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
/**
|
/**
|
||||||
@ -37,6 +38,12 @@ declare global {
|
|||||||
|
|
||||||
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;
|
||||||
};
|
};
|
||||||
@ -44,8 +51,7 @@ declare global {
|
|||||||
* Only available when running in Electron, undefined on web.
|
* Only available when running in Electron, undefined on web.
|
||||||
* Thus, avoid using this or only use it inside an {@link IS_WEB} guard.
|
* Thus, avoid using this or only use it inside an {@link IS_WEB} guard.
|
||||||
*
|
*
|
||||||
* If you really must use it, mark your plugin as Desktop App only via
|
* If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)
|
||||||
* `target: "DESKTOP"`
|
|
||||||
*/
|
*/
|
||||||
export var DiscordNative: any;
|
export var DiscordNative: any;
|
||||||
|
|
||||||
@ -54,6 +60,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);
|
||||||
|
@ -21,7 +21,7 @@ import "./updater";
|
|||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
import { Queue } from "@utils/Queue";
|
import { Queue } from "@utils/Queue";
|
||||||
import { BrowserWindow, desktopCapturer, ipcMain, shell } from "electron";
|
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";
|
||||||
@ -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) => {
|
||||||
@ -80,7 +77,7 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, 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));
|
||||||
});
|
});
|
||||||
@ -94,7 +91,8 @@ ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
|||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, "preload.js"),
|
preload: join(__dirname, "preload.js"),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false
|
nodeIntegration: false,
|
||||||
|
sandbox: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
|
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
|
||||||
|
@ -24,7 +24,7 @@ export async function calculateHashes() {
|
|||||||
const hashes = {} as Record<string, string>;
|
const hashes = {} as Record<string, string>;
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
["patcher.js", "preload.js", "renderer.js"].map(file => new Promise<void>(r => {
|
["patcher.js", "preload.js", "renderer.js", "renderer.css"].map(file => new Promise<void>(r => {
|
||||||
const fis = createReadStream(join(__dirname, file));
|
const fis = createReadStream(join(__dirname, file));
|
||||||
const hash = createHash("sha1", { encoding: "hex" });
|
const hash = createHash("sha1", { encoding: "hex" });
|
||||||
fis.once("end", () => {
|
fis.once("end", () => {
|
||||||
|
@ -28,7 +28,9 @@ const VENCORD_SRC_DIR = join(__dirname, "..");
|
|||||||
|
|
||||||
const execFile = promisify(cpExecFile);
|
const execFile = promisify(cpExecFile);
|
||||||
|
|
||||||
const isFlatpak = Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
|
const isFlatpak = process.platform === "linux" && Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
|
||||||
|
|
||||||
|
if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`;
|
||||||
|
|
||||||
function git(...args: string[]) {
|
function git(...args: string[]) {
|
||||||
const opts = { cwd: VENCORD_SRC_DIR };
|
const opts = { cwd: VENCORD_SRC_DIR };
|
||||||
@ -66,10 +68,10 @@ async function pull() {
|
|||||||
async function build() {
|
async function build() {
|
||||||
const opts = { cwd: VENCORD_SRC_DIR };
|
const opts = { cwd: VENCORD_SRC_DIR };
|
||||||
|
|
||||||
let res;
|
const command = isFlatpak ? "flatpak-spawn" : "node";
|
||||||
|
const args = isFlatpak ? ["--host", "node", "scripts/build/build.mjs"] : ["scripts/build/build.mjs"];
|
||||||
|
|
||||||
if (isFlatpak) res = await execFile("flatpak-spawn", ["--host", "node", "scripts/build/build.mjs"], opts);
|
const res = await execFile(command, args, opts);
|
||||||
else res = await execFile("node", ["scripts/build/build.mjs"], opts);
|
|
||||||
|
|
||||||
return !res.stderr.includes("Build failed");
|
return !res.stderr.includes("Build failed");
|
||||||
}
|
}
|
||||||
|
@ -37,10 +37,7 @@ async function githubGet(endpoint: string) {
|
|||||||
Accept: "application/vnd.github+json",
|
Accept: "application/vnd.github+json",
|
||||||
// "All API requests MUST include a valid User-Agent header.
|
// "All API requests MUST include a valid User-Agent header.
|
||||||
// Requests with no User-Agent header will be rejected."
|
// Requests with no User-Agent header will be rejected."
|
||||||
"User-Agent": VENCORD_USER_AGENT,
|
"User-Agent": VENCORD_USER_AGENT
|
||||||
// todo: perhaps add support for (optional) api token?
|
|
||||||
// unauthorised rate limit is 60 reqs/h
|
|
||||||
// https://github.com/settings/tokens/new?description=Vencord%20Updater
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -52,7 +49,7 @@ async function calculateGitChanges() {
|
|||||||
const res = await githubGet(`/compare/${gitHash}...HEAD`);
|
const res = await githubGet(`/compare/${gitHash}...HEAD`);
|
||||||
|
|
||||||
const data = JSON.parse(res.toString("utf-8"));
|
const data = JSON.parse(res.toString("utf-8"));
|
||||||
return data.commits.map(c => ({
|
return data.commits.map((c: any) => ({
|
||||||
// github api only sends the long sha
|
// github api only sends the long sha
|
||||||
hash: c.sha.slice(0, 7),
|
hash: c.sha.slice(0, 7),
|
||||||
author: c.author.login,
|
author: c.author.login,
|
||||||
@ -69,7 +66,7 @@ async function fetchUpdates() {
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
data.assets.forEach(({ name, browser_download_url }) => {
|
data.assets.forEach(({ name, browser_download_url }) => {
|
||||||
if (["patcher.js", "preload.js", "renderer.js"].some(s => name.startsWith(s))) {
|
if (["patcher.js", "preload.js", "renderer.js", "renderer.css"].some(s => name.startsWith(s))) {
|
||||||
PendingUpdates.push([name, browser_download_url]);
|
PendingUpdates.push([name, browser_download_url]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
7
src/modules.d.ts
vendored
7
src/modules.d.ts
vendored
@ -37,3 +37,10 @@ declare module "~fileContent/*" {
|
|||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "*.css";
|
||||||
|
|
||||||
|
declare module "*.css?managed" {
|
||||||
|
const name: string;
|
||||||
|
export default name;
|
||||||
|
}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, autoUpdater } from "electron";
|
import { app, autoUpdater } from "electron";
|
||||||
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
|
import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
|
||||||
import { basename, dirname, join } from "path";
|
import { basename, dirname, join } from "path";
|
||||||
|
|
||||||
const { setAppUserModelId } = app;
|
const { setAppUserModelId } = app;
|
||||||
@ -44,58 +44,50 @@ function isNewer($new: string, old: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function patchLatest() {
|
function patchLatest() {
|
||||||
const currentAppPath = dirname(process.execPath);
|
try {
|
||||||
const currentVersion = basename(currentAppPath);
|
const currentAppPath = dirname(process.execPath);
|
||||||
const discordPath = join(currentAppPath, "..");
|
const currentVersion = basename(currentAppPath);
|
||||||
|
const discordPath = join(currentAppPath, "..");
|
||||||
|
|
||||||
const latestVersion = readdirSync(discordPath).reduce((prev, curr) => {
|
const latestVersion = readdirSync(discordPath).reduce((prev, curr) => {
|
||||||
return (curr.startsWith("app-") && isNewer(curr, prev))
|
return (curr.startsWith("app-") && isNewer(curr, prev))
|
||||||
? curr
|
? curr
|
||||||
: prev;
|
: prev;
|
||||||
}, currentVersion as string);
|
}, currentVersion as string);
|
||||||
|
|
||||||
if (latestVersion === currentVersion) return;
|
if (latestVersion === currentVersion) return;
|
||||||
|
|
||||||
const app = join(discordPath, latestVersion, "resources", "app");
|
const resources = join(discordPath, latestVersion, "resources");
|
||||||
if (existsSync(app)) return;
|
const app = join(resources, "app.asar");
|
||||||
|
const _app = join(resources, "_app.asar");
|
||||||
|
|
||||||
console.info("[Vencord] Detected Host Update. Repatching...");
|
if (!existsSync(app) || statSync(app).isDirectory()) return;
|
||||||
|
|
||||||
const patcherPath = join(__dirname, "patcher.js");
|
console.info("[Vencord] Detected Host Update. Repatching...");
|
||||||
mkdirSync(app);
|
|
||||||
writeFileSync(join(app, "package.json"), JSON.stringify({
|
renameSync(app, _app);
|
||||||
name: "discord",
|
mkdirSync(app);
|
||||||
main: "index.js"
|
writeFileSync(join(app, "package.json"), JSON.stringify({
|
||||||
}));
|
name: "discord",
|
||||||
writeFileSync(join(app, "index.js"), `require(${JSON.stringify(patcherPath)});`);
|
main: "index.js"
|
||||||
|
}));
|
||||||
|
writeFileSync(join(app, "index.js"), `require(${JSON.stringify(join(__dirname, "patcher.js"))});`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[Vencord] Failed to repatch latest host update", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we
|
// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we
|
||||||
// need to reinject
|
// need to reinject
|
||||||
function patchUpdater() {
|
function patchUpdater() {
|
||||||
const main = require.main!;
|
|
||||||
const buildInfo = require(join(process.resourcesPath, "build_info.json"));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (buildInfo?.newUpdater) {
|
const autoStartScript = join(require.main!.filename, "..", "autoStart", "win32.js");
|
||||||
const autoStartScript = join(main.filename, "..", "autoStart", "win32.js");
|
const { update } = require(autoStartScript);
|
||||||
const { update } = require(autoStartScript);
|
|
||||||
|
|
||||||
// New Updater Injection
|
require.cache[autoStartScript]!.exports.update = function () {
|
||||||
require.cache[autoStartScript]!.exports.update = function () {
|
update.apply(this, arguments);
|
||||||
patchLatest();
|
patchLatest();
|
||||||
update.apply(this, arguments);
|
};
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const hostUpdaterScript = join(main.filename, "..", "hostUpdater.js");
|
|
||||||
const { quitAndInstall } = require(hostUpdaterScript);
|
|
||||||
|
|
||||||
// Old Updater Injection
|
|
||||||
require.cache[hostUpdaterScript]!.exports.quitAndInstall = function () {
|
|
||||||
patchLatest();
|
|
||||||
quitAndInstall.apply(this, arguments);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// OpenAsar uses electrons autoUpdater on Windows
|
// OpenAsar uses electrons autoUpdater on Windows
|
||||||
const { quitAndInstall } = autoUpdater;
|
const { quitAndInstall } = autoUpdater;
|
||||||
|
@ -17,8 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { onceDefined } from "@utils/onceDefined";
|
import { onceDefined } from "@utils/onceDefined";
|
||||||
import electron, { app, BrowserWindowConstructorOptions } from "electron";
|
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
|
||||||
import { readFileSync } from "fs";
|
|
||||||
import { dirname, join } from "path";
|
import { dirname, join } from "path";
|
||||||
|
|
||||||
import { initIpc } from "./ipcMain";
|
import { initIpc } from "./ipcMain";
|
||||||
@ -43,16 +42,51 @@ require.main!.filename = join(asarPath, discordPkg.main);
|
|||||||
app.setAppPath(asarPath);
|
app.setAppPath(asarPath);
|
||||||
|
|
||||||
if (!process.argv.includes("--vanilla")) {
|
if (!process.argv.includes("--vanilla")) {
|
||||||
|
let settings: typeof import("@api/settings").Settings = {} as any;
|
||||||
|
try {
|
||||||
|
settings = JSON.parse(readSettings());
|
||||||
|
} catch { }
|
||||||
|
|
||||||
// Repatch after host updates on Windows
|
// Repatch after host updates on Windows
|
||||||
if (process.platform === "win32")
|
if (process.platform === "win32") {
|
||||||
require("./patchWin32Updater");
|
require("./patchWin32Updater");
|
||||||
|
|
||||||
|
if (settings.winCtrlQ) {
|
||||||
|
const originalBuild = Menu.buildFromTemplate;
|
||||||
|
Menu.buildFromTemplate = function (template) {
|
||||||
|
if (template[0]?.label === "&File") {
|
||||||
|
const { submenu } = template[0];
|
||||||
|
if (Array.isArray(submenu)) {
|
||||||
|
submenu.push({
|
||||||
|
label: "Quit (Hidden)",
|
||||||
|
visible: false,
|
||||||
|
acceleratorWorksWhenHidden: true,
|
||||||
|
accelerator: "Control+Q",
|
||||||
|
click: () => app.quit()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originalBuild.call(this, template);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class BrowserWindow extends electron.BrowserWindow {
|
class BrowserWindow extends electron.BrowserWindow {
|
||||||
constructor(options: BrowserWindowConstructorOptions) {
|
constructor(options: BrowserWindowConstructorOptions) {
|
||||||
if (options?.webPreferences?.preload && options.title) {
|
if (options?.webPreferences?.preload && options.title) {
|
||||||
const original = options.webPreferences.preload;
|
const original = options.webPreferences.preload;
|
||||||
options.webPreferences.preload = join(__dirname, "preload.js");
|
options.webPreferences.preload = join(__dirname, "preload.js");
|
||||||
options.webPreferences.sandbox = false;
|
options.webPreferences.sandbox = false;
|
||||||
|
if (settings.frameless) {
|
||||||
|
options.frame = false;
|
||||||
|
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
|
||||||
|
delete options.frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.transparent) {
|
||||||
|
options.transparent = true;
|
||||||
|
options.backgroundColor = "#00000000";
|
||||||
|
}
|
||||||
|
|
||||||
process.env.DISCORD_PRELOAD = original;
|
process.env.DISCORD_PRELOAD = original;
|
||||||
|
|
||||||
@ -100,8 +134,7 @@ if (!process.argv.includes("--vanilla")) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = JSON.parse(readSettings());
|
if (settings?.enableReactDevtools)
|
||||||
if (settings.enableReactDevtools)
|
|
||||||
installExt("fmkadmapgofadopljbjfkapdkoienihi")
|
installExt("fmkadmapgofadopljbjfkapdkoienihi")
|
||||||
.then(() => console.info("[Vencord] Installed React Developer Tools"))
|
.then(() => console.info("[Vencord] Installed React Developer Tools"))
|
||||||
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
|
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
|
||||||
@ -160,21 +193,4 @@ if (!process.argv.includes("--vanilla")) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log("[Vencord] Loading original Discord app.asar");
|
console.log("[Vencord] Loading original Discord app.asar");
|
||||||
// Legacy Vencord Injector requires "../app.asar". However, because we
|
require(require.main!.filename);
|
||||||
// restore the require.main above this is messed up, so monkey patch Module._load to
|
|
||||||
// redirect such requires
|
|
||||||
// FIXME: remove this eventually
|
|
||||||
if (readFileSync(injectorPath, "utf-8").includes('require("../app.asar")')) {
|
|
||||||
console.warn("[Vencord] [--> WARNING <--] You have a legacy Vencord install. Please reinject");
|
|
||||||
const Module = require("module");
|
|
||||||
const loadModule = Module._load;
|
|
||||||
Module._load = function (path: string) {
|
|
||||||
if (path === "../app.asar") {
|
|
||||||
Module._load = loadModule;
|
|
||||||
arguments[0] = require.main!.filename;
|
|
||||||
}
|
|
||||||
return loadModule.apply(this, arguments);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
require(require.main!.filename);
|
|
||||||
}
|
|
||||||
|
42
src/plugins/alwaysTrust.ts
Normal file
42
src/plugins/alwaysTrust.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "AlwaysTrust",
|
||||||
|
description: "Removes the annoying untrusted domain and suspicious file popup",
|
||||||
|
authors: [Devs.zt],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".displayName=\"MaskedLinkStore\"",
|
||||||
|
replacement: {
|
||||||
|
match: /\.isTrustedDomain=function\(.\){return.+?};/,
|
||||||
|
replace: ".isTrustedDomain=function(){return true};"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: '"7z","ade","adp"',
|
||||||
|
replacement: {
|
||||||
|
match: /JSON\.parse\('\[.+?'\)/,
|
||||||
|
replace: "[]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
@ -36,7 +36,7 @@ export default definePlugin({
|
|||||||
replacement: {
|
replacement: {
|
||||||
match: /uploadFiles:(.{1,2}),/,
|
match: /uploadFiles:(.{1,2}),/,
|
||||||
replace:
|
replace:
|
||||||
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=Vencord.Plugins.plugins.AnonymiseFileNames.anonymise(f.filename)),$1(...args)),",
|
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f.filename)),$1(...args)),",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -24,9 +24,10 @@ import { Heart } from "@components/Heart";
|
|||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
import IpcEvents from "@utils/IpcEvents";
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "@utils/Logger";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { closeModal, Modals, openModal } from "@utils/modal";
|
import { closeModal, Modals, openModal } from "@utils/modal";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Forms, Margins } from "@webpack/common";
|
import { Forms } from "@webpack/common";
|
||||||
|
|
||||||
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
|
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
|
||||||
|
|
||||||
@ -66,11 +67,20 @@ export default definePlugin({
|
|||||||
/* Patch the badge list component on user profiles */
|
/* Patch the badge list component on user profiles */
|
||||||
{
|
{
|
||||||
find: "Messages.PROFILE_USER_BADGES,role:",
|
find: "Messages.PROFILE_USER_BADGES,role:",
|
||||||
replacement: {
|
replacement: [
|
||||||
match: /src:(\w{1,3})\[(\w{1,3})\.key\],/,
|
{
|
||||||
// <img src={badge.image ?? imageMap[badge.key]} {...badge.props} />
|
match: /src:(\w{1,3})\[(\w{1,3})\.key\],/,
|
||||||
replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,`
|
// <img src={badge.image ?? imageMap[badge.key]} {...badge.props} />
|
||||||
}
|
replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /spacing:(\d{1,2}),children:(.{1,40}(\i)\.jsx.+?(\i)\.onClick.+?\)})},/,
|
||||||
|
// if the badge provides it's own component, render that instead of an image
|
||||||
|
// the badge also includes info about the user that has it (type BadgeUserArgs), which is why it's passed as props
|
||||||
|
replace: (_, s, origBadgeComponent, React, badge) =>
|
||||||
|
`spacing:${s},children:${badge}.component ? () => (0,${React}.jsx)(${badge}.component, { ...${badge} }) : ${origBadgeComponent}},`
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -141,7 +151,7 @@ export default definePlugin({
|
|||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
This Badge is a special perk for Vencord Donors
|
This Badge is a special perk for Vencord Donors
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Forms.FormText className={Margins.marginTop20}>
|
<Forms.FormText className={Margins.top20}>
|
||||||
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
</div>
|
</div>
|
||||||
|
@ -50,10 +50,10 @@ export default definePlugin({
|
|||||||
},
|
},
|
||||||
// Show plugin name instead of "Built-In"
|
// Show plugin name instead of "Built-In"
|
||||||
{
|
{
|
||||||
find: "().source,children",
|
find: ".source,children",
|
||||||
replacement: {
|
replacement: {
|
||||||
// ...children: p?.name
|
// ...children: p?.name
|
||||||
match: /(?<=:(.{1,3})\.displayDescription\}.{0,200}\(\)\.source,children:)[^}]+/,
|
match: /(?<=:(.{1,3})\.displayDescription\}.{0,200}\.source,children:)[^}]+/,
|
||||||
replace: "$1.plugin||($&)"
|
replace: "$1.plugin||($&)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
99
src/plugins/apiContextMenu.ts
Normal file
99
src/plugins/apiContextMenu.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Settings } from "@api/settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { type PatchReplacement } from "@utils/types";
|
||||||
|
import { addListener, removeListener } from "@webpack";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last var name corresponding to the Context Menu API (Discord, not ours) module
|
||||||
|
*/
|
||||||
|
let lastVarName = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param target The patch replacement object
|
||||||
|
* @param exportKey The key exporting the build Context Menu component function
|
||||||
|
*/
|
||||||
|
function makeReplacementProxy(target: PatchReplacement, exportKey: string) {
|
||||||
|
return new Proxy(target, {
|
||||||
|
get(_, p) {
|
||||||
|
if (p === "match") return RegExp(`${exportKey},{(?<=${lastVarName}\\.${exportKey},{)`, "g");
|
||||||
|
// @ts-expect-error
|
||||||
|
return Reflect.get(...arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function listener(exports: any, id: number) {
|
||||||
|
if (!Settings.plugins.ContextMenuAPI.enabled) return removeListener(listener);
|
||||||
|
|
||||||
|
if (typeof exports !== "object" || exports === null) return;
|
||||||
|
|
||||||
|
for (const key in exports) if (key.length <= 3) {
|
||||||
|
const prop = exports[key];
|
||||||
|
if (typeof prop !== "function") continue;
|
||||||
|
|
||||||
|
const str = Function.prototype.toString.call(prop);
|
||||||
|
if (str.includes('path:["empty"]')) {
|
||||||
|
Vencord.Plugins.patches.push({
|
||||||
|
plugin: "ContextMenuAPI",
|
||||||
|
all: true,
|
||||||
|
noWarn: true,
|
||||||
|
find: "navId:",
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
// Set the lastVarName for our proxy to use
|
||||||
|
match: RegExp(`${id}(?<=(\\i)=.+?)`),
|
||||||
|
replace: (id, varName) => {
|
||||||
|
lastVarName = varName;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* We are using a proxy here to utilize the whole code the patcher gives us, instead of matching the entire module (which is super slow)
|
||||||
|
* Our proxy returns the corresponding match for that module utilizing lastVarName, which is set by the patch before
|
||||||
|
*/
|
||||||
|
makeReplacementProxy({
|
||||||
|
match: "", // Needed to canonicalizeDescriptor
|
||||||
|
replace: "$&contextMenuApiArguments:arguments,",
|
||||||
|
}, key)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
removeListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(listener);
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ContextMenuAPI",
|
||||||
|
description: "API for adding/removing items to/from context menus.",
|
||||||
|
authors: [Devs.Nuckyz],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "♫ (つ。◕‿‿◕。)つ ♪",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=function \i\((\i)\){)(?=var \i,\i=\i\.navId)/,
|
||||||
|
replace: (_, props) => `Vencord.Api.ContextMenu._patchContextMenu(${props});`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
42
src/plugins/apiMemberListDecorators.ts
Normal file
42
src/plugins/apiMemberListDecorators.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "MemberListDecoratorsAPI",
|
||||||
|
description: "API to add decorators to member list (both in servers and DMs)",
|
||||||
|
authors: [Devs.TheSun],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "lostPermissionTooltipText,",
|
||||||
|
replacement: {
|
||||||
|
match: /Fragment,{children:\[(.{30,80})\]/,
|
||||||
|
replace: "Fragment,{children:Vencord.Api.MemberListDecorators.__addDecoratorsToList(this.props).concat($1)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "PrivateChannel.renderAvatar",
|
||||||
|
replacement: {
|
||||||
|
match: /(subText:(.{1,2})\.renderSubtitle\(\).{1,50}decorators):(.{30,100}:null)/,
|
||||||
|
replace: "$1:Vencord.Api.MemberListDecorators.__addDecoratorsToList($2.props).concat($3)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
@ -43,7 +43,7 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: '"Menu API',
|
find: '"Menu API',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /function.{0,80}type===(.{1,3})\..{1,3}\).{0,50}navigable:.+?Menu API/s,
|
match: /function.{0,80}type===(\i)\).{0,50}navigable:.+?Menu API/s,
|
||||||
replace: (m, mod) => {
|
replace: (m, mod) => {
|
||||||
let nicenNames = "";
|
let nicenNames = "";
|
||||||
const redefines = [] as string[];
|
const redefines = [] as string[];
|
||||||
|
@ -25,9 +25,9 @@ export default definePlugin({
|
|||||||
authors: [Devs.Cyn],
|
authors: [Devs.Cyn],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "_messageAttachmentToEmbedMedia",
|
find: ".Messages.REMOVE_ATTACHMENT_BODY",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(\(\)\.container\)?,children:)(\[[^\]]+\])(}\)\};return)/,
|
match: /(.container\)?,children:)(\[[^\]]+\])(}\)\};return)/,
|
||||||
replace: (_, pre, accessories, post) =>
|
replace: (_, pre, accessories, post) =>
|
||||||
`${pre}Vencord.Api.MessageAccessories._modifyAccessories(${accessories},this.props)${post}`,
|
`${pre}Vencord.Api.MessageAccessories._modifyAccessories(${accessories},this.props)${post}`,
|
||||||
},
|
},
|
||||||
|
35
src/plugins/apiMessageDecorations.ts
Normal file
35
src/plugins/apiMessageDecorations.ts
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "MessageDecorationsAPI",
|
||||||
|
description: "API to add decorations to messages",
|
||||||
|
authors: [Devs.TheSun],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".withMentionPrefix",
|
||||||
|
replacement: {
|
||||||
|
match: /(.roleDot.{10,50}{children:.{1,2})}\)/,
|
||||||
|
replace: "$1.concat(Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0]))})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
@ -22,22 +22,22 @@ import definePlugin from "@utils/types";
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "MessageEventsAPI",
|
name: "MessageEventsAPI",
|
||||||
description: "Api required by anything using message events.",
|
description: "Api required by anything using message events.",
|
||||||
authors: [Devs.Arjix],
|
authors: [Devs.Arjix, Devs.hunt],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "sendMessage:function",
|
find: '"MessageActionCreators"',
|
||||||
replacement: [{
|
replacement: [{
|
||||||
match: /(?<=_sendMessage:function\([^)]+\)){/,
|
match: /_sendMessage:(function\([^)]+\)){/,
|
||||||
replace: "{if(Vencord.Api.MessageEvents._handlePreSend(...arguments)){return;};"
|
replace: "_sendMessage:async $1{if(await Vencord.Api.MessageEvents._handlePreSend(...arguments))return;"
|
||||||
}, {
|
}, {
|
||||||
match: /(?<=\beditMessage:function\([^)]+\)){/,
|
match: /\beditMessage:(function\([^)]+\)){/,
|
||||||
replace: "{Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
|
replace: "editMessage:async $1{await Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: '("interactionUsernameProfile',
|
find: '("interactionUsernameProfile',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /var \w=(\w)\.id,\w=(\w)\.id;return .{1,2}\.useCallback\(\(?function\((.{1,2})\){/,
|
match: /var \i=(\i)\.id,\i=(\i)\.id;return \i\.useCallback\(\(?function\((\i)\){/,
|
||||||
replace: (m, message, channel, event) =>
|
replace: (m, message, channel, event) =>
|
||||||
// the message param is shadowed by the event param, so need to alias them
|
// the message param is shadowed by the event param, so need to alias them
|
||||||
`var _msg=${message},_chan=${channel};${m}Vencord.Api.MessageEvents._handleClick(_msg, _chan, ${event});`
|
`var _msg=${message},_chan=${channel};${m}Vencord.Api.MessageEvents._handleClick(_msg, _chan, ${event});`
|
||||||
|
@ -22,12 +22,17 @@ import definePlugin from "@utils/types";
|
|||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "MessagePopoverAPI",
|
name: "MessagePopoverAPI",
|
||||||
description: "API to add buttons to message popovers.",
|
description: "API to add buttons to message popovers.",
|
||||||
authors: [Devs.KingFish],
|
authors: [Devs.KingFish, Devs.Ven],
|
||||||
patches: [{
|
patches: [{
|
||||||
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
|
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(message:(.).{0,100}Fragment,\{children:\[)(.{0,90}renderPopout:.{0,200}message_reaction_emoji_picker.+?return (.{1,3})\(.{0,30}"add-reaction")/,
|
// foo && !bar ? createElement(blah,...makeElement(addReactionData))
|
||||||
replace: "$1...Vencord.Api.MessagePopover._buildPopoverElements($2,$4),$3"
|
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/,
|
||||||
|
replace: (m, makeElement) => {
|
||||||
|
const msg = m.match(/message:(.{1,3}),/)?.[1];
|
||||||
|
if (!msg) throw new Error("Could not find message variable");
|
||||||
|
return `...Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}),${m}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
|
@ -16,12 +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 { migratePluginSettings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
migratePluginSettings("NoticesAPI", "NoticesApi");
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "NoticesAPI",
|
name: "NoticesAPI",
|
||||||
description: "Fixes notices being automatically dismissed",
|
description: "Fixes notices being automatically dismissed",
|
||||||
@ -29,16 +26,15 @@ export default definePlugin({
|
|||||||
required: true,
|
required: true,
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "updateNotice:",
|
find: 'displayName="NoticeStore"',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /;(.{1,2}=null;)(?=.{0,50}updateNotice)/g,
|
match: /(?=;\i=null;.{0,70}getPremiumSubscription)/g,
|
||||||
replace:
|
replace: ";if(Vencord.Api.Notices.currentNotice)return false"
|
||||||
";if(Vencord.Api.Notices.currentNotice)return !1;$1"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?<=NOTICE_DISMISS:function.+?){(?=if\(null==(.+?)\))/,
|
match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/,
|
||||||
replace: '{if($1?.id=="VencordNotice")return ($1=null,Vencord.Api.Notices.nextNotice(),true);'
|
replace: (_, notice) => `if(${notice}.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user