Compare commits
166 Commits
devbuild
...
feat/relat
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0e06b8d34c | ||
![]() |
b972aa1663 | ||
![]() |
3bf81ee0fa | ||
![]() |
486230a335 | ||
![]() |
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
|
35
.github/workflows/build.yml
vendored
35
.github/workflows/build.yml
vendored
@ -34,31 +34,42 @@ 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: 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
@ -41,3 +41,17 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
|
- name: Create Report (Canary)
|
||||||
|
timeout-minutes: 10
|
||||||
|
if: success() || failure() # even run if previous one failed
|
||||||
|
run: |
|
||||||
|
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||||
|
export CHROMIUM_BIN=$(which chromium-browser)
|
||||||
|
export USE_CANARY=true
|
||||||
|
|
||||||
|
esbuild test/generateReport.ts > dist/report.mjs
|
||||||
|
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||||
|
env:
|
||||||
|
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||||
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
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"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
39
README.md
39
README.md
@ -1,47 +1,30 @@
|
|||||||
# 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 (one click installer)
|
||||||
- Many plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
- 90+ 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
|
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
||||||
- 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)
|
- Works in all Electron versions (Confirmed working on versions 13-23)
|
||||||
|
- 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)
|
[](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 [](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip), [](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.
|
[](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [](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 QuickCSS and plugins making use of external resources will not work with the UserScript.
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
107
browser/GMPolyfill.js
Normal file
107
browser/GMPolyfill.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fetchOptions(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const opt = {
|
||||||
|
method: "OPTIONS",
|
||||||
|
url: url,
|
||||||
|
};
|
||||||
|
opt.onload = resp => resolve(resp.responseHeaders);
|
||||||
|
opt.ontimeout = () => reject("fetch timeout");
|
||||||
|
opt.onerror = () => reject("fetch error");
|
||||||
|
opt.onabort = () => reject("fetch abort");
|
||||||
|
GM_xmlhttpRequest(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHeaders(headers) {
|
||||||
|
if (!headers)
|
||||||
|
return {};
|
||||||
|
const result = {};
|
||||||
|
const headersArr = headers.trim().split("\n");
|
||||||
|
for (var i = 0; i < headersArr.length; i++) {
|
||||||
|
var row = headersArr[i];
|
||||||
|
var index = row.indexOf(":")
|
||||||
|
, key = row.slice(0, index).trim().toLowerCase()
|
||||||
|
, value = row.slice(index + 1).trim();
|
||||||
|
|
||||||
|
if (result[key] === undefined) {
|
||||||
|
result[key] = value;
|
||||||
|
} else if (Array.isArray(result[key])) {
|
||||||
|
result[key].push(value);
|
||||||
|
} else {
|
||||||
|
result[key] = [result[key], value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if CORS permits request
|
||||||
|
async function checkCors(url, method) {
|
||||||
|
const headers = parseHeaders(await fetchOptions(url));
|
||||||
|
|
||||||
|
const origin = headers["access-control-allow-origin"];
|
||||||
|
if (origin !== "*" && origin !== window.location.origin) return false;
|
||||||
|
|
||||||
|
const methods = headers["access-control-allow-methods"]?.split(/,\s/g);
|
||||||
|
if (methods && !methods.includes(method)) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function blobTo(to, blob) {
|
||||||
|
if (to === "arrayBuffer" && blob.arrayBuffer) return blob.arrayBuffer();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
var fileReader = new FileReader();
|
||||||
|
fileReader.onload = event => resolve(event.target.result);
|
||||||
|
if (to === "arrayBuffer") fileReader.readAsArrayBuffer(blob);
|
||||||
|
else if (to === "text") fileReader.readAsText(blob, "utf-8");
|
||||||
|
else reject("unknown to");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function GM_fetch(url, opt) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
checkCors(url, opt?.method || "GET")
|
||||||
|
.then(can => {
|
||||||
|
if (can) {
|
||||||
|
// https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest
|
||||||
|
const options = opt || {};
|
||||||
|
options.url = url;
|
||||||
|
options.data = options.body;
|
||||||
|
options.responseType = "blob";
|
||||||
|
options.onload = resp => {
|
||||||
|
var blob = resp.response;
|
||||||
|
resp.blob = () => Promise.resolve(blob);
|
||||||
|
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
|
||||||
|
resp.text = () => blobTo("text", blob);
|
||||||
|
resp.json = async () => JSON.parse(await blobTo("text", blob));
|
||||||
|
resolve(resp);
|
||||||
|
};
|
||||||
|
options.ontimeout = () => reject("fetch timeout");
|
||||||
|
options.onerror = () => reject("fetch error");
|
||||||
|
options.onabort = () => reject("fetch abort");
|
||||||
|
GM_xmlhttpRequest(options);
|
||||||
|
} else {
|
||||||
|
reject("CORS issue");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export const fetch = GM_fetch;
|
@ -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");
|
|
@ -183,7 +183,6 @@ In `index.js`:
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
|
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
|
||||||
require("../app.asar");
|
|
||||||
```
|
```
|
||||||
|
|
||||||
And in `package.json`:
|
And in `package.json`:
|
||||||
|
55
package.json
55
package.json
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.0.1",
|
"version": "1.0.6",
|
||||||
"description": "A Discord client mod that does things differently",
|
"description": "The cutest Discord client mod",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
@ -20,32 +20,33 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build/build.mjs",
|
"build": "node scripts/build/build.mjs",
|
||||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||||
"inject": "node scripts/patcher/install.js",
|
"inject": "node scripts/runInstaller.mjs",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
|
"lint-styles": "stylelint \"src/**/*.css\"",
|
||||||
"lint:fix": "pnpm lint --fix",
|
"lint:fix": "pnpm lint --fix",
|
||||||
"test": "pnpm lint && pnpm build && pnpm testTsc",
|
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc",
|
||||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
"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 +55,30 @@
|
|||||||
"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",
|
||||||
|
"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 +87,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));
|
1149
pnpm-lock.yaml
generated
1149
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");
|
||||||
@ -35,7 +35,7 @@ export const banner = {
|
|||||||
|
|
||||||
// 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 +46,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 +68,12 @@ export const globPlugins = {
|
|||||||
if (!existsSync(`./src/${dir}`)) continue;
|
if (!existsSync(`./src/${dir}`)) continue;
|
||||||
const files = await readdir(`./src/${dir}`);
|
const files = await readdir(`./src/${dir}`);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
if (file.startsWith(".")) continue;
|
||||||
if (file === "index.ts") {
|
if (file === "index.ts") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const mod = `p${i}`;
|
const mod = `p${i}`;
|
||||||
code += `import ${mod} from "./${dir}/${file.replace(/.tsx?$/, "")}";\n`;
|
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
|
||||||
plugins += `[${mod}.name]:${mod},\n`;
|
plugins += `[${mod}.name]:${mod},\n`;
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
@ -87,7 +88,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 +104,7 @@ export const gitHashPlugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
export const gitRemotePlugin = {
|
export const gitRemotePlugin = {
|
||||||
name: "git-remote-plugin",
|
name: "git-remote-plugin",
|
||||||
@ -125,7 +126,7 @@ export const gitRemotePlugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
export const fileIncludePlugin = {
|
export const fileIncludePlugin = {
|
||||||
name: "file-include-plugin",
|
name: "file-include-plugin",
|
||||||
@ -147,6 +148,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 +184,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);
|
||||||
|
26
scripts/build/module/style.js
Normal file
26
scripts/build/module/style.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(window.VencordStyles ??= new Map()).set(STYLE_NAME, {
|
||||||
|
name: STYLE_NAME,
|
||||||
|
source: STYLE_SOURCE,
|
||||||
|
classNames: {},
|
||||||
|
dom: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
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/`;
|
@ -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";
|
||||||
@ -31,9 +30,9 @@ import "./webpack/patchWebpack";
|
|||||||
import { popNotice, showNotice } from "./api/Notices";
|
import { popNotice, showNotice } from "./api/Notices";
|
||||||
import { PlainSettings, Settings } from "./api/settings";
|
import { PlainSettings, Settings } from "./api/settings";
|
||||||
import { 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,37 @@ async function init() {
|
|||||||
if (!IS_WEB) {
|
if (!IS_WEB) {
|
||||||
try {
|
try {
|
||||||
const isOutdated = await checkForUpdates();
|
const isOutdated = await checkForUpdates();
|
||||||
if (isOutdated && Settings.notifyAboutUpdates)
|
if (!isOutdated) return;
|
||||||
|
|
||||||
|
if (Settings.autoUpdate) {
|
||||||
|
await update();
|
||||||
|
const needsFullRestart = await rebuild();
|
||||||
|
setTimeout(() => {
|
||||||
|
showNotice(
|
||||||
|
"Vencord has been updated!",
|
||||||
|
"Restart",
|
||||||
|
() => {
|
||||||
|
if (needsFullRestart)
|
||||||
|
window.DiscordNative.app.relaunch();
|
||||||
|
else
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, 10_000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.notifyAboutUpdates)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showNotice(
|
showNotice(
|
||||||
"A Vencord update is available!",
|
"A Vencord update is available!",
|
||||||
"View Update",
|
"View Update",
|
||||||
() => {
|
() => {
|
||||||
popNotice();
|
popNotice();
|
||||||
Router.open("VencordUpdater");
|
SettingsRouter.open("VencordUpdater");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, 10000);
|
}, 10_000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
UpdateLogger.error("Failed to check for updates", err);
|
UpdateLogger.error("Failed to check for updates", err);
|
||||||
}
|
}
|
||||||
|
@ -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())}`;
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
92
src/api/Notifications/NotificationComponent.tsx
Normal file
92
src/api/Notifications/NotificationComponent.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import { useSettings } from "@api/settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Forms, React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
|
||||||
|
|
||||||
|
import { NotificationData } from "./Notifications";
|
||||||
|
|
||||||
|
export default ErrorBoundary.wrap(function NotificationComponent({
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
richBody,
|
||||||
|
color,
|
||||||
|
icon,
|
||||||
|
onClick,
|
||||||
|
onClose,
|
||||||
|
image
|
||||||
|
}: NotificationData) {
|
||||||
|
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
|
||||||
|
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
|
||||||
|
|
||||||
|
const [isHover, setIsHover] = useState(false);
|
||||||
|
const [elapsed, setElapsed] = useState(0);
|
||||||
|
|
||||||
|
const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isHover || !hasFocus || timeout === 0) return void setElapsed(0);
|
||||||
|
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
if (elapsed >= timeout)
|
||||||
|
onClose!();
|
||||||
|
else
|
||||||
|
setElapsed(elapsed);
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [timeout, isHover, hasFocus]);
|
||||||
|
|
||||||
|
const timeoutProgress = elapsed / timeout;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="vc-notification-root"
|
||||||
|
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
|
||||||
|
onClick={onClick}
|
||||||
|
onContextMenu={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onClose!();
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setIsHover(true)}
|
||||||
|
onMouseLeave={() => setIsHover(false)}
|
||||||
|
>
|
||||||
|
<div className="vc-notification">
|
||||||
|
{icon && <img className="vc-notification-icon" src={icon} alt="" />}
|
||||||
|
<div className="vc-notification-content">
|
||||||
|
<Forms.FormTitle tag="h2">{title}</Forms.FormTitle>
|
||||||
|
<div>
|
||||||
|
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{image && <img className="vc-notification-img" src={image} alt="" />}
|
||||||
|
{timeout !== 0 && (
|
||||||
|
<div
|
||||||
|
className="vc-notification-progressbar"
|
||||||
|
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
99
src/api/Notifications/Notifications.tsx
Normal file
99
src/api/Notifications/Notifications.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Settings } from "@api/settings";
|
||||||
|
import { Queue } from "@utils/Queue";
|
||||||
|
import { ReactDOM } from "@webpack/common";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type { Root } from "react-dom/client";
|
||||||
|
|
||||||
|
import NotificationComponent from "./NotificationComponent";
|
||||||
|
|
||||||
|
const NotificationQueue = new Queue();
|
||||||
|
|
||||||
|
let reactRoot: Root;
|
||||||
|
let id = 42;
|
||||||
|
|
||||||
|
function getRoot() {
|
||||||
|
if (!reactRoot) {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.id = "vc-notification-container";
|
||||||
|
document.body.append(container);
|
||||||
|
reactRoot = ReactDOM.createRoot(container);
|
||||||
|
}
|
||||||
|
return reactRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationData {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
/**
|
||||||
|
* Same as body but can be a custom component.
|
||||||
|
* Will be used over body if present.
|
||||||
|
* Not supported on desktop notifications, those will fall back to body */
|
||||||
|
richBody?: ReactNode;
|
||||||
|
/** Small icon. This is for things like profile pictures and should be square */
|
||||||
|
icon?: string;
|
||||||
|
/** Large image. Optimally, this should be around 16x9 but it doesn't matter much. Desktop Notifications might not support this */
|
||||||
|
image?: string;
|
||||||
|
onClick?(): void;
|
||||||
|
onClose?(): void;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _showNotification(notification: NotificationData, id: number) {
|
||||||
|
const root = getRoot();
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
root.render(
|
||||||
|
<NotificationComponent key={id} {...notification} onClose={() => {
|
||||||
|
notification.onClose?.();
|
||||||
|
root.render(null);
|
||||||
|
resolve();
|
||||||
|
}} />,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldBeNative() {
|
||||||
|
const { useNative } = Settings.notifications;
|
||||||
|
if (useNative === "always") return true;
|
||||||
|
if (useNative === "not-focused") return !document.hasFocus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestPermission() {
|
||||||
|
return (
|
||||||
|
Notification.permission === "granted" ||
|
||||||
|
(Notification.permission !== "denied" && (await Notification.requestPermission()) === "granted")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showNotification(data: NotificationData) {
|
||||||
|
if (shouldBeNative() && await requestPermission()) {
|
||||||
|
const { title, body, icon, image, onClick = null, onClose = null } = data;
|
||||||
|
const n = new Notification(title, {
|
||||||
|
body,
|
||||||
|
icon,
|
||||||
|
image
|
||||||
|
});
|
||||||
|
n.onclick = onClick;
|
||||||
|
n.onclose = onClose;
|
||||||
|
} else {
|
||||||
|
NotificationQueue.push(() => _showNotification(data, id++));
|
||||||
|
}
|
||||||
|
}
|
19
src/api/Notifications/index.ts
Normal file
19
src/api/Notifications/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./Notifications";
|
49
src/api/Notifications/styles.css
Normal file
49
src/api/Notifications/styles.css
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
.vc-notification-root {
|
||||||
|
/* clear default button styles */
|
||||||
|
all: unset;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 25vw;
|
||||||
|
min-height: 10vh;
|
||||||
|
color: var(--text-normal);
|
||||||
|
background-color: var(--background-secondary-alt);
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2147483647;
|
||||||
|
right: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 1.25rem;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-icon {
|
||||||
|
height: 4rem;
|
||||||
|
width: 4rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Discord adding 3km margin to generic tags */
|
||||||
|
.vc-notification h2 {
|
||||||
|
margin: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-progressbar {
|
||||||
|
height: 0.25rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-p {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
line-height: 140%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-notification-img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
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(" ");
|
||||||
|
};
|
@ -19,11 +19,15 @@
|
|||||||
import * as $Badges from "./Badges";
|
import * as $Badges from "./Badges";
|
||||||
import * as $Commands from "./Commands";
|
import * as $Commands from "./Commands";
|
||||||
import * as $DataStore from "./DataStore";
|
import * as $DataStore from "./DataStore";
|
||||||
|
import * as $MemberListDecorators from "./MemberListDecorators";
|
||||||
import * as $MessageAccessories from "./MessageAccessories";
|
import * as $MessageAccessories from "./MessageAccessories";
|
||||||
|
import * as $MessageDecorations from "./MessageDecorations";
|
||||||
import * as $MessageEventsAPI from "./MessageEvents";
|
import * as $MessageEventsAPI from "./MessageEvents";
|
||||||
import * as $MessagePopover from "./MessagePopover";
|
import * as $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 +35,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 +59,37 @@ const Commands = $Commands;
|
|||||||
* This is actually just idb-keyval, so if you're familiar with that, you're golden!
|
* This is actually just idb-keyval, so if you're familiar with that, you're golden!
|
||||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types}
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types}
|
||||||
*/
|
*/
|
||||||
const DataStore = $DataStore;
|
export const DataStore = $DataStore;
|
||||||
/**
|
/**
|
||||||
* An API allowing you to add custom components as message accessories
|
* An API allowing you to add custom components as message accessories
|
||||||
*/
|
*/
|
||||||
const MessageAccessories = $MessageAccessories;
|
export const MessageAccessories = $MessageAccessories;
|
||||||
/**
|
/**
|
||||||
* 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;
|
||||||
|
/**
|
||||||
export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, MessagePopover, Notices, 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;
|
||||||
|
@ -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,43 @@ 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;
|
||||||
useQuickCss: boolean;
|
useQuickCss: boolean;
|
||||||
enableReactDevtools: boolean;
|
enableReactDevtools: boolean;
|
||||||
themeLinks: string[];
|
themeLinks: string[];
|
||||||
|
frameless: boolean;
|
||||||
|
transparent: boolean;
|
||||||
|
winCtrlQ: boolean;
|
||||||
plugins: {
|
plugins: {
|
||||||
[plugin: string]: {
|
[plugin: string]: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
[setting: string]: any;
|
[setting: string]: any;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
notifications: {
|
||||||
|
timeout: number;
|
||||||
|
position: "top-right" | "bottom-right";
|
||||||
|
useNative: "always" | "never" | "not-focused";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultSettings: Settings = {
|
const DefaultSettings: Settings = {
|
||||||
notifyAboutUpdates: true,
|
notifyAboutUpdates: true,
|
||||||
|
autoUpdate: false,
|
||||||
useQuickCss: true,
|
useQuickCss: true,
|
||||||
themeLinks: [],
|
themeLinks: [],
|
||||||
enableReactDevtools: false,
|
enableReactDevtools: false,
|
||||||
plugins: {}
|
frameless: false,
|
||||||
|
transparent: false,
|
||||||
|
winCtrlQ: false,
|
||||||
|
plugins: {},
|
||||||
|
|
||||||
|
notifications: {
|
||||||
|
timeout: 5000,
|
||||||
|
position: "bottom-right",
|
||||||
|
useNative: "not-focused"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -144,6 +164,7 @@ 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
|
||||||
*/
|
*/
|
||||||
|
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
|
||||||
export function useSettings(paths?: string[]) {
|
export function useSettings(paths?: string[]) {
|
||||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||||
|
|
||||||
@ -198,3 +219,19 @@ 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}`)
|
||||||
|
).plugins[definedSettings.pluginName] as any,
|
||||||
|
def,
|
||||||
|
checks: checks ?? {},
|
||||||
|
pluginName: "",
|
||||||
|
};
|
||||||
|
return definedSettings;
|
||||||
|
}
|
||||||
|
29
src/components/Badge.tsx
Normal file
29
src/components/Badge.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function Badge({ text, color }): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="vc-plugins-badge" style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
justifySelf: "flex-end",
|
||||||
|
marginLeft: "auto"
|
||||||
|
}}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -103,7 +103,7 @@ 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?: Props): React.ComponentType<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
import { debounce } from "@utils/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
import { makeCodeblock } from "@utils/misc";
|
import { makeCodeblock } from "@utils/misc";
|
||||||
|
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches";
|
||||||
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, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
@ -41,20 +42,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]);
|
||||||
|
|
||||||
@ -179,9 +189,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}
|
||||||
@ -206,7 +217,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>();
|
||||||
|
|
||||||
|
@ -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,7 +37,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
|
|||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue) {
|
function handleChange(newValue) {
|
||||||
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
||||||
@ -58,7 +58,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
|
|||||||
value={state}
|
value={state}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={option.placeholder ?? "Enter a number"}
|
placeholder={option.placeholder ?? "Enter a number"}
|
||||||
disabled={option.disabled?.() ?? false}
|
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||||
|
@ -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,7 +32,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
|
|||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue) {
|
function handleChange(newValue) {
|
||||||
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
@ -45,7 +45,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,7 +30,7 @@ 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 {
|
||||||
@ -47,7 +47,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,32 @@
|
|||||||
* 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 { 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, Margins, 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 +60,27 @@ function showErrorToast(message: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReloadRequiredCardProps extends React.HTMLProps<HTMLDivElement> {
|
function ReloadRequiredCard({ required }: { required: boolean; }) {
|
||||||
plugins: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function ReloadRequiredCard({ plugins, ...props }: ReloadRequiredCardProps) {
|
|
||||||
if (plugins.length === 0) return null;
|
|
||||||
|
|
||||||
const pluginPrefix = plugins.length === 1 ? "The plugin" : "The following plugins require a reload to apply changes:";
|
|
||||||
const pluginSuffix = plugins.length === 1 ? " requires a reload to apply changes." : ".";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorCard {...props} style={{ padding: "1em", display: "grid", gridTemplateColumns: "1fr auto", gap: 8, ...props.style }}>
|
<Card className={cl("info-card", { "restart-card": required })}>
|
||||||
<span style={{ margin: "auto 0" }}>
|
{required ? (
|
||||||
{pluginPrefix} <code>{plugins.join(", ")}</code>{pluginSuffix}
|
<>
|
||||||
</span>
|
<Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle>
|
||||||
<Button look={Button.Looks.INVERTED} onClick={() => location.reload()}>Reload</Button>
|
<Forms.FormText className={cl("dep-text")}>
|
||||||
</ErrorCard>
|
Restart now to apply new plugins and their settings
|
||||||
|
</Forms.FormText>
|
||||||
|
<Button color={Button.Colors.YELLOW} onClick={() => location.reload()}>
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle tag="h5">Plugin Management</Forms.FormTitle>
|
||||||
|
<Forms.FormText>Press the cog wheel or info icon to get more info on a plugin</Forms.FormText>
|
||||||
|
<Forms.FormText>Plugins with a cog wheel have settings you can modify!</Forms.FormText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,17 +88,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}`]).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 +116,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||||||
return;
|
return;
|
||||||
} else if (restartNeeded) {
|
} else if (restartNeeded) {
|
||||||
// If any dependencies have patches, don't start the plugin yet.
|
// If any dependencies have patches, don't start the plugin yet.
|
||||||
pluginSettings.enabled = true;
|
settings.enabled = true;
|
||||||
onRestartNeeded(plugin.name);
|
onRestartNeeded(plugin.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -118,14 +124,14 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||||||
|
|
||||||
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
|
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
|
||||||
if (plugin.patches) {
|
if (plugin.patches) {
|
||||||
pluginSettings.enabled = !wasEnabled;
|
settings.enabled = !wasEnabled;
|
||||||
onRestartNeeded(plugin.name);
|
onRestartNeeded(plugin.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the plugin is enabled, but hasn't been started, then we can just toggle it off.
|
// If the plugin is enabled, but hasn't been started, then we can just toggle it off.
|
||||||
if (wasEnabled && !plugin.started) {
|
if (wasEnabled && !plugin.started) {
|
||||||
pluginSettings.enabled = !wasEnabled;
|
settings.enabled = !wasEnabled;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,53 +144,38 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginSettings.enabled = !wasEnabled;
|
settings.enabled = !wasEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex style={styles.PluginsGridItem} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
<Flex className={cl("card", { "card-disabled": disabled })} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||||
<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 +216,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.marginTop16}>
|
||||||
|
<ReloadRequiredCard required={changes.hasChanges} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
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.marginBottom20} />
|
||||||
<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}
|
||||||
@ -271,49 +323,17 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||||||
|
|
||||||
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.marginTop20}>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.FormDivider className={Margins.marginTop20} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
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 +346,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;
|
||||||
|
}
|
76
src/components/Switch.tsx
Normal file
76
src/components/Switch.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./Switch.css";
|
||||||
|
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
|
||||||
|
interface SwitchProps {
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SWITCH_ON = "var(--green-360)";
|
||||||
|
const SWITCH_OFF = "var(--primary-400)";
|
||||||
|
const SwitchClasses = findByPropsLazy("slider", "input", "container");
|
||||||
|
|
||||||
|
export function Switch({ checked, onChange, disabled }: SwitchProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={`${SwitchClasses.container} default-colors`} style={{
|
||||||
|
backgroundColor: checked ? SWITCH_ON : SWITCH_OFF,
|
||||||
|
opacity: disabled ? 0.3 : 1
|
||||||
|
}}>
|
||||||
|
<svg
|
||||||
|
className={SwitchClasses.slider + " vc-switch-slider"}
|
||||||
|
viewBox="0 0 28 20"
|
||||||
|
preserveAspectRatio="xMinYMid meet"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
transform: checked ? "translateX(12px)" : "translateX(-3px)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<rect fill="white" x="4" y="0" height="20" width="20" rx="10" />
|
||||||
|
<svg viewBox="0 0 20 20" fill="none">
|
||||||
|
{checked ? (
|
||||||
|
<>
|
||||||
|
<path fill={SWITCH_ON} d="M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z" />
|
||||||
|
<path fill={SWITCH_ON} d="M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<path fill={SWITCH_OFF} d="M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z" />
|
||||||
|
<path fill={SWITCH_OFF} d="M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
disabled={disabled}
|
||||||
|
type="checkbox"
|
||||||
|
className={SwitchClasses.input}
|
||||||
|
tabIndex={0}
|
||||||
|
checked={checked}
|
||||||
|
onChange={e => onChange(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -18,19 +18,14 @@
|
|||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
|
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, Margins, Text } from "@webpack/common";
|
||||||
|
|
||||||
function BackupRestoreTab() {
|
function BackupRestoreTab() {
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection title="Settings Sync">
|
<Forms.FormSection title="Settings Sync" className={Margins.marginTop16}>
|
||||||
<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>
|
||||||
@ -50,7 +45,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
|
||||||
|
@ -57,7 +57,8 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
|||||||
{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 +75,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 +89,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.marginTop8 + " " + Margins.marginBottom8} />
|
||||||
<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>
|
||||||
@ -122,8 +119,8 @@ export default ErrorBoundary.wrap(function () {
|
|||||||
padding: ".5em",
|
padding: ".5em",
|
||||||
border: "1px solid var(--background-modifier-accent)"
|
border: "1px solid var(--background-modifier-accent)"
|
||||||
}}
|
}}
|
||||||
ref={ref}
|
value={themeText}
|
||||||
defaultValue={settings.themeLinks.join("\n")}
|
onChange={e => setThemeText(e.currentTarget.value)}
|
||||||
className={TextAreaProps.textarea}
|
className={TextAreaProps.textarea}
|
||||||
placeholder="Theme Links"
|
placeholder="Theme Links"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
@ -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 { 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";
|
||||||
@ -23,7 +24,7 @@ import { handleComponentFailed } from "@components/handleComponentFailed";
|
|||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
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, Margins, Parser, React, Switch, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
@ -69,14 +70,18 @@ interface CommonProps {
|
|||||||
repoPending: boolean;
|
repoPending: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string, disabled?: boolean; }) {
|
||||||
|
return <Link href={`${repo}/commit/${hash}`} disabled={disabled}>
|
||||||
|
{hash}
|
||||||
|
</Link>;
|
||||||
|
}
|
||||||
|
|
||||||
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
|
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
|
||||||
return (
|
return (
|
||||||
<Card style={{ padding: ".5em" }}>
|
<Card style={{ padding: ".5em" }}>
|
||||||
{updates.map(({ hash, author, message }) => (
|
{updates.map(({ hash, author, message }) => (
|
||||||
<div>
|
<div>
|
||||||
<Link href={`${repo}/commit/${hash}`} disabled={repoPending}>
|
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
|
||||||
<code>{hash}</code>
|
|
||||||
</Link>
|
|
||||||
<span style={{
|
<span style={{
|
||||||
marginLeft: "0.5em",
|
marginLeft: "0.5em",
|
||||||
color: "var(--text-normal)"
|
color: "var(--text-normal)"
|
||||||
@ -179,6 +184,8 @@ function Newer(props: CommonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Updater() {
|
function Updater() {
|
||||||
|
const settings = useSettings(["notifyAboutUpdates", "autoUpdate"]);
|
||||||
|
|
||||||
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -192,16 +199,33 @@ function Updater() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection>
|
<Forms.FormSection className={Margins.marginTop16}>
|
||||||
|
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||||
|
<Switch
|
||||||
|
value={settings.notifyAboutUpdates}
|
||||||
|
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||||
|
note="Shows a toast on startup"
|
||||||
|
disabled={settings.autoUpdate}
|
||||||
|
>
|
||||||
|
Get notified about new updates
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
value={settings.autoUpdate}
|
||||||
|
onChange={(v: boolean) => settings.autoUpdate = v}
|
||||||
|
note="Automatically update Vencord without confirmation prompt"
|
||||||
|
>
|
||||||
|
Automatically update
|
||||||
|
</Switch>
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
|
||||||
|
|
||||||
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
|
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
|
||||||
<Link href={repo}>
|
<Link href={repo}>
|
||||||
{repo.split("/").slice(-2).join("/")}
|
{repo.split("/").slice(-2).join("/")}
|
||||||
</Link>
|
</Link>
|
||||||
)} ({gitHash})</Forms.FormText>
|
)} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText>
|
||||||
|
|
||||||
<Forms.FormDivider />
|
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
||||||
|
|
||||||
|
@ -18,31 +18,73 @@
|
|||||||
|
|
||||||
|
|
||||||
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"
|
||||||
|
},
|
||||||
|
!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 +124,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 +205,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 +216,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,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 "./settingsStyles.css";
|
||||||
|
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { findByCodeLazy } from "@webpack";
|
import { findByCodeLazy } from "@webpack";
|
||||||
import { Forms, Router, Text } from "@webpack/common";
|
import { Forms, SettingsRouter, Text } from "@webpack/common";
|
||||||
|
|
||||||
import cssText from "~fileContent/settingsStyles.css";
|
|
||||||
|
|
||||||
import BackupRestoreTab from "./BackupRestoreTab";
|
import BackupRestoreTab from "./BackupRestoreTab";
|
||||||
import PluginsTab from "./PluginsTab";
|
import PluginsTab from "./PluginsTab";
|
||||||
@ -28,11 +29,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"]');
|
||||||
|
|
||||||
@ -66,15 +63,15 @@ function Settings(props: SettingsProps) {
|
|||||||
<TabBar
|
<TabBar
|
||||||
type={TabBar.Types.TOP}
|
type={TabBar.Types.TOP}
|
||||||
look={TabBar.Looks.BRAND}
|
look={TabBar.Looks.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>;
|
||||||
|
@ -1,23 +1,40 @@
|
|||||||
.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;
|
||||||
|
}
|
||||||
|
8
src/globals.d.ts
vendored
8
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;
|
||||||
};
|
};
|
||||||
@ -54,6 +61,7 @@ declare global {
|
|||||||
push(chunk: any): any;
|
push(chunk: any): any;
|
||||||
pop(): any;
|
pop(): any;
|
||||||
};
|
};
|
||||||
|
_: LoDashStatic;
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,9 +67,18 @@ export async function installExt(id: string) {
|
|||||||
try {
|
try {
|
||||||
await access(extDir, fsConstants.F_OK);
|
await access(extDir, fsConstants.F_OK);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`;
|
const url = id === "fmkadmapgofadopljbjfkapdkoienihi"
|
||||||
const buf = await get(url);
|
// React Devtools v4.25
|
||||||
await extract(crxToZip(buf), extDir);
|
// v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843
|
||||||
|
// Unfortunately, Google does not serve old versions, so this is the only way
|
||||||
|
? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip"
|
||||||
|
: `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`;
|
||||||
|
const buf = await get(url, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Vencord (https://github.com/Vendicated/Vencord)"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await extract(crxToZip(buf), extDir).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
session.defaultSession.loadExtension(extDir);
|
session.defaultSession.loadExtension(extDir);
|
||||||
|
@ -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,48 @@ 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;
|
||||||
|
}
|
||||||
|
if (settings.transparent) {
|
||||||
|
options.transparent = true;
|
||||||
|
options.backgroundColor = "#00000000";
|
||||||
|
}
|
||||||
|
|
||||||
process.env.DISCORD_PRELOAD = original;
|
process.env.DISCORD_PRELOAD = original;
|
||||||
|
|
||||||
@ -100,8 +131,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 +190,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: "\"github.com\":new RegExp(\"\\\\/releases\\\\S*\\\\/download\"),",
|
||||||
|
replacement: {
|
||||||
|
match: /const o=JSON.parse\('\[.+?'\)/,
|
||||||
|
replace: "const o=[]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
@ -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)),",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -66,11 +66,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}},`
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -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||($&)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
@ -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}`,
|
||||||
},
|
},
|
||||||
|
@ -20,16 +20,16 @@ import { Devs } from "@utils/constants";
|
|||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "NoReplyMention",
|
name: "MessageDecorationsAPI",
|
||||||
description: "Disables reply pings by default",
|
description: "API to add decorations to messages",
|
||||||
authors: [Devs.DustyAngel47],
|
authors: [Devs.TheSun],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "CREATE_PENDING_REPLY:function",
|
find: ".withMentionPrefix",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /CREATE_PENDING_REPLY:function\((.{1,2})\){/,
|
match: /(.roleDot.{10,50}{children:.{1,2})}\)/,
|
||||||
replace: "CREATE_PENDING_REPLY:function($1){$1.shouldMention=false;"
|
replace: "$1.concat(Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0]))})"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
});
|
});
|
@ -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,20}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/,
|
||||||
|
replace: (m, bools, makeElement) => {
|
||||||
|
const msg = m.match(/message:(.{1,3}),/)?.[1];
|
||||||
|
if (!msg) throw new Error("Could not find message variable");
|
||||||
|
return `...(${bools}?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,16 @@ 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: /;.{1,2}=null;.{0,70}getPremiumSubscription/g,
|
||||||
replace:
|
replace:
|
||||||
";if(Vencord.Api.Notices.currentNotice)return !1;$1"
|
";if(Vencord.Api.Notices.currentNotice)return false$&"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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: 'if($1?.id=="VencordNotice")return($1=null,Vencord.Api.Notices.nextNotice(),true);'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ const assetManager = mapMangledModuleLazy(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const rpcManager = findByCodeLazy(".APPLICATION_RPC(");
|
const lookupRpcApp = findByCodeLazy(".APPLICATION_RPC(");
|
||||||
|
|
||||||
async function lookupAsset(applicationId: string, key: string): Promise<string> {
|
async function lookupAsset(applicationId: string, key: string): Promise<string> {
|
||||||
return (await assetManager.getAsset(applicationId, [key, undefined]))[0];
|
return (await assetManager.getAsset(applicationId, [key, undefined]))[0];
|
||||||
@ -39,7 +39,7 @@ async function lookupAsset(applicationId: string, key: string): Promise<string>
|
|||||||
const apps: any = {};
|
const apps: any = {};
|
||||||
async function lookupApp(applicationId: string): Promise<string> {
|
async function lookupApp(applicationId: string): Promise<string> {
|
||||||
const socket: any = {};
|
const socket: any = {};
|
||||||
await rpcManager.lookupApp(socket, applicationId);
|
await lookupRpcApp(socket, applicationId);
|
||||||
return socket.application;
|
return socket.application;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ export default definePlugin({
|
|||||||
replacement: {
|
replacement: {
|
||||||
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
|
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
|
||||||
replace:
|
replace:
|
||||||
"Vencord.Plugins.plugins.BetterGifAltText.altify(e);$1",
|
"$self.altify(e);$1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -39,7 +39,7 @@ export default definePlugin({
|
|||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/,
|
match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/,
|
||||||
replace:
|
replace:
|
||||||
"?($1.alt='GIF',Vencord.Plugins.plugins.BetterGifAltText.altify($1))",
|
"?($1.alt='GIF',$self.altify($1))",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -33,7 +33,7 @@ export default definePlugin({
|
|||||||
find: "M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V16C20 18.2091 18.2091 20 16 20H4C1.79086 20 0 18.2091 0 16V4Z",
|
find: "M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V16C20 18.2091 18.2091 20 16 20H4C1.79086 20 0 18.2091 0 16V4Z",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /viewBox:"0 0 20 20"/,
|
match: /viewBox:"0 0 20 20"/,
|
||||||
replace: "$&,onClick:()=>Vencord.Plugins.plugins.BetterRoleDot.copyToClipBoard(e.color),style:{cursor:'pointer'}",
|
replace: "$&,onClick:()=>$self.copyToClipBoard(e.color),style:{cursor:'pointer'}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -41,7 +41,7 @@ export default definePlugin({
|
|||||||
all: true,
|
all: true,
|
||||||
predicate: () => Settings.plugins.BetterRoleDot.bothStyles,
|
predicate: () => Settings.plugins.BetterRoleDot.bothStyles,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /"(?:username|dot)"===\w\b/g,
|
match: /"(?:username|dot)"===\w(?!\.\w)/g,
|
||||||
replace: "true",
|
replace: "true",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -43,12 +43,12 @@ export default definePlugin({
|
|||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "().embedWrapper,embed",
|
find: ".embedWrapper,embed",
|
||||||
replacement: [{
|
replacement: [{
|
||||||
match: /(\.renderEmbed=.+?(.)=.\.props)(.+?\(\)\.embedWrapper)/g,
|
match: /(\.renderEmbed=.+?(.)=.\.props)(.+?\.embedWrapper)/g,
|
||||||
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
||||||
}, {
|
}, {
|
||||||
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\(\)\.embedWrapper)/g,
|
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g,
|
||||||
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
@ -74,8 +74,8 @@ export default definePlugin({
|
|||||||
patches: [{
|
patches: [{
|
||||||
find: ".renderConnectionStatus=",
|
find: ".renderConnectionStatus=",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=renderConnectionStatus=.+\(\)\.channel,children:)\w/,
|
match: /(?<=renderConnectionStatus=.+\.channel,children:)\w/,
|
||||||
replace: "[$&, Vencord.Plugins.plugins.CallTimer.renderTimer(this.props.channel.id)]"
|
replace: "[$&, $self.renderTimer(this.props.channel.id)]"
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
renderTimer(channelId: string) {
|
renderTimer(channelId: string) {
|
||||||
|
37
src/plugins/colorSighted.ts
Normal file
37
src/plugins/colorSighted.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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: "ColorSighted",
|
||||||
|
description: "Removes the colorblind-friendly icons from statuses, just like 2015-2017 Discord",
|
||||||
|
authors: [Devs.lewisakura],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "Masks.STATUS_ONLINE",
|
||||||
|
replacement: {
|
||||||
|
// we can use global replacement here - these are specific to the status icons and are used nowhere else,
|
||||||
|
// so it keeps the patch and plugin small and simple
|
||||||
|
match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g,
|
||||||
|
replace: "Masks.STATUS_ONLINE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
@ -99,7 +99,7 @@ export default definePlugin({
|
|||||||
const newName = video.name.replace(/\.mp4$/i, ".corrupt.mp4");
|
const newName = video.name.replace(/\.mp4$/i, ".corrupt.mp4");
|
||||||
const promptToUpload = findByCode("UPLOAD_FILE_LIMIT_ERROR");
|
const promptToUpload = findByCode("UPLOAD_FILE_LIMIT_ERROR");
|
||||||
const file = new File([buf], newName, { type: "video/mp4" });
|
const file = new File([buf], newName, { type: "video/mp4" });
|
||||||
setImmediate(() => promptToUpload([file], ctx.channel, DRAFT_TYPE));
|
setTimeout(() => promptToUpload([file], ctx.channel, DRAFT_TYPE), 10);
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
251
src/plugins/customRPC.tsx
Normal file
251
src/plugins/customRPC.tsx
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
/*
|
||||||
|
* 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 { definePluginSettings } from "@api/settings";
|
||||||
|
import { Link } from "@components/Link";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { useAwaiter } from "@utils/misc";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
|
||||||
|
import {
|
||||||
|
FluxDispatcher,
|
||||||
|
Forms,
|
||||||
|
GuildStore,
|
||||||
|
React,
|
||||||
|
SelectedChannelStore,
|
||||||
|
SelectedGuildStore,
|
||||||
|
UserStore
|
||||||
|
} from "@webpack/common";
|
||||||
|
|
||||||
|
const ActivityComponent = findByCodeLazy("onOpenGameProfile");
|
||||||
|
const ActivityClassName = findByPropsLazy("activity", "buttonColor");
|
||||||
|
const Colors = findByPropsLazy("profileColors");
|
||||||
|
|
||||||
|
// START yoinked from lastfm.tsx
|
||||||
|
const assetManager = mapMangledModuleLazy(
|
||||||
|
"getAssetImage: size must === [number, number] for Twitch",
|
||||||
|
{
|
||||||
|
getAsset: filters.byCode("apply("),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function getApplicationAsset(key: string): Promise<string> {
|
||||||
|
return (await assetManager.getAsset(settings.store.appID, [key, undefined]))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivityAssets {
|
||||||
|
large_image?: string;
|
||||||
|
large_text?: string;
|
||||||
|
small_image?: string;
|
||||||
|
small_text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Activity {
|
||||||
|
state: string;
|
||||||
|
details?: string;
|
||||||
|
timestamps?: {
|
||||||
|
start?: Number;
|
||||||
|
end?: Number;
|
||||||
|
};
|
||||||
|
assets?: ActivityAssets;
|
||||||
|
buttons?: Array<string>;
|
||||||
|
name: string;
|
||||||
|
application_id: string;
|
||||||
|
metadata?: {
|
||||||
|
button_urls?: Array<string>;
|
||||||
|
};
|
||||||
|
type: ActivityType;
|
||||||
|
flags: Number;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActivityType {
|
||||||
|
PLAYING = 0,
|
||||||
|
LISTENING = 2,
|
||||||
|
WATCHING = 3,
|
||||||
|
COMPETING = 5
|
||||||
|
}
|
||||||
|
// END
|
||||||
|
|
||||||
|
const strOpt = (description: string) => ({
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description,
|
||||||
|
onChange: setRpc
|
||||||
|
}) as const;
|
||||||
|
|
||||||
|
const numOpt = (description: string) => ({
|
||||||
|
type: OptionType.NUMBER,
|
||||||
|
description,
|
||||||
|
onChange: setRpc
|
||||||
|
}) as const;
|
||||||
|
|
||||||
|
const choice = (label: string, value: any, _default?: Boolean) => ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
default: _default
|
||||||
|
}) as const;
|
||||||
|
|
||||||
|
const choiceOpt = (description: string, options) => ({
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description,
|
||||||
|
onChange: setRpc,
|
||||||
|
options
|
||||||
|
}) as const;
|
||||||
|
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
appID: strOpt("The ID of the application for the rich presence."),
|
||||||
|
appName: strOpt("The name of the presence."),
|
||||||
|
details: strOpt("Line 1 of rich presence."),
|
||||||
|
state: strOpt("Line 2 of rich presence."),
|
||||||
|
type: choiceOpt("Type of presence", [
|
||||||
|
choice("Playing", ActivityType.PLAYING, true),
|
||||||
|
choice("Listening", ActivityType.LISTENING),
|
||||||
|
choice("Watching", ActivityType.WATCHING),
|
||||||
|
choice("Competing", ActivityType.COMPETING)
|
||||||
|
]),
|
||||||
|
startTime: numOpt("Unix Timestamp for beginning of activity."),
|
||||||
|
endTime: numOpt("Unix Timestamp for end of activity."),
|
||||||
|
imageBig: strOpt("Sets the big image to the specified image."),
|
||||||
|
imageBigTooltip: strOpt("Sets the tooltip text for the big image."),
|
||||||
|
imageSmall: strOpt("Sets the small image to the specified image."),
|
||||||
|
imageSmallTooltip: strOpt("Sets the tooltip text for the small image."),
|
||||||
|
buttonOneText: strOpt("The text for the first button"),
|
||||||
|
buttonOneURL: strOpt("The URL for the first button"),
|
||||||
|
buttonTwoText: strOpt("The text for the second button"),
|
||||||
|
buttonTwoURL: strOpt("The URL for the second button")
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createActivity(): Promise<Activity | undefined> {
|
||||||
|
const {
|
||||||
|
appID,
|
||||||
|
appName,
|
||||||
|
details,
|
||||||
|
state,
|
||||||
|
type,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
imageBig,
|
||||||
|
imageBigTooltip,
|
||||||
|
imageSmall,
|
||||||
|
imageSmallTooltip,
|
||||||
|
buttonOneText,
|
||||||
|
buttonOneURL,
|
||||||
|
buttonTwoText,
|
||||||
|
buttonTwoURL
|
||||||
|
} = settings.store;
|
||||||
|
|
||||||
|
if (!appName) return;
|
||||||
|
|
||||||
|
const activity: Activity = {
|
||||||
|
application_id: appID || "0",
|
||||||
|
name: appName,
|
||||||
|
state,
|
||||||
|
details,
|
||||||
|
type,
|
||||||
|
flags: 1 << 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (startTime) {
|
||||||
|
activity.timestamps = {
|
||||||
|
start: startTime,
|
||||||
|
};
|
||||||
|
if (endTime) {
|
||||||
|
activity.timestamps.end = endTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttonOneText) {
|
||||||
|
activity.buttons = [
|
||||||
|
buttonOneText,
|
||||||
|
buttonTwoText
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
activity.metadata = {
|
||||||
|
button_urls: [
|
||||||
|
buttonOneURL,
|
||||||
|
buttonTwoURL
|
||||||
|
].filter(Boolean)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageBig) {
|
||||||
|
activity.assets = {
|
||||||
|
large_image: await getApplicationAsset(imageBig),
|
||||||
|
large_text: imageBigTooltip
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageSmall) {
|
||||||
|
activity.assets = {
|
||||||
|
...activity.assets,
|
||||||
|
small_image: await getApplicationAsset(imageSmall),
|
||||||
|
small_text: imageSmallTooltip
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (const k in activity) {
|
||||||
|
if (k === "type") continue; // without type, the presence is considered invalid.
|
||||||
|
const v = activity[k];
|
||||||
|
if (!v || v.length === 0)
|
||||||
|
delete activity[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
// WHAT DO YOU WANT FROM ME
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
return activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setRpc(disable?: Boolean) {
|
||||||
|
const activity: Activity | undefined = await createActivity();
|
||||||
|
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "LOCAL_ACTIVITY_UPDATE",
|
||||||
|
activity: !disable ? activity : {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "CustomRPC",
|
||||||
|
description: "Allows you to set a custom rich presence.",
|
||||||
|
authors: [Devs.captain],
|
||||||
|
start: setRpc,
|
||||||
|
stop: () => setRpc(true),
|
||||||
|
settings,
|
||||||
|
|
||||||
|
settingsAboutComponent: () => {
|
||||||
|
const activity = useAwaiter(createActivity);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle tag="h2">NOTE:</Forms.FormTitle>
|
||||||
|
<Forms.FormText>
|
||||||
|
You will need to <Link href="https://discord.com/developers/applications">create an
|
||||||
|
application</Link> and
|
||||||
|
get its ID to use this plugin.
|
||||||
|
</Forms.FormText>
|
||||||
|
<Forms.FormDivider />
|
||||||
|
<div style={{ width: "284px" }} className={Colors.profileColors}>
|
||||||
|
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
|
||||||
|
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
|
||||||
|
application={{ id: settings.store.appID }}
|
||||||
|
user={UserStore.getCurrentUser()} />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
35
src/plugins/disableDMCallIdle.ts
Normal file
35
src/plugins/disableDMCallIdle.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: "DisableDMCallIdle",
|
||||||
|
description: "Disables automatically getting kicked from a DM voice call after 5 minutes.",
|
||||||
|
authors: [Devs.Nuckyz],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
|
||||||
|
replacement: {
|
||||||
|
match: /function (?<functionName>.{1,3})\(\){.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT.+?}}/,
|
||||||
|
replace: "function $<functionName>(){}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -50,14 +50,14 @@ function getGuildCandidates(isAnimated: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function doClone(guildId: string, id: string, name: string, isAnimated: boolean) {
|
async function doClone(guildId: string, id: string, name: string, isAnimated: boolean) {
|
||||||
const data = await fetch(`https://cdn.discordapp.com/emojis/${id}.${isAnimated ? "gif" : "png"}`)
|
const data = await fetch(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${id}.${isAnimated ? "gif" : "png"}`)
|
||||||
.then(r => r.blob());
|
.then(r => r.blob());
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
uploadEmoji({
|
uploadEmoji({
|
||||||
guildId,
|
guildId,
|
||||||
name,
|
name: name.split("~")[0],
|
||||||
image: reader.result
|
image: reader.result
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
Toasts.show({
|
Toasts.show({
|
||||||
@ -187,7 +187,7 @@ export default definePlugin({
|
|||||||
find: "open-native-link",
|
find: "open-native-link",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
|
match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
|
||||||
replace: "$&,Vencord.Plugins.plugins.EmoteCloner.makeMenu(arguments[2])"
|
replace: "$&,$self.makeMenu(arguments[2])"
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -226,7 +226,7 @@ export default definePlugin({
|
|||||||
<img
|
<img
|
||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
src={`https://cdn.discordapp.com/emojis/${id}.${isAnimated ? "gif" : "png"}`}
|
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${id}.${isAnimated ? "gif" : "png"}`}
|
||||||
alt=""
|
alt=""
|
||||||
height={24}
|
height={24}
|
||||||
width={24}
|
width={24}
|
||||||
|
@ -42,7 +42,7 @@ export default definePlugin({
|
|||||||
}, {
|
}, {
|
||||||
find: 'type:"user",revision',
|
find: 'type:"user",revision',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(\w)\|\|"CONNECTION_OPEN".+?;/g,
|
match: /!(\w{1,3})&&"CONNECTION_OPEN".+?;/g,
|
||||||
replace: "$1=!0;"
|
replace: "$1=!0;"
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user