Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0bfe6f069e |
118
.eslintrc.json
118
.eslintrc.json
@ -1,118 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"ignorePatterns": ["dist", "browser"],
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"header",
|
||||
"simple-import-sort",
|
||||
"unused-imports",
|
||||
"path-alias"
|
||||
],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"alias": {
|
||||
"map": [
|
||||
["@webpack", "./src/webpack"],
|
||||
["@webpack/common", "./src/webpack/common"],
|
||||
["@utils", "./src/utils"],
|
||||
["@api", "./src/api"],
|
||||
["@components", "./src/components"]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
// Since it's only been a month and Vencord has already been stolen
|
||||
// by random skids who rebranded it to "AlphaCord" and erased all license
|
||||
// information
|
||||
"header/header": [
|
||||
2,
|
||||
"block",
|
||||
[
|
||||
{
|
||||
"pattern": "!?",
|
||||
"template": " "
|
||||
},
|
||||
" * Vencord, a modification for Discord's desktop app",
|
||||
{
|
||||
"pattern": " \\* Copyright \\(c\\) \\d{4}",
|
||||
"template": " * 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/>.",
|
||||
""
|
||||
],
|
||||
2
|
||||
],
|
||||
"quotes": ["error", "double", { "avoidEscape": true }],
|
||||
"jsx-quotes": ["error", "prefer-double"],
|
||||
"no-mixed-spaces-and-tabs": "error",
|
||||
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||
"arrow-parens": ["error", "as-needed"],
|
||||
"eol-last": ["error", "always"],
|
||||
"func-call-spacing": ["error", "never"],
|
||||
"no-multi-spaces": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-whitespace-before-property": "error",
|
||||
"semi": ["error", "always"],
|
||||
"semi-style": ["error", "last"],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"block-spacing": ["error", "always"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"eqeqeq": ["error", "always", { "null": "ignore" }],
|
||||
"spaced-comment": ["error", "always", { "markers": ["!"] }],
|
||||
"yoda": "error",
|
||||
"prefer-destructuring": ["error", { "object": true, "array": false }],
|
||||
"operator-assignment": ["error", "always"],
|
||||
"no-useless-computed-key": "error",
|
||||
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
|
||||
"no-invalid-regexp": "error",
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"no-duplicate-imports": "error",
|
||||
"no-extra-semi": "error",
|
||||
"dot-notation": "error",
|
||||
"no-useless-escape": [
|
||||
"error",
|
||||
{
|
||||
"extra": "i"
|
||||
}
|
||||
],
|
||||
"no-fallthrough": "error",
|
||||
"for-direction": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-dupe-else-if": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-loss-of-precision": "error",
|
||||
"no-misleading-character-class": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-unexpected-multiline": "error",
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-useless-backreference": "error",
|
||||
"use-isnan": "error",
|
||||
"prefer-const": "error",
|
||||
"prefer-spread": "error",
|
||||
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
|
||||
"path-alias/no-relative": "error"
|
||||
}
|
||||
}
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1 +0,0 @@
|
||||
* text=auto eol=lf
|
22
.github/ISSUE_TEMPLATE/blank.yml
vendored
22
.github/ISSUE_TEMPLATE/blank.yml
vendored
@ -1,22 +0,0 @@
|
||||
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
66
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,66 +0,0 @@
|
||||
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
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +0,0 @@
|
||||
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
32
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -1,32 +0,0 @@
|
||||
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
|
75
.github/workflows/build.yml
vendored
75
.github/workflows/build.yml
vendored
@ -1,75 +0,0 @@
|
||||
name: Build DevBuild
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- .github/workflows/build.yml
|
||||
- src/**
|
||||
- browser/**
|
||||
- scripts/build/**
|
||||
- package.json
|
||||
- pnpm-lock.yaml
|
||||
env:
|
||||
FORCE_COLOR: true
|
||||
|
||||
jobs:
|
||||
Build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 19
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 19
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build web
|
||||
run: pnpm buildWeb --standalone
|
||||
|
||||
- name: Build
|
||||
run: pnpm build --standalone
|
||||
|
||||
- name: Clean up obsolete files
|
||||
run: |
|
||||
rm -rf dist/extension* Vencord.user.css
|
||||
|
||||
- name: Get some values needed for the release
|
||||
id: release_values
|
||||
run: |
|
||||
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload DevBuild as release
|
||||
run: |
|
||||
gh release upload devbuild --clobber dist/*
|
||||
gh release edit devbuild --title "DevBuild $RELEASE_TAG"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
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
61
.github/workflows/publish.yml
vendored
@ -1,61 +0,0 @@
|
||||
name: Release Browser Extension
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
Publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: check that tag matches package.json version
|
||||
run: |
|
||||
pkg_version="v$(jq -r .version < package.json)"
|
||||
if [[ "${{ github.ref_name }}" != "$pkg_version" ]]; then
|
||||
echo "Tag ${{ github.ref_name }} does not match package.json version $pkg_version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 19
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 19
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build web
|
||||
run: pnpm buildWeb --standalone
|
||||
|
||||
- name: Publish extension
|
||||
run: |
|
||||
cd dist/extension-unpacked
|
||||
|
||||
# Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later
|
||||
EXIT_CODE=0
|
||||
|
||||
# Chrome
|
||||
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$?
|
||||
|
||||
# Firefox
|
||||
npm i -g web-ext@7.4.0 web-ext-submit@7.4.0
|
||||
web-ext-submit || EXIT_CODE=$?
|
||||
|
||||
exit $EXIT_CODE
|
||||
env:
|
||||
# Chrome
|
||||
EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
|
||||
CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
|
||||
CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||
REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||
|
||||
# Firefox
|
||||
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
|
||||
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}
|
||||
|
57
.github/workflows/reportBrokenPlugins.yml
vendored
57
.github/workflows/reportBrokenPlugins.yml
vendored
@ -1,57 +0,0 @@
|
||||
name: Test Patches
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Every day at midnight
|
||||
- cron: 0 0 * * *
|
||||
|
||||
jobs:
|
||||
TestPlugins:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 19
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 19
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm add puppeteer
|
||||
|
||||
sudo apt-get install -y chromium-browser
|
||||
|
||||
- name: Build web
|
||||
run: pnpm buildWeb --standalone
|
||||
|
||||
- name: Create Report
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||
export CHROMIUM_BIN=$(which chromium-browser)
|
||||
|
||||
esbuild test/generateReport.ts > dist/report.mjs
|
||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||
env:
|
||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
- name: Create Report (Canary)
|
||||
timeout-minutes: 10
|
||||
if: success() || failure() # even run if previous one failed
|
||||
run: |
|
||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||
export CHROMIUM_BIN=$(which chromium-browser)
|
||||
export USE_CANARY=true
|
||||
|
||||
esbuild test/generateReport.ts > dist/report.mjs
|
||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||
env:
|
||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
30
.github/workflows/test.yml
vendored
30
.github/workflows/test.yml
vendored
@ -1,30 +0,0 @@
|
||||
name: test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint & Test if desktop version compiles
|
||||
run: pnpm test
|
||||
|
||||
- name: Lint & Test if web version compiles
|
||||
run: pnpm testWeb
|
22
.gitignore
vendored
22
.gitignore
vendored
@ -1,24 +1,2 @@
|
||||
dist
|
||||
node_modules
|
||||
|
||||
*.exe
|
||||
vencord_installer
|
||||
|
||||
.idea
|
||||
.DS_Store
|
||||
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
*.tsbuildinfo
|
||||
|
||||
src/userplugins
|
||||
|
||||
ExtensionCache/
|
||||
settings/
|
||||
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"indentation": 4
|
||||
}
|
||||
}
|
10
.vscode/extensions.json
vendored
10
.vscode/extensions.json
vendored
@ -1,11 +1,3 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eamodio.gitlens",
|
||||
"EditorConfig.EditorConfig",
|
||||
"ExodiusStudios.comment-anchors",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"GregorBiswanger.json2ts",
|
||||
"stylelint.vscode-stylelint"
|
||||
]
|
||||
"recommendations": [ "EditorConfig.EditorConfig" ]
|
||||
}
|
||||
|
37
.vscode/launch.json
vendored
37
.vscode/launch.json
vendored
@ -1,37 +0,0 @@
|
||||
{
|
||||
// this allows you to debug Vencord from VSCode.
|
||||
// How to use:
|
||||
// You need to run Discord via the command line to pass some flags to it.
|
||||
// If you want to debug the main (node.js) process (preload.ts, ipcMain/*, patcher.ts),
|
||||
// add the --inspect flag
|
||||
// To debug the renderer (99% of Vencord), add the --remote-debugging-port=9223 flag
|
||||
//
|
||||
// Now launch the desired configuration in VSCode and start Discord with the flags.
|
||||
// For example, to debug both process, run Electron: All then launch Discord with
|
||||
// discord --remote-debugging-port=9223 --inspect
|
||||
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Electron: Main",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"port": 9229,
|
||||
"timeout": 30000
|
||||
},
|
||||
{
|
||||
"name": "Electron: Renderer",
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"port": 9223,
|
||||
"timeout": 30000,
|
||||
"webRoot": "${workspaceFolder}/src"
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Electron: All",
|
||||
"configurations": ["Electron: Main", "Electron: Renderer"]
|
||||
}
|
||||
]
|
||||
}
|
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@ -1,16 +0,0 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"javascript.format.semicolons": "insert",
|
||||
"typescript.format.semicolons": "insert",
|
||||
"typescript.preferences.quoteStyle": "double",
|
||||
"javascript.preferences.quoteStyle": "double"
|
||||
}
|
@ -4,8 +4,6 @@ First of all, thank you for contributing! :3
|
||||
|
||||
To ensure your contribution is robust, please follow the below guide!
|
||||
|
||||
For a friendly introduction to plugins, see [Megu's Plugin Guide!](docs/2_PLUGINS.md)
|
||||
|
||||
## Style Guide
|
||||
|
||||
- This project has a very minimal .editorconfig. Make sure your editor supports this!
|
||||
@ -15,6 +13,7 @@ For a friendly introduction to plugins, see [Megu's Plugin Guide!](docs/2_PLUGIN
|
||||
- Follow the file naming convention. File names should usually be camelCase, unless they export a Class
|
||||
or React Component, in which case they should be PascalCase
|
||||
|
||||
|
||||
## Contributing a Plugin
|
||||
|
||||
Because plugins modify code directly, incompatibilities are a problem.
|
||||
@ -24,6 +23,7 @@ This way we can ensure compatibility and high quality patches.
|
||||
|
||||
Follow the below guide to make your first plugin!
|
||||
|
||||
|
||||
### Finding the right module to patch
|
||||
|
||||
If the thing you want to patch is an action performed when interacting with a part of the UI, use React DevTools.
|
||||
@ -50,14 +50,12 @@ This is the regex that will operate on the module found with "find". Just like i
|
||||
this only matches exactly the part you want to patch and no other parts in the file.
|
||||
|
||||
The easiest way to write and test your regex is the following:
|
||||
|
||||
- Get the ID of the module you want to patch. To do this, go to it in the sources tab and scroll up until you
|
||||
see something like `447887: (e,t,n)=>{` (Obviously the number will differ).
|
||||
- Now paste the following into the console: `Vencord.Webpack.wreq.m[447887].toString()` (Changing the number to your ID)
|
||||
- Now either test regexes on this string in the console or use a tool like https://regex101.com
|
||||
|
||||
Also pay attention to the following:
|
||||
|
||||
- Never hardcode variable or parameter names or any other minified names. They will change in the future. The only Exception to this rule
|
||||
are the react props parameter which seems to always be `e`, but even then only rely on this if it is necessary.
|
||||
Instead, use one of the following approaches where applicable:
|
||||
@ -77,6 +75,6 @@ and use those in your replacement
|
||||
Make sure your replacement does not introduce any whitespace. While this might seem weird, random whitespace may mess up other patches.
|
||||
This includes spaces, tabs and especially newlines
|
||||
|
||||
---
|
||||
___
|
||||
|
||||
And that's it! Now open a Pull Request with your Plugin
|
||||
|
41
README.md
41
README.md
@ -1,41 +1,34 @@
|
||||
# Vencord
|
||||
|
||||
The cutest Discord client mod
|
||||
My own Discord Desktop mod :)
|
||||
|
||||
## Features
|
||||
|
||||
- Super easy to install (one click installer)
|
||||
- 90+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
||||
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
|
||||
- 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)
|
||||
- Works in all Electron versions (Confirmed working on versions 13-23)
|
||||
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
||||
- Works on Discord's latest swc update that breaks all other mods
|
||||
- Proper context isolation -> Works in newer Electron versions (Confirmed working on versions 13-21)
|
||||
- Inline patches: Patch Discord's code with regex replacements! See [the experiments plugin](src/plugins/experiments.ts) for an example. While being more complex, this is more powerful than monkey patching since you can patch only small parts of functions instead of fully replacing them, access non exported/local variables and even replace constants (like in the aforementioned experiments patch!)
|
||||
- Experiments
|
||||
- Custom Css: Manually edit `%appdata%/Vencord/settings/quickCss.css` / `~/.config/Vencord/settings/quickCss.css` with your favourite editor and the client will automatically apply your changes
|
||||
- Many Useful™ plugins - [List](https://github.com/Vendicated/Vencord/tree/main/src/plugins)
|
||||
|
||||
## Installing / Uninstalling
|
||||
## Installing
|
||||
|
||||
[![Download and run the Installer ](https://img.shields.io/github/v/release/Vencord/Installer?label=Download%20Vencord%20Installer&style=for-the-badge)](https://github.com/Vencord/Installer#usage)
|
||||
|
||||
## Installing on Browser
|
||||
|
||||
[![Get it on the Firefox Webstore](https://blog.mozilla.org/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [![Get it on the Chrome Webstore](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
|
||||
|
||||
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that QuickCSS and plugins making use of external resources will not work with the UserScript.
|
||||
|
||||
## Building from Source
|
||||
|
||||
See the docs folder
|
||||
Make sure you have NodeJs and git installed. I will be using pnpm, you can use npm instead
|
||||
```sh
|
||||
git clone https://github.com/Vendicated/Vencord
|
||||
cd Vencord
|
||||
pnpm i
|
||||
pnpm build
|
||||
```
|
||||
The builds are now in the dist/ folder (Vencord/dist).
|
||||
Now install with either the powershell/bash script or use [X1nto's installer](https://github.com/X1nto/VencordInstaller/releases/latest)
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) and [Megu's Plugin Guide!](docs/2_PLUGINS.md)
|
||||
|
||||
[contribute]: CONTRIBUTING.md
|
||||
|
||||
[contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute]
|
||||
|
||||
## Join
|
||||
|
||||
[join]: https://discord.gg/D9uwnFnqmd
|
||||
|
||||
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
|
||||
|
@ -1,107 +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/>.
|
||||
*/
|
||||
|
||||
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,21 +0,0 @@
|
||||
/*!
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./VencordNativeStub";
|
||||
|
||||
export * from "../src/Vencord";
|
@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as DataStore from "../src/api/DataStore";
|
||||
import IpcEvents from "../src/utils/IpcEvents";
|
||||
|
||||
// Discord deletes this so need to store in variable
|
||||
const { localStorage } = window;
|
||||
|
||||
// listeners for ipc.on
|
||||
const listeners = {} as Record<string, Set<Function>>;
|
||||
|
||||
const handlers = {
|
||||
[IpcEvents.GET_REPO]: () => "https://github.com/Vendicated/Vencord", // shrug
|
||||
[IpcEvents.GET_SETTINGS_DIR]: () => "LocalStorage",
|
||||
|
||||
[IpcEvents.GET_QUICK_CSS]: () => DataStore.get("VencordQuickCss").then(s => s ?? ""),
|
||||
[IpcEvents.SET_QUICK_CSS]: (css: string) => {
|
||||
DataStore.set("VencordQuickCss", css);
|
||||
listeners[IpcEvents.QUICK_CSS_UPDATE]?.forEach(l => l(null, css));
|
||||
},
|
||||
|
||||
[IpcEvents.GET_SETTINGS]: () => localStorage.getItem("VencordSettings") || "{}",
|
||||
[IpcEvents.SET_SETTINGS]: (s: string) => localStorage.setItem("VencordSettings", s),
|
||||
|
||||
[IpcEvents.GET_UPDATES]: () => ({ ok: true, value: [] }),
|
||||
|
||||
[IpcEvents.OPEN_EXTERNAL]: (url: string) => open(url, "_blank"),
|
||||
};
|
||||
|
||||
function onEvent(event: string, ...args: any[]) {
|
||||
const handler = handlers[event];
|
||||
if (!handler) throw new Error(`Event ${event} not implemented.`);
|
||||
return handler(...args);
|
||||
}
|
||||
|
||||
// probably should make this less cursed at some point
|
||||
window.VencordNative = {
|
||||
getVersions: () => ({}),
|
||||
ipc: {
|
||||
send: (event: string, ...args: any[]) => void onEvent(event, ...args),
|
||||
sendSync: onEvent,
|
||||
on(event: string, listener: () => {}) {
|
||||
(listeners[event] ??= new Set()).add(listener);
|
||||
},
|
||||
off(event: string, listener: () => {}) {
|
||||
return listeners[event]?.delete(listener);
|
||||
},
|
||||
invoke: (event: string, ...args: any[]) => Promise.resolve(onEvent(event, ...args))
|
||||
},
|
||||
};
|
@ -1,19 +0,0 @@
|
||||
if (typeof browser === "undefined") {
|
||||
var browser = chrome;
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = browser.runtime.getURL("dist/Vencord.js");
|
||||
|
||||
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
BIN
browser/icon.png
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
@ -1,51 +0,0 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"minimum_chrome_version": "91",
|
||||
|
||||
"name": "Vencord Web",
|
||||
"description": "The cutest Discord mod now in your browser",
|
||||
"author": "Vendicated",
|
||||
"homepage_url": "https://github.com/Vendicated/Vencord",
|
||||
"icons": {
|
||||
"128": "icon.png"
|
||||
},
|
||||
|
||||
"host_permissions": [
|
||||
"*://*.discord.com/*",
|
||||
"https://raw.githubusercontent.com/*"
|
||||
],
|
||||
|
||||
"permissions": ["declarativeNetRequest"],
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"run_at": "document_start",
|
||||
"matches": ["*://*.discord.com/*"],
|
||||
"js": ["content.js"]
|
||||
}
|
||||
],
|
||||
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["dist/Vencord.js", "dist/Vencord.css"],
|
||||
"matches": ["*://*.discord.com/*"]
|
||||
}
|
||||
],
|
||||
|
||||
"declarative_net_request": {
|
||||
"rule_resources": [
|
||||
{
|
||||
"id": "modifyResponseHeaders",
|
||||
"enabled": true,
|
||||
"path": "modifyResponseHeaders.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "vencord-firefox@vendicated.dev",
|
||||
"strict_min_version": "109.0"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"action": {
|
||||
"type": "modifyHeaders",
|
||||
"responseHeaders": [
|
||||
{
|
||||
"header": "content-security-policy",
|
||||
"operation": "remove"
|
||||
},
|
||||
{
|
||||
"header": "content-security-policy-report-only",
|
||||
"operation": "remove"
|
||||
}
|
||||
]
|
||||
},
|
||||
"condition": {
|
||||
"resourceTypes": ["main_frame"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"action": {
|
||||
"type": "modifyHeaders",
|
||||
"responseHeaders": [
|
||||
{
|
||||
"header": "content-type",
|
||||
"operation": "set",
|
||||
"value": "text/css"
|
||||
}
|
||||
]
|
||||
},
|
||||
"condition": {
|
||||
"resourceTypes": ["stylesheet"],
|
||||
"urlFilter": "https://raw.githubusercontent.com/*"
|
||||
}
|
||||
}
|
||||
]
|
@ -1,24 +0,0 @@
|
||||
// ==UserScript==
|
||||
// @name Vencord
|
||||
// @description A Discord client mod - Web version
|
||||
// @version %version%
|
||||
// @author Vendicated (https://github.com/Vendicated)
|
||||
// @namespace https://github.com/Vendicated/Vencord
|
||||
// @supportURL https://github.com/Vendicated/Vencord
|
||||
// @license GPL-3.0
|
||||
// @match *://*.discord.com/*
|
||||
// @grant GM_xmlhttpRequest
|
||||
// @run-at document-start
|
||||
// @compatible chrome Chrome + Tampermonkey or Violentmonkey
|
||||
// @compatible firefox Firefox Tampermonkey
|
||||
// @compatible opera Opera + Tampermonkey or Violentmonkey
|
||||
// @compatible edge Edge + Tampermonkey or Violentmonkey
|
||||
// @compatible safari Safari + Tampermonkey or Violentmonkey
|
||||
// ==/UserScript==
|
||||
|
||||
|
||||
// this UserScript DOES NOT work on Firefox with Violentmonkey or Greasemonkey due to a bug that makes it impossible
|
||||
// to overwrite stuff on the window on sites that use CSP. Use Tampermonkey or use a chromium based browser
|
||||
// https://github.com/violentmonkey/violentmonkey/issues/997
|
||||
|
||||
// this is a compiled and minified version of Vencord. For the source code, visit the GitHub repo
|
122
build.mjs
Executable file
122
build.mjs
Executable file
@ -0,0 +1,122 @@
|
||||
#!/usr/bin/node
|
||||
import { execSync } from "child_process";
|
||||
import esbuild from "esbuild";
|
||||
import { readdirSync } from "fs";
|
||||
import { performance } from "perf_hooks";
|
||||
|
||||
/**
|
||||
* @type {esbuild.WatchMode|false}
|
||||
*/
|
||||
const watch = process.argv.includes("--watch");
|
||||
|
||||
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
||||
/**
|
||||
* @type {esbuild.Plugin}
|
||||
*/
|
||||
const makeAllPackagesExternalPlugin = {
|
||||
name: 'make-all-packages-external',
|
||||
setup(build) {
|
||||
let filter = /^[^.\/]|^\.[^.\/]|^\.\.[^\/]/; // Must not start with "/" or "./" or "../"
|
||||
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {esbuild.Plugin}
|
||||
*/
|
||||
const globPlugins = {
|
||||
name: "glob-plugins",
|
||||
setup: build => {
|
||||
build.onResolve({ filter: /^plugins$/ }, args => {
|
||||
return {
|
||||
namespace: "import-plugins",
|
||||
path: args.path
|
||||
};
|
||||
});
|
||||
|
||||
build.onLoad({ filter: /^plugins$/, namespace: "import-plugins" }, () => {
|
||||
const files = readdirSync("./src/plugins");
|
||||
let code = "";
|
||||
let obj = "";
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (files[i] === "index.ts") {
|
||||
continue;
|
||||
}
|
||||
const mod = `__pluginMod${i}`;
|
||||
code += `import ${mod} from "./${files[i].replace(/.tsx?$/, "")}";\n`;
|
||||
obj += `[${mod}.name]: ${mod},`;
|
||||
}
|
||||
code += `export default {${obj}}`;
|
||||
return {
|
||||
contents: code,
|
||||
resolveDir: "./src/plugins"
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
||||
/**
|
||||
* @type {esbuild.Plugin}
|
||||
*/
|
||||
const gitHashPlugin = {
|
||||
name: "git-hash-plugin",
|
||||
setup: build => {
|
||||
const filter = /^git-hash$/;
|
||||
build.onResolve({ filter }, args => ({
|
||||
namespace: "git-hash", path: args.path
|
||||
}));
|
||||
build.onLoad({ filter, namespace: "git-hash" }, () => ({
|
||||
contents: `export default "${gitHash}"`
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
esbuild.build({
|
||||
logLevel: "info",
|
||||
entryPoints: ["src/preload.ts"],
|
||||
outfile: "dist/preload.js",
|
||||
format: "cjs",
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
target: ["esnext"],
|
||||
sourcemap: "linked",
|
||||
plugins: [makeAllPackagesExternalPlugin],
|
||||
watch
|
||||
}),
|
||||
esbuild.build({
|
||||
logLevel: "info",
|
||||
entryPoints: ["src/patcher.ts"],
|
||||
outfile: "dist/patcher.js",
|
||||
bundle: true,
|
||||
format: "cjs",
|
||||
target: ["esnext"],
|
||||
external: ["electron"],
|
||||
platform: "node",
|
||||
sourcemap: "linked",
|
||||
plugins: [makeAllPackagesExternalPlugin],
|
||||
watch
|
||||
}),
|
||||
esbuild.build({
|
||||
logLevel: "info",
|
||||
entryPoints: ["src/Vencord.ts"],
|
||||
outfile: "dist/renderer.js",
|
||||
format: "iife",
|
||||
bundle: true,
|
||||
target: ["esnext"],
|
||||
footer: { js: "//# sourceURL=VencordRenderer" },
|
||||
globalName: "Vencord",
|
||||
external: ["plugins", "git-hash"],
|
||||
plugins: [
|
||||
globPlugins,
|
||||
gitHashPlugin
|
||||
],
|
||||
sourcemap: false,
|
||||
watch,
|
||||
minify: true,
|
||||
})
|
||||
]).catch(err => {
|
||||
console.error("Build failed");
|
||||
console.error(err.message);
|
||||
});
|
@ -1,202 +0,0 @@
|
||||
> **Warning**
|
||||
> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead.
|
||||
|
||||
# Installation Guide
|
||||
|
||||
Welcome to Megu's Installation Guide! In this file, you will learn about how to download, install, and uninstall Vencord!
|
||||
|
||||
## Sections
|
||||
|
||||
- [Installation Guide](#installation-guide)
|
||||
- [Sections](#sections)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Installing Vencord](#installing-vencord)
|
||||
- [Updating Vencord](#updating-vencord)
|
||||
- [Uninstalling Vencord](#uninstalling-vencord)
|
||||
- [Manually Installing Vencord](#manually-installing-vencord)
|
||||
- [On Windows](#on-windows)
|
||||
- [On Linux](#on-linux)
|
||||
- [On MacOS](#on-macos)
|
||||
- [Manual Patching](#manual-patching)
|
||||
- [Manually Uninstalling Vencord](#manually-uninstalling-vencord)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Install Git from https://git-scm.com/download
|
||||
- Install Node.JS LTS from here: https://nodejs.dev/en/
|
||||
|
||||
## Installing Vencord
|
||||
|
||||
> :exclamation: If this doesn't work, see [Manually Installing Vencord](#manually-installing-vencord)
|
||||
|
||||
Install `pnpm`:
|
||||
|
||||
> :exclamation: this may need to be run as admin depending on your system, and you may need to close and reopen your terminal.
|
||||
|
||||
```shell
|
||||
npm i -g pnpm
|
||||
```
|
||||
|
||||
Clone Vencord:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/Vendicated/Vencord
|
||||
cd Vencord
|
||||
```
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```shell
|
||||
pnpm install --frozen-lockfile
|
||||
```
|
||||
|
||||
Build Vencord:
|
||||
|
||||
```shell
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Inject vencord into your client:
|
||||
|
||||
```shell
|
||||
pnpm inject
|
||||
```
|
||||
|
||||
Then fully close Discord from your taskbar or task manager, and restart it. Vencord should be injected - you can check this by looking for the Vencord section in Discord settings.
|
||||
|
||||
## Updating Vencord
|
||||
|
||||
If you're using Discord already, go into the `Updater` tab in settings.
|
||||
|
||||
Sometimes it may be neccessary to manually update if the GUI updater fails.
|
||||
|
||||
To pull latest changes:
|
||||
|
||||
```shell
|
||||
git pull
|
||||
```
|
||||
|
||||
If this fails, you likely need to reset your local changes to vencord to resolve merge errors:
|
||||
|
||||
> :exclamation: This command will remove any local changes you've made to vencord. Make sure you back up if you made any code changes you don't want to lose!
|
||||
|
||||
```shell
|
||||
git reset --hard
|
||||
git pull
|
||||
```
|
||||
|
||||
and then to build the changes:
|
||||
|
||||
```shell
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Then just refresh your client
|
||||
|
||||
## Uninstalling Vencord
|
||||
|
||||
Simply run:
|
||||
|
||||
```shell
|
||||
pnpm uninject
|
||||
```
|
||||
|
||||
The above command may ask you to also run:
|
||||
|
||||
```shell
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm uninject
|
||||
```
|
||||
|
||||
## Manually Installing Vencord
|
||||
|
||||
- [Windows](#on-windows)
|
||||
- [Linux](#on-linux)
|
||||
- [MacOS](#on-macos)
|
||||
|
||||
### On Windows
|
||||
|
||||
Press Win+R and enter: `%LocalAppData%` and hit enter. In this page, find the page (Discord, DiscordPTB, DiscordCanary, etc) that you want to patch.
|
||||
|
||||
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||
|
||||
### On Linux
|
||||
|
||||
The Discord folder is usually in one of the following paths:
|
||||
|
||||
- /usr/share
|
||||
- /usr/lib64
|
||||
- /opt
|
||||
- /home/$USER/.local/share
|
||||
|
||||
If you use flatpak, it will usually be in one of the following paths:
|
||||
|
||||
- /var/lib/flatpak/app/com.discordapp.Discord/current/active/files
|
||||
- /home/$USER/.local/share/flatpak/app/com.discordapp.Discord/current/active/files
|
||||
|
||||
You will need to give flatpak access to vencord with one of the following commands:
|
||||
|
||||
> :exclamation: If not on stable, replace `com.discordapp.Discord` with your branch name, e.g., `com.discordapp.DiscordCanary`
|
||||
|
||||
> :exclamation: Replace `/path/to/vencord/` with the path to your vencord folder (NOT the dist folder)
|
||||
|
||||
If Discord flatpak install is in /home/:
|
||||
|
||||
```shell
|
||||
flatpak override --user com.discordapp.Discord --filesystem="/path/to/vencord/"
|
||||
```
|
||||
|
||||
If Discord flatpak install not in /home/:
|
||||
|
||||
```shell
|
||||
sudo flatpak override com.discordapp.Discord --filesystem="/path/to/vencord"
|
||||
```
|
||||
|
||||
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||
|
||||
### On MacOS
|
||||
|
||||
Open finder and go to your Applications folder. Right-Click on the Discord application you want to patch, and view contents.
|
||||
|
||||
Go to the `Contents/Resources` folder.
|
||||
|
||||
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||
|
||||
### Manual Patching
|
||||
|
||||
> :exclamation: If using Flatpak on linux, go to the folder that contains the `app.asar` file, and skip to where we create the `app` folder below.
|
||||
|
||||
> :exclamation: On Linux/MacOS, there's a chance there won't be an `app-<number>` folder, but there probably is a `resources` folder, so keep reading :)
|
||||
|
||||
Inside there, look for the `app-<number>` folders. If you have multiple, use the highest number. If that doesn't work, do it for the rest of the `app-<number>` folders.
|
||||
|
||||
Inside there, go to the `resources` folder. There should be a file called `app.asar`. If there isn't, look at a different `app-<number>` folder instead.
|
||||
|
||||
Make a new folder in `resources` called `app`. In here, we will make two files:
|
||||
|
||||
`package.json` and `index.js`
|
||||
|
||||
In `index.js`:
|
||||
|
||||
> :exclamation: Replace the path in the first line with the path to `patcher.js` in your vencord dist folder.
|
||||
> On Windows, you can get this by shift-rightclicking the patcher.js file and selecting "copy as path"
|
||||
|
||||
```js
|
||||
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
|
||||
```
|
||||
|
||||
And in `package.json`:
|
||||
|
||||
```json
|
||||
{ "name": "discord", "main": "index.js" }
|
||||
```
|
||||
|
||||
Finally, fully close & reopen your Discord client and check to see that `Vencord` appears in settings!
|
||||
|
||||
### Manually Uninstalling Vencord
|
||||
|
||||
> :exclamation: Do not delete `app.asar` - Only delete the `app` folder we created.
|
||||
|
||||
Use the instructions above to find the `app` folder, and delete it. Then Close & Reopen Discord.
|
||||
|
||||
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
@ -1,111 +0,0 @@
|
||||
# Plugins Guide
|
||||
|
||||
Welcome to Megu's Plugin Guide! In this file, you will learn about how to write your own plugin!
|
||||
|
||||
You don't need to run `pnpm build` every time you make a change. Instead, use `pnpm watch` - this will auto-compile Vencord whenever you make a change. If using code patches (recommended), you will need to CTRL+R to load the changes.
|
||||
|
||||
## Plugin Entrypoint
|
||||
|
||||
> If it doesn't already exist, create a folder called `userplugins` in the `src` directory of this repo.
|
||||
|
||||
1. Create a folder in `src/userplugins/` with the name of your plugin. For example, `src/userplugins/epicPlugin/` - All of your plugin files will go here.
|
||||
|
||||
2. Create a file in that folder called `index.ts`
|
||||
|
||||
3. In `index.ts`, copy-paste the following template code:
|
||||
|
||||
```ts
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "Epic Plugin",
|
||||
description: "This plugin is absolutely epic",
|
||||
authors: [
|
||||
{
|
||||
id: 12345n,
|
||||
name: "Your Name",
|
||||
},
|
||||
],
|
||||
patches: [],
|
||||
// Delete these two below if you are only using code patches
|
||||
start() {},
|
||||
stop() {},
|
||||
});
|
||||
```
|
||||
|
||||
Change the name, description, and authors to your own information.
|
||||
|
||||
Replace `12345n` with your user ID ending in `n` (e.g., `545581357812678656n`). If you don't want to share your Discord account, use `0n` instead!
|
||||
|
||||
## How Plugins Work In Vencord
|
||||
|
||||
Vencord uses a different way of making mods than you're used to.
|
||||
Instead of monkeypatching webpack, we directly modify the code before Discord loads it.
|
||||
|
||||
This is _significantly_ more efficient than monkeypatching webpack, and is surprisingly easy, but it may be confusing at first.
|
||||
|
||||
## Making your patch
|
||||
|
||||
For an in-depth guide into patching code, see [CONTRIBUTING.md](../CONTRIBUTING.md)
|
||||
|
||||
in the `index.ts` file we made earlier, you'll see a `patches` array.
|
||||
|
||||
> You'll see examples of how patches are used in all the existing plugins, and it'll be easier to understand by looking at those examples, so do that first, and then return here!
|
||||
|
||||
> For a good example of a plugin using code patches AND runtime patching, check `src/plugins/unindent.ts`, which uses code patches to run custom runtime code.
|
||||
|
||||
One of the patches in the `isStaff` plugin, looks like this:
|
||||
|
||||
```ts
|
||||
{
|
||||
match: /(\w+)\.isStaff=function\(\){return\s*!1};/,
|
||||
replace: "$1.isStaff=function(){return true};",
|
||||
},
|
||||
```
|
||||
|
||||
The above regex matches the string in discord that will look something like:
|
||||
|
||||
```js
|
||||
abc.isStaff = function () {
|
||||
return !1;
|
||||
};
|
||||
```
|
||||
|
||||
Remember that Discord code is minified, so there won't be any newlines, and there will only be spaces where necessary. So the source code looks something like:
|
||||
|
||||
```
|
||||
abc.isStaff=function(){return!1;}
|
||||
```
|
||||
|
||||
You can find these snippets by opening the devtools (`ctrl+shift+i`) and pressing `ctrl+shift+f`, searching for what you're looking to modify in there, and beautifying the file to make it more readable.
|
||||
|
||||
In the `match` regex in the example shown above, you'll notice at the start there is a `(\w+)`.
|
||||
Anything in the brackets will be accessible in the `replace` string using `$<number>`. e.g., the first pair of brackets will be `$1`, the second will be `$2`, etc.
|
||||
|
||||
The replacement string we used is:
|
||||
|
||||
```
|
||||
"$1.isStaff=function(){return true;};"
|
||||
```
|
||||
|
||||
Which, using the above example, would replace the code with:
|
||||
|
||||
> **Note**
|
||||
> In this example, `$1` becomes `abc`
|
||||
|
||||
```js
|
||||
abc.isStaff = function () {
|
||||
return true;
|
||||
};
|
||||
```
|
||||
|
||||
The match value _can_ be a string, rather than regex, however usually regex will be better suited, as it can work with unknown values, whereas strings must be exact matches.
|
||||
|
||||
Once you've made your plugin, make sure you run `pnpm test` and make sure your code is nice and clean!
|
||||
|
||||
If you want to publish your plugin into the Vencord repo, move your plugin from `src/userplugins` into the `src/plugins` folder and open a PR!
|
||||
|
||||
> **Warning**
|
||||
> Make sure you've read [CONTRIBUTING.md](../CONTRIBUTING.md) before opening a PR
|
||||
|
||||
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
90
install.ps1
Normal file
90
install.ps1
Normal file
@ -0,0 +1,90 @@
|
||||
# Vencord Windows Installer
|
||||
|
||||
$patcher = "$PWD\dist\patcher.js"
|
||||
$patcher_safe = $patcher -replace '\\', '\\'
|
||||
|
||||
$APP_PATCH = @"
|
||||
require("$patcher_safe");
|
||||
require("../app.asar");
|
||||
"@
|
||||
|
||||
$PACKAGE_JSON = @"
|
||||
{
|
||||
"main": "index.js",
|
||||
"name": "discord"
|
||||
}
|
||||
"@
|
||||
|
||||
$branch_paths = Get-ChildItem -Directory -Path $env:LOCALAPPDATA |
|
||||
Select-String -Pattern "Discord\w*" -AllMatches |
|
||||
Select-String -Pattern "DiscordGames" -NotMatch # Ignore DiscordGames folder
|
||||
|
||||
$branches = @()
|
||||
|
||||
foreach ($branch in $branch_paths) {
|
||||
$branch = $branch.Line.Split("\")[-1]
|
||||
|
||||
if ($branch -eq "Discord") {
|
||||
$branch = "Discord Stable"
|
||||
} else {
|
||||
$branch = $branch.Replace("Discord", "Discord ")
|
||||
}
|
||||
|
||||
$branches = $branches + $branch
|
||||
}
|
||||
|
||||
$branch_count = $branches.Count
|
||||
|
||||
Write-Output "Found $branch_count Branches"
|
||||
Write-Output "====================================="
|
||||
Write-Output "===== Select a Branch to patch ======"
|
||||
|
||||
$i = 0
|
||||
foreach ($branch in $branches) {
|
||||
Write-Output "=== $i. $branch"
|
||||
$i++
|
||||
}
|
||||
|
||||
Write-Output "====================================="
|
||||
$pos = Read-Host "Enter a number"
|
||||
|
||||
if ($null -eq $branches[$pos]) {
|
||||
Write-Output "Invalid branch selection"
|
||||
exit
|
||||
}
|
||||
|
||||
$branch = $branches.Get($pos)
|
||||
$discord_root = $branch_paths.Get($pos)
|
||||
|
||||
Write-Output "`nPatching $branch"
|
||||
|
||||
$app_folders = Get-ChildItem -Directory -Path $discord_root |
|
||||
Select-String -Pattern "app-"
|
||||
|
||||
foreach ($folder in $app_folders)
|
||||
{
|
||||
$version = [regex]::match($folder, 'app-([\d\.]+)').Groups[1].Value
|
||||
Write-Output "Patching Version $version"
|
||||
|
||||
$resources = "$folder\resources"
|
||||
if (-not(Test-Path -Path "$resources")) {
|
||||
Write-Error "Resources folder does not exist. Outdated version?`n"
|
||||
continue
|
||||
}
|
||||
if (-not(Test-Path -Path "$resources\app.asar")) {
|
||||
Write-Error "Failed to find app.asar in $folder`n"
|
||||
continue
|
||||
}
|
||||
|
||||
$app = "$resources\app"
|
||||
if (Test-Path -Path $app) {
|
||||
Write-Error "Are you already patched? App folder already exists at $resources`n"
|
||||
continue
|
||||
}
|
||||
|
||||
$null = New-Item -Path $app -ItemType Directory
|
||||
$null = Tee-Object -InputObject $APP_PATCH -FilePath "$app\index.js"
|
||||
$null = Tee-Object -InputObject $PACKAGE_JSON -FilePath "$app\package.json"
|
||||
|
||||
Write-Output "Patched $branch (version $version) successfully"
|
||||
}
|
75
install.sh
Executable file
75
install.sh
Executable file
@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Super simple installer. You should probably run this as root.
|
||||
# If you are getting permission issues, this is probably why.
|
||||
#
|
||||
# If this doesn't work for you, or you're not on Linux, just
|
||||
# - locate your Discord folder
|
||||
# - inside the resources folder, create a new folder "app"
|
||||
# - inside app create the files index.js and package.json.
|
||||
# See the two tee commands at the end of the file for their contents
|
||||
|
||||
patcher="$PWD/dist/patcher.js"
|
||||
|
||||
discord_bin="$(which discord)"
|
||||
discord_actual="$(readlink "$discord_bin")"
|
||||
|
||||
if [ -z "$discord_actual" ]; then
|
||||
case "$(head -n1 "$discord_bin")" in
|
||||
# has shebang?
|
||||
\#!/*)
|
||||
# Wrapper script, assume 2nd line has exec electron call and try to match asar path
|
||||
path="$(tail -1 "$discord_bin" | grep -Eo "\S+/app.asar" | sed 's/${name}/discord/')"
|
||||
if [ -z "$path" ]; then
|
||||
echo "Unsupported Install. $discord_bin is wrapper script but last line isn't exec call?"
|
||||
exit
|
||||
elif [ -e "$path" ]; then
|
||||
discord="$(dirname "$path")"
|
||||
else
|
||||
echo "Unsupported Install. $path not found"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported Install. $discord_bin is neither symlink nor a wrapper script.";
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
discord="$(dirname "$discord_actual")"
|
||||
fi
|
||||
|
||||
resources="$discord/resources"
|
||||
app="$resources/app"
|
||||
app_asar="app.asar"
|
||||
|
||||
if [ ! -e "$resources" ]; then
|
||||
if [ -e "$discord/app.asar.unpacked" ]; then
|
||||
# System Electron Install
|
||||
mv "$discord/app.asar" "$discord/_app.asar"
|
||||
mv "$discord/app.asar.unpacked" "$discord/_app.asar.unpacked"
|
||||
app="$discord/app.asar"
|
||||
app_asar="_app.asar"
|
||||
else
|
||||
echo "Unsupported Install. $discord has no resources folder but also isn't system electron install"
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -e "$app" ]; then
|
||||
echo "app folder exists. Looks like your Discord is already modified."
|
||||
exit
|
||||
fi
|
||||
|
||||
mkdir "$app"
|
||||
tee > "$app/index.js" << EOF
|
||||
require("$patcher");
|
||||
require("../$app_asar");
|
||||
EOF
|
||||
|
||||
tee > "$app/package.json" << EOF
|
||||
{
|
||||
"main": "index.js",
|
||||
"name": "discord"
|
||||
}
|
||||
EOF
|
100
package.json
100
package.json
@ -1,94 +1,20 @@
|
||||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.0.6",
|
||||
"description": "The cutest Discord client mod",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Vendicated/Vencord/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Vendicated/Vencord.git"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"author": "Vendicated",
|
||||
"directories": {
|
||||
"doc": "docs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node scripts/build/build.mjs",
|
||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||
"inject": "node scripts/runInstaller.mjs",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"lint-styles": "stylelint \"src/**/*.css\"",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc",
|
||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
||||
"testTsc": "tsc --noEmit",
|
||||
"uninject": "node scripts/runInstaller.mjs",
|
||||
"watch": "node scripts/build/build.mjs --watch"
|
||||
"devDependencies": {
|
||||
"@types/flux": "^3.1.11",
|
||||
"@types/node": "^18.7.13",
|
||||
"@types/react": "^18.0.17",
|
||||
"electron": "^20.1.0",
|
||||
"esbuild": "^0.15.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vap/core": "0.0.12",
|
||||
"@vap/shiki": "0.10.3",
|
||||
"fflate": "^0.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.0.2",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/yazl": "^2.4.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.49.0",
|
||||
"diff": "^5.1.0",
|
||||
"@swc/wasm-web": "^1.3.4",
|
||||
"discord-types": "^1.3.26",
|
||||
"esbuild": "^0.15.18",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-path-alias": "^1.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"highlight.js": "10.6.0",
|
||||
"moment": "^2.29.4",
|
||||
"puppeteer-core": "^19.6.0",
|
||||
"standalone-electron-types": "^1.0.0",
|
||||
"stylelint": "^14.16.1",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"type-fest": "^3.5.3",
|
||||
"typescript": "^4.9.4"
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"jsposed": "^1.0.2",
|
||||
"prettier": "^2.7.1"
|
||||
},
|
||||
"packageManager": "pnpm@7.13.4",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
||||
"eslint@8.28.0": "patches/eslint@8.28.0.patch"
|
||||
},
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
"eslint-plugin-import",
|
||||
"eslint"
|
||||
]
|
||||
},
|
||||
"allowedDeprecatedVersions": {
|
||||
"source-map-resolve": "*",
|
||||
"resolve-url": "*",
|
||||
"source-map-url": "*",
|
||||
"urix": "*"
|
||||
}
|
||||
},
|
||||
"webExt": {
|
||||
"artifactsDir": "./dist",
|
||||
"build": {
|
||||
"overwriteDest": true
|
||||
},
|
||||
"sourceDir": "./dist/extension-v2-unpacked"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"scripts": {
|
||||
"build": "node build.mjs",
|
||||
"watch": "node build.mjs --watch"
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
diff --git a/lib/rules/no-relative.js b/lib/rules/no-relative.js
|
||||
index 71594c83f1f4f733ffcc6047d7f7084348335dbe..d8623d87c89499c442171db3272cba07c9efabbe 100644
|
||||
--- a/lib/rules/no-relative.js
|
||||
+++ b/lib/rules/no-relative.js
|
||||
@@ -41,7 +41,7 @@ module.exports = {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
|
||||
- if (!/^(\.?\.\/)/.test(importPath)) {
|
||||
+ if (!/^(\.\.\/)/.test(importPath)) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,45 +0,0 @@
|
||||
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));
|
2885
pnpm-lock.yaml
generated
2885
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,89 +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/>.
|
||||
*/
|
||||
|
||||
import esbuild from "esbuild";
|
||||
|
||||
import { commonOpts, globPlugins, isStandalone, watch } from "./common.mjs";
|
||||
|
||||
const defines = {
|
||||
IS_STANDALONE: isStandalone,
|
||||
IS_DEV: JSON.stringify(watch)
|
||||
};
|
||||
if (defines.IS_STANDALONE === "false")
|
||||
// If this is a local build (not standalone), optimise
|
||||
// for the specific platform we're on
|
||||
defines["process.platform"] = JSON.stringify(process.platform);
|
||||
|
||||
/**
|
||||
* @type {esbuild.BuildOptions}
|
||||
*/
|
||||
const nodeCommonOpts = {
|
||||
...commonOpts,
|
||||
format: "cjs",
|
||||
platform: "node",
|
||||
target: ["esnext"],
|
||||
minify: true,
|
||||
bundle: true,
|
||||
external: ["electron", ...commonOpts.external],
|
||||
define: defines,
|
||||
};
|
||||
|
||||
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
|
||||
const sourcemap = watch ? "inline" : "external";
|
||||
|
||||
await Promise.all([
|
||||
esbuild.build({
|
||||
...nodeCommonOpts,
|
||||
entryPoints: ["src/preload.ts"],
|
||||
outfile: "dist/preload.js",
|
||||
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
|
||||
sourcemap,
|
||||
}),
|
||||
esbuild.build({
|
||||
...nodeCommonOpts,
|
||||
entryPoints: ["src/patcher.ts"],
|
||||
outfile: "dist/patcher.js",
|
||||
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
|
||||
sourcemap,
|
||||
}),
|
||||
esbuild.build({
|
||||
...commonOpts,
|
||||
entryPoints: ["src/Vencord.ts"],
|
||||
outfile: "dist/renderer.js",
|
||||
format: "iife",
|
||||
target: ["esnext"],
|
||||
footer: { js: "//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
|
||||
globalName: "Vencord",
|
||||
sourcemap,
|
||||
plugins: [
|
||||
globPlugins,
|
||||
...commonOpts.plugins
|
||||
],
|
||||
define: {
|
||||
...defines,
|
||||
IS_WEB: false
|
||||
}
|
||||
}),
|
||||
]).catch(err => {
|
||||
console.error("Build failed");
|
||||
console.error(err.message);
|
||||
// make ci fail
|
||||
if (!commonOpts.watch)
|
||||
process.exitCode = 1;
|
||||
});
|
@ -1,145 +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/>.
|
||||
*/
|
||||
|
||||
|
||||
import esbuild from "esbuild";
|
||||
import { zip } from "fflate";
|
||||
import { readFileSync } from "fs";
|
||||
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
// wtf is this assert syntax
|
||||
import PackageJSON from "../../package.json" assert { type: "json" };
|
||||
import { commonOpts, globPlugins, watch } from "./common.mjs";
|
||||
|
||||
/**
|
||||
* @type {esbuild.BuildOptions}
|
||||
*/
|
||||
const commonOptions = {
|
||||
...commonOpts,
|
||||
entryPoints: ["browser/Vencord.ts"],
|
||||
globalName: "Vencord",
|
||||
format: "iife",
|
||||
external: ["plugins", "git-hash"],
|
||||
plugins: [
|
||||
globPlugins,
|
||||
...commonOpts.plugins,
|
||||
],
|
||||
target: ["esnext"],
|
||||
define: {
|
||||
IS_WEB: "true",
|
||||
IS_STANDALONE: "true",
|
||||
IS_DEV: JSON.stringify(watch)
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
esbuild.build({
|
||||
...commonOptions,
|
||||
outfile: "dist/browser.js",
|
||||
footer: { js: "//# sourceURL=VencordWeb" },
|
||||
}),
|
||||
esbuild.build({
|
||||
...commonOptions,
|
||||
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
|
||||
define: {
|
||||
"window": "unsafeWindow",
|
||||
...(commonOptions?.define)
|
||||
},
|
||||
outfile: "dist/Vencord.user.js",
|
||||
banner: {
|
||||
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${PackageJSON.version}.${new Date().getTime()}`)
|
||||
},
|
||||
footer: {
|
||||
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
|
||||
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
|
||||
},
|
||||
})
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* @type {(target: string, files: string[], shouldZip: boolean) => Promise<void>}
|
||||
*/
|
||||
async function buildPluginZip(target, files, shouldZip) {
|
||||
const entries = {
|
||||
"dist/Vencord.js": await readFile("dist/browser.js"),
|
||||
"dist/Vencord.css": await readFile("dist/browser.css"),
|
||||
...Object.fromEntries(await Promise.all(files.map(async 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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
zip(entries, {}, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
const out = join("dist", target);
|
||||
writeFile(out, data).then(() => {
|
||||
console.info("Extension written to " + out);
|
||||
resolve();
|
||||
}).catch(reject);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await rm(target, { recursive: true, force: true });
|
||||
await Promise.all(Object.entries(entries).map(async ([file, content]) => {
|
||||
const dest = join("dist", target, file);
|
||||
const parentDirectory = join(dest, "..");
|
||||
await mkdir(parentDirectory, { recursive: true });
|
||||
await writeFile(dest, content);
|
||||
}));
|
||||
|
||||
console.info("Unpacked Extension written to dist/" + target);
|
||||
}
|
||||
}
|
||||
|
||||
const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => {
|
||||
const cssRuntime = `
|
||||
;document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(
|
||||
Object.assign(document.createElement("style"), {
|
||||
textContent: \`${content.replaceAll("`", "\\`")}\`,
|
||||
id: "vencord-css-core"
|
||||
})
|
||||
), { once: true });
|
||||
`;
|
||||
|
||||
return appendFile("dist/Vencord.user.js", cssRuntime);
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
appendCssRuntime,
|
||||
buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true),
|
||||
buildPluginZip("extension-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
|
||||
]);
|
||||
|
@ -1,194 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { exec, execSync } from "child_process";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { readdir, readFile } from "fs/promises";
|
||||
import { join, relative } from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
export const watch = process.argv.includes("--watch");
|
||||
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
||||
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
||||
export const banner = {
|
||||
js: `
|
||||
// Vencord ${gitHash}
|
||||
// Standalone: ${isStandalone}
|
||||
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
|
||||
`.trim()
|
||||
};
|
||||
|
||||
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
||||
/**
|
||||
* @type {import("esbuild").Plugin}
|
||||
*/
|
||||
export const makeAllPackagesExternalPlugin = {
|
||||
name: "make-all-packages-external",
|
||||
setup(build) {
|
||||
const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
|
||||
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import("esbuild").Plugin}
|
||||
*/
|
||||
export const globPlugins = {
|
||||
name: "glob-plugins",
|
||||
setup: build => {
|
||||
const filter = /^~plugins$/;
|
||||
build.onResolve({ filter }, args => {
|
||||
return {
|
||||
namespace: "import-plugins",
|
||||
path: args.path
|
||||
};
|
||||
});
|
||||
|
||||
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
|
||||
const pluginDirs = ["plugins", "userplugins"];
|
||||
let code = "";
|
||||
let plugins = "\n";
|
||||
let i = 0;
|
||||
for (const dir of pluginDirs) {
|
||||
if (!existsSync(`./src/${dir}`)) continue;
|
||||
const files = await readdir(`./src/${dir}`);
|
||||
for (const file of files) {
|
||||
if (file.startsWith(".")) continue;
|
||||
if (file === "index.ts") {
|
||||
continue;
|
||||
}
|
||||
const mod = `p${i}`;
|
||||
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
|
||||
plugins += `[${mod}.name]:${mod},\n`;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
code += `export default {${plugins}};`;
|
||||
return {
|
||||
contents: code,
|
||||
resolveDir: "./src"
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import("esbuild").Plugin}
|
||||
*/
|
||||
export const gitHashPlugin = {
|
||||
name: "git-hash-plugin",
|
||||
setup: build => {
|
||||
const filter = /^~git-hash$/;
|
||||
build.onResolve({ filter }, args => ({
|
||||
namespace: "git-hash", path: args.path
|
||||
}));
|
||||
build.onLoad({ filter, namespace: "git-hash" }, () => ({
|
||||
contents: `export default "${gitHash}"`
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import("esbuild").Plugin}
|
||||
*/
|
||||
export const gitRemotePlugin = {
|
||||
name: "git-remote-plugin",
|
||||
setup: build => {
|
||||
const filter = /^~git-remote$/;
|
||||
build.onResolve({ filter }, args => ({
|
||||
namespace: "git-remote", path: args.path
|
||||
}));
|
||||
build.onLoad({ filter, namespace: "git-remote" }, async () => {
|
||||
const res = await promisify(exec)("git remote get-url origin", { encoding: "utf-8" });
|
||||
const remote = res.stdout.trim()
|
||||
.replace("https://github.com/", "")
|
||||
.replace("git@github.com:", "")
|
||||
.replace(/.git$/, "");
|
||||
|
||||
return { contents: `export default "${remote}"` };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import("esbuild").Plugin}
|
||||
*/
|
||||
export const fileIncludePlugin = {
|
||||
name: "file-include-plugin",
|
||||
setup: build => {
|
||||
const filter = /^~fileContent\/.+$/;
|
||||
build.onResolve({ filter }, args => ({
|
||||
namespace: "include-file",
|
||||
path: args.path,
|
||||
pluginData: {
|
||||
path: join(args.resolveDir, args.path.slice("include-file/".length))
|
||||
}
|
||||
}));
|
||||
build.onLoad({ filter, namespace: "include-file" }, async ({ pluginData: { path } }) => {
|
||||
const [name, format] = path.split(";");
|
||||
return {
|
||||
contents: `export default ${JSON.stringify(await readFile(name, format ?? "utf-8"))}`
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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}
|
||||
*/
|
||||
export const commonOpts = {
|
||||
logLevel: "info",
|
||||
bundle: true,
|
||||
watch,
|
||||
minify: !watch,
|
||||
sourcemap: watch ? "inline" : "",
|
||||
legalComments: "linked",
|
||||
banner,
|
||||
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
||||
external: ["~plugins", "~git-hash", "~git-remote"],
|
||||
inject: ["./scripts/build/inject/react.mjs"],
|
||||
jsxFactory: "VencordCreateElement",
|
||||
jsxFragment: "VencordFragment",
|
||||
// Work around https://github.com/evanw/esbuild/issues/2460
|
||||
tsconfig: "./scripts/build/tsconfig.esbuild.json"
|
||||
};
|
@ -1,21 +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 VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment");
|
||||
export let VencordCreateElement =
|
||||
(...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);
|
@ -1,26 +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/>.
|
||||
*/
|
||||
|
||||
(window.VencordStyles ??= new Map()).set(STYLE_NAME, {
|
||||
name: STYLE_NAME,
|
||||
source: STYLE_SOURCE,
|
||||
classNames: {},
|
||||
dom: null,
|
||||
});
|
||||
|
||||
export default STYLE_NAME;
|
@ -1,7 +0,0 @@
|
||||
// Work around https://github.com/evanw/esbuild/issues/2460
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
/*
|
||||
* 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,62 +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/>.
|
||||
*/
|
||||
|
||||
// A script to automatically generate a list of all plugins.
|
||||
// Just copy paste the entire file into a running Vencord install and it will prompt you
|
||||
// to save the file
|
||||
|
||||
// eslint-disable-next-line spaced-comment
|
||||
/// <reference types="../src/modules"/>
|
||||
|
||||
(() => {
|
||||
/**
|
||||
* @type {typeof import("~plugins").default}
|
||||
*/
|
||||
const Plugins = Vencord.Plugins.plugins;
|
||||
|
||||
const header = `
|
||||
<!-- This file is auto generated, do not edit -->
|
||||
|
||||
# Vencord Plugins
|
||||
`;
|
||||
|
||||
let tableOfContents = "\n\n";
|
||||
|
||||
let list = "\n\n";
|
||||
|
||||
for (const p of Object.values(Plugins).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
tableOfContents += `- [${p.name}](#${p.name.replaceAll(" ", "-")})\n`;
|
||||
|
||||
list += `## ${p.name}
|
||||
|
||||
${p.description}
|
||||
|
||||
**Authors**: ${p.authors.map(a => a.name).join(", ")}
|
||||
`;
|
||||
|
||||
if (p.commands?.length) {
|
||||
list += "\n\n#### Commands\n";
|
||||
for (const cmd of p.commands) {
|
||||
list += `${cmd.name} - ${cmd.description}\n\n`;
|
||||
}
|
||||
}
|
||||
list += "\n\n";
|
||||
}
|
||||
|
||||
copy(header + tableOfContents + list);
|
||||
})();
|
@ -1,128 +0,0 @@
|
||||
/*
|
||||
* 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"
|
||||
}
|
||||
});
|
@ -1,24 +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/>.
|
||||
*/
|
||||
|
||||
process.emit = (originalEmit => function (name, data) {
|
||||
if (name === "warning" && data?.name === "ExperimentalWarning")
|
||||
return false;
|
||||
|
||||
return originalEmit.apply(process, arguments);
|
||||
})(process.emit);
|
@ -1,97 +1,48 @@
|
||||
/*!
|
||||
* 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 * as Api from "./api";
|
||||
export * as Plugins from "./plugins";
|
||||
export * as Util from "./utils";
|
||||
export * as QuickCss from "./utils/quickCss";
|
||||
export * as Updater from "./utils/updater";
|
||||
export * as Webpack from "./webpack";
|
||||
export { PlainSettings, Settings };
|
||||
|
||||
import "./utils/quickCss";
|
||||
import "./webpack/patchWebpack";
|
||||
export * as Api from "./api";
|
||||
export * as Updater from "./utils/updater";
|
||||
export * as QuickCss from "./utils/quickCss";
|
||||
export * as Swc from "@swc/wasm-web";
|
||||
import initSwc from "@swc/wasm-web";
|
||||
|
||||
import { popNotice, showNotice } from "./api/Notices";
|
||||
import { PlainSettings, Settings } from "./api/settings";
|
||||
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
||||
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
|
||||
import { onceReady } from "./webpack";
|
||||
import { SettingsRouter } from "./webpack/common";
|
||||
import { Settings } from "./api/settings";
|
||||
import { startAllPlugins } from "./plugins";
|
||||
|
||||
export let Components: any;
|
||||
export { Settings };
|
||||
|
||||
import "./webpack/patchWebpack";
|
||||
import "./utils/quickCss";
|
||||
import { checkForUpdates, UpdateLogger } from './utils/updater';
|
||||
import { onceReady } from "./webpack";
|
||||
import { Router } from "./webpack/common";
|
||||
import IpcEvents from "./utils/IpcEvents";
|
||||
|
||||
export let Components;
|
||||
|
||||
async function init() {
|
||||
await initSwc(await VencordNative.ipc.invoke(IpcEvents.GET_SWC_WASM));
|
||||
await onceReady;
|
||||
startAllPlugins();
|
||||
Components = await import("./components");
|
||||
|
||||
if (!IS_WEB) {
|
||||
try {
|
||||
const isOutdated = await checkForUpdates();
|
||||
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)
|
||||
if (isOutdated && Settings.notifyAboutUpdates)
|
||||
setTimeout(() => {
|
||||
showNotice(
|
||||
"A Vencord update is available!",
|
||||
"View Update",
|
||||
() => {
|
||||
popNotice();
|
||||
SettingsRouter.open("VencordUpdater");
|
||||
Router.open("VencordUpdater");
|
||||
}
|
||||
);
|
||||
}, 10_000);
|
||||
}, 10000);
|
||||
} catch (err) {
|
||||
UpdateLogger.error("Failed to check for updates", err);
|
||||
}
|
||||
}
|
||||
|
||||
if (IS_DEV) {
|
||||
const pendingPatches = patches.filter(p => !p.all && p.predicate?.() !== false);
|
||||
if (pendingPatches.length)
|
||||
PMLogger.warn(
|
||||
"Webpack has finished initialising, but some patches haven't been applied yet.",
|
||||
"This might be expected since some Modules are lazy loaded, but please verify",
|
||||
"that all plugins are working as intended.",
|
||||
"You are seeing this warning because this is a Development build of Vencord.",
|
||||
"\nThe following patches have not been applied:",
|
||||
"\n\n" + pendingPatches.map(p => `${p.plugin}: ${p.find}`).join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
@ -1,23 +1,5 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022
|
||||
*
|
||||
* 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 IPC_EVENTS from "@utils/IpcEvents";
|
||||
import { IpcRenderer, ipcRenderer } from "electron";
|
||||
import IPC_EVENTS from './utils/IpcEvents';
|
||||
import { IpcRenderer, ipcRenderer } from 'electron';
|
||||
|
||||
function assertEventAllowed(event: string) {
|
||||
if (!(event in IPC_EVENTS)) throw new Error(`Event ${event} not allowed.`);
|
||||
@ -45,5 +27,14 @@ export default {
|
||||
assertEventAllowed(event);
|
||||
return ipcRenderer.invoke(event, ...args);
|
||||
}
|
||||
},
|
||||
require(mod: string) {
|
||||
const settings = ipcRenderer.sendSync(IPC_EVENTS.GET_SETTINGS);
|
||||
try {
|
||||
if (!JSON.parse(settings).unsafeRequire) throw "no";
|
||||
} catch {
|
||||
throw new Error("Unsafe require is not allowed. Enable it in settings and try again.");
|
||||
}
|
||||
return require(mod);
|
||||
}
|
||||
};
|
||||
|
@ -1,107 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { User } from "discord-types/general";
|
||||
import { ComponentType, HTMLProps } from "react";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
|
||||
export enum BadgePosition {
|
||||
START,
|
||||
END
|
||||
}
|
||||
|
||||
export interface ProfileBadge {
|
||||
/** The tooltip to show on hover. Required for image badges */
|
||||
tooltip?: string;
|
||||
/** Custom component for the badge (tooltip not included) */
|
||||
component?: ComponentType<ProfileBadge & BadgeUserArgs>;
|
||||
/** The custom image to use */
|
||||
image?: string;
|
||||
/** Action to perform when you click the badge */
|
||||
onClick?(): void;
|
||||
/** Should the user display this badge? */
|
||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
||||
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
||||
props?: HTMLProps<HTMLImageElement>;
|
||||
/** Insert at start or end? */
|
||||
position?: BadgePosition;
|
||||
/** The badge name to display, Discord uses this. Required for component badges */
|
||||
key?: string;
|
||||
}
|
||||
|
||||
const Badges = new Set<ProfileBadge>();
|
||||
|
||||
/**
|
||||
* Register a new badge with the Badges API
|
||||
* @param badge The badge to register
|
||||
*/
|
||||
export function addBadge(badge: ProfileBadge) {
|
||||
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
|
||||
Badges.add(badge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a badge from the Badges API
|
||||
* @param badge The badge to remove
|
||||
*/
|
||||
export function removeBadge(badge: ProfileBadge) {
|
||||
return Badges.delete(badge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject badges into the profile badges array.
|
||||
* You probably don't need to use this.
|
||||
*/
|
||||
export function inject(badgeArray: ProfileBadge[], args: BadgeUserArgs) {
|
||||
for (const badge of Badges) {
|
||||
if (!badge.shouldShow || badge.shouldShow(args)) {
|
||||
badge.position === BadgePosition.START
|
||||
? badgeArray.unshift({ ...badge, ...args })
|
||||
: badgeArray.push({ ...badge, ...args });
|
||||
}
|
||||
}
|
||||
(Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id);
|
||||
|
||||
return badgeArray;
|
||||
}
|
||||
|
||||
export interface BadgeUserArgs {
|
||||
user: User;
|
||||
profile: Profile;
|
||||
premiumSince: Date;
|
||||
premiumGuildSince?: Date;
|
||||
}
|
||||
|
||||
interface ConnectedAccount {
|
||||
type: string;
|
||||
id: string;
|
||||
name: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
connectedAccounts: ConnectedAccount[];
|
||||
premiumType: number;
|
||||
premiumSince: string;
|
||||
premiumGuildSince?: any;
|
||||
lastFetched: number;
|
||||
profileFetchFailed: boolean;
|
||||
application?: any;
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { mergeDefaults } from "@utils/misc";
|
||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||
import { SnowflakeUtils } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
import type { PartialDeep } from "type-fest";
|
||||
|
||||
import { Argument } from "./types";
|
||||
|
||||
const createBotMessage = findByCodeLazy('username:"Clyde"');
|
||||
const MessageSender = findByPropsLazy("receiveMessage");
|
||||
|
||||
export function generateId() {
|
||||
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message as Clyde
|
||||
* @param {string} channelId ID of channel to send message to
|
||||
* @param {Message} message Message to send
|
||||
* @returns {Message}
|
||||
*/
|
||||
export function sendBotMessage(channelId: string, message: PartialDeep<Message>): Message {
|
||||
const botMessage = createBotMessage({ channelId, content: "", embeds: [] });
|
||||
|
||||
MessageSender.receiveMessage(channelId, mergeDefaults(message, botMessage));
|
||||
|
||||
return message as Message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of an option by name
|
||||
* @param args Arguments array (first argument passed to execute)
|
||||
* @param name Name of the argument
|
||||
* @param fallbackValue Fallback value in case this option wasn't passed
|
||||
* @returns Value
|
||||
*/
|
||||
export function findOption<T>(args: Argument[], name: string): T & {} | undefined;
|
||||
export function findOption<T>(args: Argument[], name: string, fallbackValue: T): T & {};
|
||||
export function findOption(args: Argument[], name: string, fallbackValue?: any) {
|
||||
return (args.find(a => a.name === name)?.value || fallbackValue) as any;
|
||||
}
|
@ -1,166 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { makeCodeblock } from "@utils/misc";
|
||||
|
||||
import { sendBotMessage } from "./commandHelpers";
|
||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
|
||||
|
||||
export * from "./commandHelpers";
|
||||
export * from "./types";
|
||||
|
||||
export let BUILT_IN: Command[];
|
||||
export const commands = {} as Record<string, Command>;
|
||||
|
||||
// hack for plugins being evaluated before we can grab these from webpack
|
||||
const OptPlaceholder = Symbol("OptionalMessageOption") as any as Option;
|
||||
const ReqPlaceholder = Symbol("RequiredMessageOption") as any as Option;
|
||||
/**
|
||||
* Optional message option named "message" you can use in commands.
|
||||
* Used in "tableflip" or "shrug"
|
||||
* @see {@link RequiredMessageOption}
|
||||
*/
|
||||
export let OptionalMessageOption: Option = OptPlaceholder;
|
||||
/**
|
||||
* Required message option named "message" you can use in commands.
|
||||
* Used in "me"
|
||||
* @see {@link OptionalMessageOption}
|
||||
*/
|
||||
export let RequiredMessageOption: Option = ReqPlaceholder;
|
||||
|
||||
export const _init = function (cmds: Command[]) {
|
||||
try {
|
||||
BUILT_IN = cmds;
|
||||
OptionalMessageOption = cmds.find(c => c.name === "shrug")!.options![0];
|
||||
RequiredMessageOption = cmds.find(c => c.name === "me")!.options![0];
|
||||
} catch (e) {
|
||||
console.error("Failed to load CommandsApi");
|
||||
}
|
||||
return cmds;
|
||||
} as never;
|
||||
|
||||
export const _handleCommand = function (cmd: Command, args: Argument[], ctx: CommandContext) {
|
||||
if (!cmd.isVencordCommand)
|
||||
return cmd.execute(args, ctx);
|
||||
|
||||
const handleError = (err: any) => {
|
||||
// TODO: cancel send if cmd.inputType === BUILT_IN_TEXT
|
||||
const msg = `An Error occurred while executing command "${cmd.name}"`;
|
||||
const reason = err instanceof Error ? err.stack || err.message : String(err);
|
||||
|
||||
console.error(msg, err);
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: `${msg}:\n${makeCodeblock(reason)}`,
|
||||
author: {
|
||||
username: "Vencord"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const res = cmd.execute(args, ctx);
|
||||
return res instanceof Promise ? res.catch(handleError) : res;
|
||||
} catch (err) {
|
||||
return handleError(err);
|
||||
}
|
||||
} as never;
|
||||
|
||||
|
||||
/**
|
||||
* Prepare a Command Option for Discord by filling missing fields
|
||||
* @param opt
|
||||
*/
|
||||
export function prepareOption<O extends Option | Command>(opt: O): O {
|
||||
opt.displayName ||= opt.name;
|
||||
opt.displayDescription ||= opt.description;
|
||||
opt.options?.forEach((opt, i, opts) => {
|
||||
// See comment above Placeholders
|
||||
if (opt === OptPlaceholder) opts[i] = OptionalMessageOption;
|
||||
else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;
|
||||
opt.choices?.forEach(x => x.displayName ||= x.name);
|
||||
|
||||
prepareOption(opts[i]);
|
||||
});
|
||||
return opt;
|
||||
}
|
||||
|
||||
// Yes, Discord registers individual commands for each subcommand
|
||||
// TODO: This probably doesn't support nested subcommands. If that is ever needed,
|
||||
// investigate
|
||||
function registerSubCommands(cmd: Command, plugin: string) {
|
||||
cmd.options?.forEach(o => {
|
||||
if (o.type !== ApplicationCommandOptionType.SUB_COMMAND)
|
||||
throw new Error("When specifying sub-command options, all options must be sub-commands.");
|
||||
const subCmd = {
|
||||
...cmd,
|
||||
...o,
|
||||
type: ApplicationCommandType.CHAT_INPUT,
|
||||
name: `${cmd.name} ${o.name}`,
|
||||
displayName: `${cmd.name} ${o.name}`,
|
||||
subCommandPath: [{
|
||||
name: o.name,
|
||||
type: o.type,
|
||||
displayName: o.name
|
||||
}],
|
||||
rootCommand: cmd
|
||||
};
|
||||
registerCommand(subCmd as any, plugin);
|
||||
});
|
||||
}
|
||||
|
||||
export function registerCommand<C extends Command>(command: C, plugin: string) {
|
||||
if (!BUILT_IN) {
|
||||
console.warn(
|
||||
"[CommandsAPI]",
|
||||
`Not registering ${command.name} as the CommandsAPI hasn't been initialised.`,
|
||||
"Please restart to use commands"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (BUILT_IN.some(c => c.name === command.name))
|
||||
throw new Error(`Command '${command.name}' already exists.`);
|
||||
|
||||
command.isVencordCommand = true;
|
||||
command.id ??= `-${BUILT_IN.length + 1}`;
|
||||
command.applicationId ??= "-1"; // BUILT_IN;
|
||||
command.type ??= ApplicationCommandType.CHAT_INPUT;
|
||||
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
|
||||
command.plugin ||= plugin;
|
||||
|
||||
prepareOption(command);
|
||||
|
||||
if (command.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND) {
|
||||
registerSubCommands(command, plugin);
|
||||
return;
|
||||
}
|
||||
|
||||
commands[command.name] = command;
|
||||
BUILT_IN.push(command);
|
||||
}
|
||||
|
||||
export function unregisterCommand(name: string) {
|
||||
const idx = BUILT_IN.findIndex(c => c.name === name);
|
||||
if (idx === -1)
|
||||
return false;
|
||||
|
||||
BUILT_IN.splice(idx, 1);
|
||||
delete commands[name];
|
||||
|
||||
return true;
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Channel, Guild } from "discord-types/general";
|
||||
import { Promisable } from "type-fest";
|
||||
|
||||
export interface CommandContext {
|
||||
channel: Channel;
|
||||
guild?: Guild;
|
||||
}
|
||||
|
||||
export enum ApplicationCommandOptionType {
|
||||
SUB_COMMAND = 1,
|
||||
SUB_COMMAND_GROUP = 2,
|
||||
STRING = 3,
|
||||
INTEGER = 4,
|
||||
BOOLEAN = 5,
|
||||
USER = 6,
|
||||
CHANNEL = 7,
|
||||
ROLE = 8,
|
||||
MENTIONABLE = 9,
|
||||
NUMBER = 10,
|
||||
ATTACHMENT = 11,
|
||||
}
|
||||
|
||||
export enum ApplicationCommandInputType {
|
||||
BUILT_IN = 0,
|
||||
BUILT_IN_TEXT = 1,
|
||||
BUILT_IN_INTEGRATION = 2,
|
||||
BOT = 3,
|
||||
PLACEHOLDER = 4,
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
type: ApplicationCommandOptionType;
|
||||
description: string;
|
||||
displayDescription?: string;
|
||||
required?: boolean;
|
||||
options?: Option[];
|
||||
choices?: Array<ChoicesOption>;
|
||||
}
|
||||
|
||||
export interface ChoicesOption {
|
||||
label: string;
|
||||
value: string;
|
||||
name: string;
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
export enum ApplicationCommandType {
|
||||
CHAT_INPUT = 1,
|
||||
USER = 2,
|
||||
MESSAGE = 3,
|
||||
}
|
||||
|
||||
export interface CommandReturnValue {
|
||||
content: string;
|
||||
/** TODO: implement */
|
||||
cancel?: boolean;
|
||||
}
|
||||
|
||||
export interface Argument {
|
||||
type: ApplicationCommandOptionType;
|
||||
name: string;
|
||||
value: string;
|
||||
focused: undefined;
|
||||
options: Argument[];
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
id?: string;
|
||||
applicationId?: string;
|
||||
type?: ApplicationCommandType;
|
||||
inputType?: ApplicationCommandInputType;
|
||||
plugin?: string;
|
||||
isVencordCommand?: boolean;
|
||||
|
||||
name: string;
|
||||
displayName?: string;
|
||||
description: string;
|
||||
displayDescription?: string;
|
||||
|
||||
options?: Option[];
|
||||
predicate?(ctx: CommandContext): boolean;
|
||||
|
||||
execute(args: Argument[], ctx: CommandContext): Promisable<void | CommandReturnValue>;
|
||||
}
|
@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -1,279 +0,0 @@
|
||||
/* eslint-disable header/header */
|
||||
|
||||
/*!
|
||||
* idb-keyval v6.2.0
|
||||
* Copyright 2016, Jake Archibald
|
||||
* Copyright 2022, Vendicated
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export function promisifyRequest<T = undefined>(
|
||||
request: IDBRequest<T> | IDBTransaction,
|
||||
): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
// @ts-ignore - file size hacks
|
||||
request.oncomplete = request.onsuccess = () => resolve(request.result);
|
||||
// @ts-ignore - file size hacks
|
||||
request.onabort = request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
export function createStore(dbName: string, storeName: string): UseStore {
|
||||
const request = indexedDB.open(dbName);
|
||||
request.onupgradeneeded = () => request.result.createObjectStore(storeName);
|
||||
const dbp = promisifyRequest(request);
|
||||
|
||||
return (txMode, callback) =>
|
||||
dbp.then(db =>
|
||||
callback(db.transaction(storeName, txMode).objectStore(storeName)),
|
||||
);
|
||||
}
|
||||
|
||||
export type UseStore = <T>(
|
||||
txMode: IDBTransactionMode,
|
||||
callback: (store: IDBObjectStore) => T | PromiseLike<T>,
|
||||
) => Promise<T>;
|
||||
|
||||
let defaultGetStoreFunc: UseStore | undefined;
|
||||
|
||||
function defaultGetStore() {
|
||||
if (!defaultGetStoreFunc) {
|
||||
defaultGetStoreFunc = createStore("VencordData", "VencordStore");
|
||||
}
|
||||
return defaultGetStoreFunc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value by its key.
|
||||
*
|
||||
* @param key
|
||||
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
||||
*/
|
||||
export function get<T = any>(
|
||||
key: IDBValidKey,
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<T | undefined> {
|
||||
return customStore("readonly", store => promisifyRequest(store.get(key)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value with a key.
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
||||
*/
|
||||
export function set(
|
||||
key: IDBValidKey,
|
||||
value: any,
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<void> {
|
||||
return customStore("readwrite", store => {
|
||||
store.put(value, key);
|
||||
return promisifyRequest(store.transaction);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set multiple values at once. This is faster than calling set() multiple times.
|
||||
* It's also atomic – if one of the pairs can't be added, none will be added.
|
||||
*
|
||||
* @param entries Array of entries, where each entry is an array of `[key, value]`.
|
||||
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
||||
*/
|
||||
export function setMany(
|
||||
entries: [IDBValidKey, any][],
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<void> {
|
||||
return customStore("readwrite", store => {
|
||||
entries.forEach(entry => store.put(entry[1], entry[0]));
|
||||
return promisifyRequest(store.transaction);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple values by their keys
|
||||
*
|
||||
* @param keys
|
||||
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
||||
*/
|
||||
export function getMany<T = any>(
|
||||
keys: IDBValidKey[],
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<T[]> {
|
||||
return customStore("readonly", store =>
|
||||
Promise.all(keys.map(key => promisifyRequest(store.get(key)))),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a value. This lets you see the old value and update it as an atomic operation.
|
||||
*
|
||||
* @param key
|
||||
* @param updater A callback that takes the old value and returns a new value.
|
||||
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
||||
*/
|
||||
export function update<T = any>(
|
||||
key: IDBValidKey,
|
||||
updater: (oldValue: T | undefined) => T,
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<void> {
|
||||
return customStore(
|
||||
"readwrite",
|
||||
store =>
|
||||
// Need to create the promise manually.
|
||||
// If I try to chain promises, the transaction closes in browsers
|
||||
// that use a promise polyfill (IE10/11).
|
||||
new Promise((resolve, reject) => {
|
||||
store.get(key).onsuccess = function () {
|
||||
try {
|
||||
store.put(updater(this.result), key);
|
||||
resolve(promisifyRequest(store.transaction));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a particular key from the store.
|
||||
*
|
||||
* @param key
|
||||
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
||||
*/
|
||||
export function del(
|
||||
key: IDBValidKey,
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<void> {
|
||||
return customStore("readwrite", store => {
|
||||
store.delete(key);
|
||||
return promisifyRequest(store.transaction);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple keys at once.
|
||||
*
|
||||
* @param keys List of keys to delete.
|
||||
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
||||
*/
|
||||
export function delMany(
|
||||
keys: IDBValidKey[],
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<void> {
|
||||
return customStore("readwrite", (store: IDBObjectStore) => {
|
||||
keys.forEach((key: IDBValidKey) => store.delete(key));
|
||||
return promisifyRequest(store.transaction);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all values in the store.
|
||||
*
|
||||
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
||||
*/
|
||||
export function clear(customStore = defaultGetStore()): Promise<void> {
|
||||
return customStore("readwrite", store => {
|
||||
store.clear();
|
||||
return promisifyRequest(store.transaction);
|
||||
});
|
||||
}
|
||||
|
||||
function eachCursor(
|
||||
store: IDBObjectStore,
|
||||
callback: (cursor: IDBCursorWithValue) => void,
|
||||
): Promise<void> {
|
||||
store.openCursor().onsuccess = function () {
|
||||
if (!this.result) return;
|
||||
callback(this.result);
|
||||
this.result.continue();
|
||||
};
|
||||
return promisifyRequest(store.transaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keys in the store.
|
||||
*
|
||||
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
||||
*/
|
||||
export function keys<KeyType extends IDBValidKey>(
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<KeyType[]> {
|
||||
return customStore("readonly", store => {
|
||||
// Fast path for modern browsers
|
||||
if (store.getAllKeys) {
|
||||
return promisifyRequest(
|
||||
store.getAllKeys() as unknown as IDBRequest<KeyType[]>,
|
||||
);
|
||||
}
|
||||
|
||||
const items: KeyType[] = [];
|
||||
|
||||
return eachCursor(store, cursor =>
|
||||
items.push(cursor.key as KeyType),
|
||||
).then(() => items);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all values in the store.
|
||||
*
|
||||
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
||||
*/
|
||||
export function values<T = any>(customStore = defaultGetStore()): Promise<T[]> {
|
||||
return customStore("readonly", store => {
|
||||
// Fast path for modern browsers
|
||||
if (store.getAll) {
|
||||
return promisifyRequest(store.getAll() as IDBRequest<T[]>);
|
||||
}
|
||||
|
||||
const items: T[] = [];
|
||||
|
||||
return eachCursor(store, cursor => items.push(cursor.value as T)).then(
|
||||
() => items,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entries in the store. Each entry is an array of `[key, value]`.
|
||||
*
|
||||
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
||||
*/
|
||||
export function entries<KeyType extends IDBValidKey, ValueType = any>(
|
||||
customStore = defaultGetStore(),
|
||||
): Promise<[KeyType, ValueType][]> {
|
||||
return customStore("readonly", store => {
|
||||
// Fast path for modern browsers
|
||||
// (although, hopefully we'll get a simpler path some day)
|
||||
if (store.getAll && store.getAllKeys) {
|
||||
return Promise.all([
|
||||
promisifyRequest(
|
||||
store.getAllKeys() as unknown as IDBRequest<KeyType[]>,
|
||||
),
|
||||
promisifyRequest(store.getAll() as IDBRequest<ValueType[]>),
|
||||
]).then(([keys, values]) => keys.map((key, i) => [key, values[i]]));
|
||||
}
|
||||
|
||||
const items: [KeyType, ValueType][] = [];
|
||||
|
||||
return customStore("readonly", store =>
|
||||
eachCursor(store, cursor =>
|
||||
items.push([cursor.key as KeyType, cursor.value]),
|
||||
).then(() => items),
|
||||
);
|
||||
});
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { 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;
|
||||
});
|
||||
}
|
@ -1,68 +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 type AccessoryCallback = (props: Record<string, any>) => JSX.Element | null | Array<JSX.Element | null>;
|
||||
export type Accessory = {
|
||||
callback: AccessoryCallback;
|
||||
position?: number;
|
||||
};
|
||||
|
||||
export const accessories = new Map<String, Accessory>();
|
||||
|
||||
export function addAccessory(
|
||||
identifier: string,
|
||||
callback: AccessoryCallback,
|
||||
position?: number
|
||||
) {
|
||||
accessories.set(identifier, {
|
||||
callback,
|
||||
position,
|
||||
});
|
||||
}
|
||||
|
||||
export function removeAccessory(identifier: string) {
|
||||
accessories.delete(identifier);
|
||||
}
|
||||
|
||||
export function _modifyAccessories(
|
||||
elements: JSX.Element[],
|
||||
props: Record<string, any>
|
||||
) {
|
||||
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(
|
||||
accessory.position != null
|
||||
? accessory.position < 0
|
||||
? elements.length + accessory.position
|
||||
: accessory.position
|
||||
: elements.length,
|
||||
0,
|
||||
...accessories.filter(e => e != null) as JSX.Element[]
|
||||
);
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { 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);
|
||||
});
|
||||
}
|
@ -1,24 +1,5 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Logger from "@utils/Logger";
|
||||
import { MessageStore } from "@webpack/common";
|
||||
import type { Channel, Message } from "discord-types/general";
|
||||
import type { Message, Channel } from 'discord-types/general';
|
||||
import Logger from '../utils/logger';
|
||||
|
||||
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
||||
|
||||
@ -37,37 +18,25 @@ export interface MessageObject {
|
||||
validNonShortcutEmojis: Emoji[];
|
||||
}
|
||||
|
||||
export interface MessageExtra {
|
||||
stickerIds?: string[];
|
||||
}
|
||||
|
||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => void | { cancel: boolean; };
|
||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: any) => void;
|
||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
|
||||
|
||||
const sendListeners = new Set<SendListener>();
|
||||
const editListeners = new Set<EditListener>();
|
||||
|
||||
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
|
||||
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: any) {
|
||||
for (const listener of sendListeners) {
|
||||
try {
|
||||
const result = listener(channelId, messageObj, extra);
|
||||
if (result && result.cancel === true) {
|
||||
return true;
|
||||
listener(channelId, messageObj, extra);
|
||||
} catch (e) { MessageEventsLogger.error(`MessageSendHandler: Listener encoutered an unknown error. (${e})`); }
|
||||
}
|
||||
} catch (e) {
|
||||
MessageEventsLogger.error("MessageSendHandler: Listener encountered an unknown error\n", e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
|
||||
export function _handlePreEdit(channeld: string, messageId: string, messageObj: MessageObject) {
|
||||
for (const listener of editListeners) {
|
||||
try {
|
||||
listener(channelId, messageId, messageObj);
|
||||
} catch (e) {
|
||||
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
|
||||
}
|
||||
listener(channeld, messageId, messageObj);
|
||||
} catch (e) { MessageEventsLogger.error(`MessageEditHandler: Listener encoutered an unknown error. (${e})`); }
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,15 +67,11 @@ type ClickListener = (message: Message, channel: Channel, event: MouseEvent) =>
|
||||
|
||||
const listeners = new Set<ClickListener>();
|
||||
|
||||
export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
|
||||
// message object may be outdated, so (try to) fetch latest one
|
||||
message = MessageStore.getMessage(channel.id, message.id) ?? message;
|
||||
export function _handleClick(message, channel, event) {
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener(message, channel, event);
|
||||
} catch (e) {
|
||||
MessageEventsLogger.error("MessageClickHandler: Listener encountered an unknown error\n", e);
|
||||
}
|
||||
} catch (e) { MessageEventsLogger.error(`MessageClickHandler: Listener encoutered an unknown error. (${e})`); }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Logger from "@utils/Logger";
|
||||
import { Channel, Message } from "discord-types/general";
|
||||
import type { MouseEventHandler } from "react";
|
||||
|
||||
const logger = new Logger("MessagePopover");
|
||||
|
||||
export interface ButtonItem {
|
||||
key?: string,
|
||||
label: string,
|
||||
icon: React.ComponentType<any>,
|
||||
message: Message,
|
||||
channel: Channel,
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>,
|
||||
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export type getButtonItem = (message: Message) => ButtonItem | null;
|
||||
|
||||
export const buttons = new Map<string, getButtonItem>();
|
||||
|
||||
export function addButton(
|
||||
identifier: string,
|
||||
item: getButtonItem,
|
||||
) {
|
||||
buttons.set(identifier, item);
|
||||
}
|
||||
|
||||
export function removeButton(identifier: string) {
|
||||
buttons.delete(identifier);
|
||||
}
|
||||
|
||||
export function _buildPopoverElements(
|
||||
msg: Message,
|
||||
makeButton: (item: ButtonItem) => React.ComponentType
|
||||
) {
|
||||
const items = [] as React.ComponentType[];
|
||||
|
||||
for (const [identifier, getItem] of buttons.entries()) {
|
||||
try {
|
||||
const item = getItem(msg);
|
||||
if (item) {
|
||||
item.key ??= identifier;
|
||||
items.push(makeButton(item));
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`[${identifier}]`, err);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
@ -1,22 +1,4 @@
|
||||
/*
|
||||
* 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 { waitFor } from "@webpack";
|
||||
import { waitFor } from "../webpack";
|
||||
|
||||
let NoticesModule: any;
|
||||
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
|
||||
|
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* 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>
|
||||
);
|
||||
});
|
@ -1,99 +0,0 @@
|
||||
/*
|
||||
* 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++));
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* 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";
|
@ -1,49 +0,0 @@
|
||||
.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%;
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Logger from "@utils/Logger";
|
||||
|
||||
const logger = new Logger("ServerListAPI");
|
||||
|
||||
export enum ServerListRenderPosition {
|
||||
Above,
|
||||
In,
|
||||
}
|
||||
|
||||
const renderFunctionsAbove = new Set<Function>();
|
||||
const renderFunctionsIn = new Set<Function>();
|
||||
|
||||
function getRenderFunctions(position: ServerListRenderPosition) {
|
||||
return position === ServerListRenderPosition.Above ? renderFunctionsAbove : renderFunctionsIn;
|
||||
}
|
||||
|
||||
export function addServerListElement(position: ServerListRenderPosition, renderFunction: Function) {
|
||||
getRenderFunctions(position).add(renderFunction);
|
||||
}
|
||||
|
||||
export function removeServerListElement(position: ServerListRenderPosition, renderFunction: Function) {
|
||||
getRenderFunctions(position).delete(renderFunction);
|
||||
}
|
||||
|
||||
export const renderAll = (position: ServerListRenderPosition) => {
|
||||
const ret: Array<JSX.Element> = [];
|
||||
|
||||
for (const renderFunction of getRenderFunctions(position)) {
|
||||
try {
|
||||
ret.unshift(renderFunction());
|
||||
} catch (e) {
|
||||
logger.error("Failed to render server list element:", e);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
@ -1,162 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 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(" ");
|
||||
};
|
@ -1,95 +1,2 @@
|
||||
/*
|
||||
* 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 * as $Badges from "./Badges";
|
||||
import * as $Commands from "./Commands";
|
||||
import * as $DataStore from "./DataStore";
|
||||
import * as $MemberListDecorators from "./MemberListDecorators";
|
||||
import * as $MessageAccessories from "./MessageAccessories";
|
||||
import * as $MessageDecorations from "./MessageDecorations";
|
||||
import * as $MessageEventsAPI from "./MessageEvents";
|
||||
import * as $MessagePopover from "./MessagePopover";
|
||||
import * as $Notices from "./Notices";
|
||||
import * as $Notifications from "./Notifications";
|
||||
import * as $ServerList from "./ServerList";
|
||||
import * as $Styles from "./Styles";
|
||||
|
||||
/**
|
||||
* An API allowing you to listen to Message Clicks or run your own logic
|
||||
* before a message is sent
|
||||
*
|
||||
* If your plugin uses this, you must add MessageEventsAPI to its dependencies
|
||||
*/
|
||||
export const MessageEvents = $MessageEventsAPI;
|
||||
/**
|
||||
* An API allowing you to create custom notices
|
||||
* (snackbars on the top, like the Update prompt)
|
||||
*/
|
||||
export const Notices = $Notices;
|
||||
/**
|
||||
* An API allowing you to register custom commands
|
||||
*/
|
||||
export const Commands = $Commands;
|
||||
/**
|
||||
* A wrapper around IndexedDB. This can store arbitrarily
|
||||
* large data and supports a lot of datatypes (Blob, Map, ...).
|
||||
* For a full list, see the mdn link below
|
||||
*
|
||||
* This should always be preferred over the Settings API if possible, as
|
||||
* localstorage has very strict size restrictions and blocks the event loop
|
||||
*
|
||||
* Make sure your keys are unique (tip: prefix them with ur plugin name)
|
||||
* and please clean up no longer needed entries.
|
||||
*
|
||||
* 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}
|
||||
*/
|
||||
export const DataStore = $DataStore;
|
||||
/**
|
||||
* An API allowing you to add custom components as message accessories
|
||||
*/
|
||||
export const MessageAccessories = $MessageAccessories;
|
||||
/**
|
||||
* An API allowing you to add custom buttons in the message popover
|
||||
*/
|
||||
export const MessagePopover = $MessagePopover;
|
||||
/**
|
||||
* An API allowing you to add badges to user profiles
|
||||
*/
|
||||
export const Badges = $Badges;
|
||||
/**
|
||||
* An API allowing you to add custom elements to the server list
|
||||
*/
|
||||
export const ServerList = $ServerList;
|
||||
/**
|
||||
* An API allowing you to add components as message accessories
|
||||
*/
|
||||
export const MessageDecorations = $MessageDecorations;
|
||||
/**
|
||||
* An API allowing you to add components to member list users, in both DM's and servers
|
||||
*/
|
||||
export const MemberListDecorators = $MemberListDecorators;
|
||||
/**
|
||||
* An API allowing you to dynamically load styles
|
||||
* a
|
||||
*/
|
||||
export const Styles = $Styles;
|
||||
/**
|
||||
* An API allowing you to display notifications
|
||||
*/
|
||||
export const Notifications = $Notifications;
|
||||
export * as MessageEvents from "./MessageEvents";
|
||||
export * as Notices from "./Notices";
|
||||
|
@ -1,159 +1,74 @@
|
||||
/*
|
||||
* 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 plugins from "plugins";
|
||||
import IpcEvents from "../utils/IpcEvents";
|
||||
import { React } from "../webpack/common";
|
||||
import { mergeDefaults } from '../utils/misc';
|
||||
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import Logger from "@utils/Logger";
|
||||
import { mergeDefaults } from "@utils/misc";
|
||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
import plugins from "~plugins";
|
||||
|
||||
const logger = new Logger("Settings");
|
||||
export interface Settings {
|
||||
interface Settings {
|
||||
notifyAboutUpdates: boolean;
|
||||
autoUpdate: boolean;
|
||||
unsafeRequire: boolean;
|
||||
useQuickCss: boolean;
|
||||
enableReactDevtools: boolean;
|
||||
themeLinks: string[];
|
||||
frameless: boolean;
|
||||
transparent: boolean;
|
||||
winCtrlQ: boolean;
|
||||
plugins: {
|
||||
[plugin: string]: {
|
||||
enabled: boolean;
|
||||
[setting: string]: any;
|
||||
};
|
||||
};
|
||||
|
||||
notifications: {
|
||||
timeout: number;
|
||||
position: "top-right" | "bottom-right";
|
||||
useNative: "always" | "never" | "not-focused";
|
||||
};
|
||||
}
|
||||
|
||||
const DefaultSettings: Settings = {
|
||||
notifyAboutUpdates: true,
|
||||
autoUpdate: false,
|
||||
unsafeRequire: false,
|
||||
useQuickCss: true,
|
||||
themeLinks: [],
|
||||
enableReactDevtools: false,
|
||||
frameless: false,
|
||||
transparent: false,
|
||||
winCtrlQ: false,
|
||||
plugins: {},
|
||||
|
||||
notifications: {
|
||||
timeout: 5000,
|
||||
position: "bottom-right",
|
||||
useNative: "not-focused"
|
||||
}
|
||||
plugins: {}
|
||||
};
|
||||
|
||||
for (const plugin in plugins) {
|
||||
DefaultSettings.plugins[plugin] = {
|
||||
enabled: plugins[plugin].required ?? false
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings;
|
||||
for (const key in DefaultSettings) {
|
||||
settings[key] ??= DefaultSettings[key];
|
||||
}
|
||||
mergeDefaults(settings, DefaultSettings);
|
||||
} catch (err) {
|
||||
console.error("Corrupt settings file. ", err);
|
||||
var settings = mergeDefaults({} as Settings, DefaultSettings);
|
||||
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
|
||||
}
|
||||
|
||||
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
|
||||
const subscriptions = new Set<SubscriptionCallback>();
|
||||
|
||||
const proxyCache = {} as Record<string, any>;
|
||||
|
||||
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
|
||||
function makeProxy(settings: any, root = settings, path = ""): Settings {
|
||||
return proxyCache[path] ??= new Proxy(settings, {
|
||||
function makeProxy(settings: Settings, root = settings, path = ""): Settings {
|
||||
return new Proxy(settings, {
|
||||
get(target, p: string) {
|
||||
const v = target[p];
|
||||
|
||||
// using "in" is important in the following cases to properly handle falsy or nullish values
|
||||
if (!(p in target)) {
|
||||
// Return empty for plugins with no settings
|
||||
if (path === "plugins" && p in plugins)
|
||||
return target[p] = makeProxy({
|
||||
enabled: plugins[p].required ?? false
|
||||
}, root, `plugins.${p}`);
|
||||
|
||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||
// the default value.
|
||||
if (path.startsWith("plugins.")) {
|
||||
const plugin = path.slice("plugins.".length);
|
||||
if (plugin in plugins) {
|
||||
const setting = plugins[plugin].options?.[p];
|
||||
if (!setting) return v;
|
||||
if ("default" in setting)
|
||||
// normal setting with a default value
|
||||
return (target[p] = setting.default);
|
||||
if (setting.type === OptionType.SELECT) {
|
||||
const def = setting.options.find(o => o.default);
|
||||
if (def)
|
||||
target[p] = def.value;
|
||||
return def?.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Recursively proxy Objects with the updated property path
|
||||
if (typeof v === "object" && !Array.isArray(v) && v !== null)
|
||||
if (typeof v === "object" && !Array.isArray(v))
|
||||
return makeProxy(v, root, `${path}${path && "."}${p}`);
|
||||
|
||||
// primitive or similar, no need to proxy further
|
||||
return v;
|
||||
},
|
||||
|
||||
set(target, p: string, v) {
|
||||
// avoid unnecessary updates to React Components and other listeners
|
||||
if (target[p] === v) return true;
|
||||
|
||||
target[p] = v;
|
||||
// Call any listeners that are listening to a setting of this path
|
||||
const setPath = `${path}${path && "."}${p}`;
|
||||
for (const subscription of subscriptions) {
|
||||
if (!subscription._path || subscription._path === setPath) {
|
||||
subscription(v, setPath);
|
||||
}
|
||||
}
|
||||
// And don't forget to persist the settings!
|
||||
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link Settings} but unproxied. You should treat this as readonly,
|
||||
* as modifying properties on this will not save to disk or call settings
|
||||
* listeners.
|
||||
* WARNING: default values specified in plugin.options will not be ensured here. In other words,
|
||||
* settings for which you specified a default value may be uninitialised. If you need proper
|
||||
* handling for default values, use {@link Settings}
|
||||
*/
|
||||
export const PlainSettings = settings;
|
||||
/**
|
||||
* A smart settings object. Altering props automagically saves
|
||||
* the updated settings to disk.
|
||||
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}
|
||||
*/
|
||||
export const Settings = makeProxy(settings);
|
||||
|
||||
@ -161,20 +76,14 @@ export const Settings = makeProxy(settings);
|
||||
* Settings hook for React components. Returns a smart settings
|
||||
* object that automagically triggers a rerender if any properties
|
||||
* are altered
|
||||
* @param paths An optional list of paths to whitelist for rerenders
|
||||
* @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() {
|
||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||
|
||||
const onUpdate: SubscriptionCallback = paths
|
||||
? (value, path) => paths.includes(path) && forceUpdate()
|
||||
: forceUpdate;
|
||||
|
||||
React.useEffect(() => {
|
||||
subscriptions.add(onUpdate);
|
||||
return () => void subscriptions.delete(onUpdate);
|
||||
subscriptions.add(forceUpdate);
|
||||
return () => void subscriptions.delete(forceUpdate);
|
||||
}, []);
|
||||
|
||||
return Settings;
|
||||
@ -201,37 +110,3 @@ export function addSettingsListener(path: string, onUpdate: (newValue: any, path
|
||||
(onUpdate as SubscriptionCallback)._path = path;
|
||||
subscriptions.add(onUpdate);
|
||||
}
|
||||
|
||||
export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
||||
const { plugins } = settings;
|
||||
if (name in plugins) return;
|
||||
|
||||
for (const oldName of oldNames) {
|
||||
if (oldName in plugins) {
|
||||
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
|
||||
plugins[name] = plugins[oldName];
|
||||
delete plugins[oldName];
|
||||
VencordNative.ipc.invoke(
|
||||
IpcEvents.SET_SETTINGS,
|
||||
JSON.stringify(settings, null, 4)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function definePluginSettings<D extends SettingsDefinition, C extends SettingsChecks<D>>(def: D, checks?: C) {
|
||||
const definedSettings: DefinedSettings<D> = {
|
||||
get store() {
|
||||
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
|
||||
return Settings.plugins[definedSettings.pluginName] as any;
|
||||
},
|
||||
use: settings => useSettings(
|
||||
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`)
|
||||
).plugins[definedSettings.pluginName] as any,
|
||||
def,
|
||||
checks: checks ?? {},
|
||||
pluginName: "",
|
||||
};
|
||||
return definedSettings;
|
||||
}
|
||||
|
@ -1,29 +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 function Badge({ text, color }): JSX.Element {
|
||||
return (
|
||||
<div className="vc-plugins-badge" style={{
|
||||
backgroundColor: color,
|
||||
justifySelf: "flex-end",
|
||||
marginLeft: "auto"
|
||||
}}>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { React, TextInput } from "@webpack/common";
|
||||
|
||||
// TODO: Refactor settings to use this as well
|
||||
interface TextInputProps {
|
||||
/**
|
||||
* WARNING: Changing this between renders will have no effect!
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* This will only be called if the new value passed validate()
|
||||
*/
|
||||
onChange(newValue: string): void;
|
||||
/**
|
||||
* Optionally validate the user input
|
||||
* Return true if the input is valid
|
||||
* Otherwise, return a string containing the reason for this input being invalid
|
||||
*/
|
||||
validate(v: string): true | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A very simple wrapper around Discord's TextInput that validates input and shows
|
||||
* the user an error message and only calls your onChange when the input is valid
|
||||
*/
|
||||
export function CheckedTextInput({ value: initialValue, onChange, validate }: TextInputProps) {
|
||||
const [value, setValue] = React.useState(initialValue);
|
||||
const [error, setError] = React.useState<string>();
|
||||
|
||||
function handleChange(v: string) {
|
||||
setValue(v);
|
||||
const res = validate(v);
|
||||
if (res === true) {
|
||||
setError(void 0);
|
||||
onChange(v);
|
||||
} else {
|
||||
setError(res);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
error={error}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { Button } from "@webpack/common";
|
||||
|
||||
import { Heart } from "./Heart";
|
||||
|
||||
export default function DonateButton(props: any) {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
look={Button.Looks.LINK}
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
onClick={() =>
|
||||
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/sponsors/Vendicated")
|
||||
}
|
||||
>
|
||||
<Heart />
|
||||
Donate
|
||||
</Button>
|
||||
);
|
||||
}
|
@ -1,36 +1,10 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Logger from "@utils/Logger";
|
||||
import { LazyComponent } from "@utils/misc";
|
||||
import { Margins, React } from "@webpack/common";
|
||||
|
||||
import Logger from "../utils/logger";
|
||||
import { Card, React } from "../webpack/common";
|
||||
import { ErrorCard } from "./ErrorCard";
|
||||
|
||||
interface Props {
|
||||
/** Render nothing if an error occurs */
|
||||
noop?: boolean;
|
||||
/** Fallback component to render if an error occurs */
|
||||
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
||||
/** called when an error occurs */
|
||||
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; }>>;
|
||||
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
||||
/** Custom error message */
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const color = "#e78284";
|
||||
@ -39,29 +13,25 @@ const logger = new Logger("React ErrorBoundary", color);
|
||||
|
||||
const NO_ERROR = {};
|
||||
|
||||
// We might want to import this in a place where React isn't ready yet.
|
||||
// Thus, wrap in a LazyComponent
|
||||
const ErrorBoundary = LazyComponent(() => {
|
||||
return class ErrorBoundary extends React.PureComponent<React.PropsWithChildren<Props>> {
|
||||
export default class ErrorBoundary extends React.Component<React.PropsWithChildren<Props>> {
|
||||
static wrap<T = any>(Component: React.ComponentType<T>): (props: T) => React.ReactElement {
|
||||
return (props) => (
|
||||
<ErrorBoundary>
|
||||
<Component {...props as any/* I hate react typings ??? */} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
state = {
|
||||
error: NO_ERROR as any,
|
||||
stack: "",
|
||||
message: ""
|
||||
};
|
||||
|
||||
static getDerivedStateFromError(error: any) {
|
||||
let stack = error?.stack ?? "";
|
||||
let message = error?.message || String(error);
|
||||
|
||||
if (error instanceof Error && stack) {
|
||||
const eolIdx = stack.indexOf("\n");
|
||||
if (eolIdx !== -1) {
|
||||
message = stack.slice(0, eolIdx);
|
||||
stack = stack.slice(eolIdx + 1).replace(/https:\/\/\S+\/assets\//g, "");
|
||||
}
|
||||
}
|
||||
|
||||
return { error, stack, message };
|
||||
return {
|
||||
error: error?.stack?.replace(/https:\/\/\S+\/assets\//g, "") || error?.message || String(error)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
@ -73,43 +43,27 @@ const ErrorBoundary = LazyComponent(() => {
|
||||
render() {
|
||||
if (this.state.error === NO_ERROR) return this.props.children;
|
||||
|
||||
if (this.props.noop) return null;
|
||||
|
||||
if (this.props.fallback)
|
||||
return <this.props.fallback
|
||||
children={this.props.children}
|
||||
{...this.state}
|
||||
error={this.state.error}
|
||||
/>;
|
||||
|
||||
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
||||
|
||||
return (
|
||||
<ErrorCard style={{
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<h1>Oh no!</h1>
|
||||
<p>{msg}</p>
|
||||
<p>
|
||||
An error occurred while rendering this Component. More info can be found below
|
||||
and in your console.
|
||||
</p>
|
||||
<code>
|
||||
{this.state.message}
|
||||
{!!this.state.stack && (
|
||||
<pre className={Margins.marginTop8}>
|
||||
{this.state.stack}
|
||||
<pre>
|
||||
{this.state.error}
|
||||
</pre>
|
||||
)}
|
||||
</code>
|
||||
</ErrorCard>
|
||||
);
|
||||
}
|
||||
};
|
||||
}) as
|
||||
React.ComponentType<React.PropsWithChildren<Props>> & {
|
||||
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
|
||||
};
|
||||
|
||||
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
||||
<ErrorBoundary {...errorBoundaryProps}>
|
||||
<Component {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
export default ErrorBoundary;
|
||||
}
|
||||
|
@ -1,22 +1,4 @@
|
||||
/*
|
||||
* 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 { Card } from "@webpack/common";
|
||||
import { Card } from "../webpack/common";
|
||||
|
||||
interface Props {
|
||||
style?: React.CSSProperties;
|
||||
|
@ -1,34 +1,15 @@
|
||||
/*
|
||||
* 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 { React } from "@webpack/common";
|
||||
import { PropsWithChildren } from "react";
|
||||
import type { React } from '../webpack/common';
|
||||
|
||||
export function Flex(props: React.PropsWithChildren<{
|
||||
flexDirection?: React.CSSProperties["flexDirection"];
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
} & React.HTMLProps<HTMLDivElement>>) {
|
||||
}>) {
|
||||
props.style ??= {};
|
||||
props.style.display = "flex";
|
||||
// TODO(ven): Remove me, what was I thinking??
|
||||
props.style.gap ??= "1em";
|
||||
props.style.flexDirection ||= props.flexDirection;
|
||||
delete props.flexDirection;
|
||||
props.style.gap ??= "1em";
|
||||
props.style.display = "flex";
|
||||
return (
|
||||
<div {...props}>
|
||||
{props.children}
|
||||
|
@ -1,35 +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 function Heart() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
style={{ marginRight: "0.5em", transform: "translateY(2px)" }}
|
||||
>
|
||||
<path
|
||||
fill="#db61a2"
|
||||
fill-rule="evenodd"
|
||||
d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,35 +1,18 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { React } from "../webpack/common";
|
||||
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
interface Props extends React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> {
|
||||
interface Props {
|
||||
href: string;
|
||||
disabled?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function Link(props: React.PropsWithChildren<Props>) {
|
||||
if (props.disabled) {
|
||||
props.style ??= {};
|
||||
props.style.pointerEvents = "none";
|
||||
props["aria-disabled"] = true;
|
||||
}
|
||||
return (
|
||||
<a role="link" target="_blank" {...props}>
|
||||
<a href={props.href} target="_blank" style={props.style}>
|
||||
{props.children}
|
||||
</a>
|
||||
);
|
||||
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { debounce } from "@utils/debounce";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { Queue } from "@utils/Queue";
|
||||
import { find } from "@webpack";
|
||||
|
||||
import monacoHtml from "~fileContent/monacoWin.html";
|
||||
|
||||
const queue = new Queue();
|
||||
const setCss = debounce((css: string) => {
|
||||
queue.push(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css));
|
||||
});
|
||||
|
||||
export async function launchMonacoEditor() {
|
||||
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.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
|
||||
win.getTheme = () =>
|
||||
find(m =>
|
||||
m.ProtoClass?.typeName.endsWith("PreloadedUserSettings")
|
||||
)?.getCurrentValue()?.appearance?.theme === 2
|
||||
? "vs-light"
|
||||
: "vs-dark";
|
||||
|
||||
win.document.write(monacoHtml);
|
||||
|
||||
window.__VENCORD_MONACO_WIN__ = new WeakRef(win);
|
||||
}
|
@ -1,308 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { makeCodeblock } from "@utils/misc";
|
||||
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches";
|
||||
import { search } from "@webpack";
|
||||
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
||||
|
||||
import { CheckedTextInput } from "./CheckedTextInput";
|
||||
import ErrorBoundary from "./ErrorBoundary";
|
||||
|
||||
// Do not include diff in non dev builds (side effects import)
|
||||
if (IS_DEV) {
|
||||
var differ = require("diff") as typeof import("diff");
|
||||
}
|
||||
|
||||
const findCandidates = debounce(function ({ find, setModule, setError }) {
|
||||
const candidates = search(find);
|
||||
const keys = Object.keys(candidates);
|
||||
const len = keys.length;
|
||||
if (len === 0)
|
||||
setError("No match. Perhaps that module is lazy loaded?");
|
||||
else if (len !== 1)
|
||||
setError("Multiple matches. Please refine your filter");
|
||||
else
|
||||
setModule([keys[0], candidates[keys[0]]]);
|
||||
});
|
||||
|
||||
interface ReplacementComponentProps {
|
||||
module: [id: number, factory: Function];
|
||||
match: string | RegExp;
|
||||
replacement: string | ReplaceFn;
|
||||
setReplacementError(error: any): void;
|
||||
}
|
||||
|
||||
function ReplacementComponent({ module, match, replacement, setReplacementError }: ReplacementComponentProps) {
|
||||
const [id, fact] = module;
|
||||
const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
|
||||
|
||||
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
||||
const src: string = fact.toString().replaceAll("\n", "");
|
||||
const canonicalMatch = canonicalizeMatch(match);
|
||||
try {
|
||||
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
||||
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
||||
setReplacementError(void 0);
|
||||
} catch (e) {
|
||||
setReplacementError((e as Error).message);
|
||||
return ["", [], []];
|
||||
}
|
||||
const m = src.match(canonicalMatch);
|
||||
return [patched, m, makeDiff(src, patched, m)];
|
||||
}, [id, match, replacement]);
|
||||
|
||||
function makeDiff(original: string, patched: string, match: RegExpMatchArray | null) {
|
||||
if (!match || original === patched) return null;
|
||||
|
||||
const changeSize = patched.length - original.length;
|
||||
|
||||
// Use 200 surrounding characters of context
|
||||
const start = Math.max(0, match.index! - 200);
|
||||
const end = Math.min(original.length, match.index! + match[0].length + 200);
|
||||
// (changeSize may be negative)
|
||||
const endPatched = end + changeSize;
|
||||
|
||||
const context = original.slice(start, end);
|
||||
const patchedContext = patched.slice(start, endPatched);
|
||||
|
||||
return differ.diffWordsWithSpace(context, patchedContext);
|
||||
}
|
||||
|
||||
function renderMatch() {
|
||||
if (!matchResult)
|
||||
return <Forms.FormText>Regex doesn't match!</Forms.FormText>;
|
||||
|
||||
const fullMatch = matchResult[0] ? makeCodeblock(matchResult[0], "js") : "";
|
||||
const groups = matchResult.length > 1
|
||||
? makeCodeblock(matchResult.slice(1).map((g, i) => `Group ${i + 1}: ${g}`).join("\n"), "yml")
|
||||
: "";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ userSelect: "text" }}>{Parser.parse(fullMatch)}</div>
|
||||
<div style={{ userSelect: "text" }}>{Parser.parse(groups)}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDiff() {
|
||||
return diff?.map(p => {
|
||||
const color = p.added ? "lime" : p.removed ? "red" : "grey";
|
||||
return <div style={{ color, userSelect: "text" }}>{p.value}</div>;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Forms.FormTitle>Module {id}</Forms.FormTitle>
|
||||
|
||||
{!!matchResult?.[0]?.length && (
|
||||
<>
|
||||
<Forms.FormTitle>Match</Forms.FormTitle>
|
||||
{renderMatch()}
|
||||
</>)
|
||||
}
|
||||
|
||||
{!!diff?.length && (
|
||||
<>
|
||||
<Forms.FormTitle>Diff</Forms.FormTitle>
|
||||
{renderDiff()}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!!diff?.length && (
|
||||
<Button className={Margins.marginTop20} onClick={() => {
|
||||
try {
|
||||
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
||||
setCompileResult([true, "Compiled successfully"]);
|
||||
} catch (err) {
|
||||
setCompileResult([false, (err as Error).message]);
|
||||
}
|
||||
}}>Compile</Button>
|
||||
)}
|
||||
|
||||
{compileResult &&
|
||||
<Forms.FormText style={{ color: compileResult[0] ? "var(--text-positive)" : "var(--text-danger)" }}>
|
||||
{compileResult[1]}
|
||||
</Forms.FormText>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
||||
const [isFunc, setIsFunc] = React.useState(false);
|
||||
const [error, setError] = React.useState<string>();
|
||||
|
||||
function onChange(v: string) {
|
||||
setError(void 0);
|
||||
|
||||
if (isFunc) {
|
||||
try {
|
||||
const func = (0, eval)(v);
|
||||
if (typeof func === "function")
|
||||
setReplacement(() => func);
|
||||
else
|
||||
setError("Replacement must be a function");
|
||||
} catch (e) {
|
||||
setReplacement(v);
|
||||
setError((e as Error).message);
|
||||
}
|
||||
} else {
|
||||
setReplacement(v);
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(
|
||||
() => void (isFunc ? onChange(replacement) : setError(void 0)),
|
||||
[isFunc]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Forms.FormTitle>replacement</Forms.FormTitle>
|
||||
<TextInput
|
||||
value={replacement?.toString()}
|
||||
onChange={onChange}
|
||||
error={error ?? replacementError}
|
||||
/>
|
||||
{!isFunc && (
|
||||
<>
|
||||
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
|
||||
{Object.entries({
|
||||
"$$": "Insert a $",
|
||||
"$&": "Insert the entire match",
|
||||
"$`\u200b": "Insert the substring before the match",
|
||||
"$'": "Insert the substring after the match",
|
||||
"$n": "Insert the nth capturing group ($1, $2...)",
|
||||
"$self": "Insert the plugin instance",
|
||||
}).map(([placeholder, desc]) => (
|
||||
<Forms.FormText key={placeholder}>
|
||||
{Parser.parse("`" + placeholder + "`")}: {desc}
|
||||
</Forms.FormText>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Switch
|
||||
className={Margins.marginTop8}
|
||||
value={isFunc}
|
||||
onChange={setIsFunc}
|
||||
note="'replacement' will be evaled if this is toggled"
|
||||
hideBorder={true}
|
||||
>
|
||||
Treat as Function
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PatchHelper() {
|
||||
const [find, setFind] = React.useState<string>("");
|
||||
const [match, setMatch] = React.useState<string>("");
|
||||
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
||||
|
||||
const [replacementError, setReplacementError] = React.useState<string>();
|
||||
|
||||
const [module, setModule] = React.useState<[number, Function]>();
|
||||
const [findError, setFindError] = React.useState<string>();
|
||||
|
||||
const code = React.useMemo(() => {
|
||||
return `
|
||||
{
|
||||
find: ${JSON.stringify(find)},
|
||||
replacement: {
|
||||
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
||||
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
||||
}
|
||||
}
|
||||
`.trim();
|
||||
}, [find, match, replacement]);
|
||||
|
||||
function onFindChange(v: string) {
|
||||
setFindError(void 0);
|
||||
setFind(v);
|
||||
if (v.length) {
|
||||
findCandidates({ find: v, setModule, setError: setFindError });
|
||||
}
|
||||
}
|
||||
|
||||
function onMatchChange(v: string) {
|
||||
try {
|
||||
new RegExp(v);
|
||||
setFindError(void 0);
|
||||
setMatch(v);
|
||||
} catch (e: any) {
|
||||
setFindError((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Text variant="heading-md/normal" tag="h2" className={Margins.marginBottom8}>Patch Helper</Text>
|
||||
<Forms.FormTitle>find</Forms.FormTitle>
|
||||
<TextInput
|
||||
type="text"
|
||||
value={find}
|
||||
onChange={onFindChange}
|
||||
error={findError}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle>match</Forms.FormTitle>
|
||||
<CheckedTextInput
|
||||
value={match}
|
||||
onChange={onMatchChange}
|
||||
validate={v => {
|
||||
try {
|
||||
return (new RegExp(v), true);
|
||||
} catch (e) {
|
||||
return (e as Error).message;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ReplacementInput
|
||||
replacement={replacement}
|
||||
setReplacement={setReplacement}
|
||||
replacementError={replacementError}
|
||||
/>
|
||||
|
||||
<Forms.FormDivider />
|
||||
{module && (
|
||||
<ReplacementComponent
|
||||
module={module}
|
||||
match={new RegExp(match)}
|
||||
replacement={replacement}
|
||||
setReplacementError={setReplacementError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!!(find && match && replacement) && (
|
||||
<>
|
||||
<Forms.FormTitle className={Margins.marginTop20}>Code</Forms.FormTitle>
|
||||
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
||||
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
||||
</>
|
||||
)}
|
||||
</Forms.FormSection>
|
||||
);
|
||||
}
|
||||
|
||||
export default IS_DEV ? ErrorBoundary.wrap(PatchHelper) : null;
|
@ -1,245 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { generateId } from "@api/Commands";
|
||||
import { useSettings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { LazyComponent } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||
import { proxyLazy } from "@utils/proxyLazy";
|
||||
import { OptionType, Plugin } from "@utils/types";
|
||||
import { findByCode, findByPropsLazy } from "@webpack";
|
||||
import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
import { Constructor } from "type-fest";
|
||||
|
||||
import {
|
||||
ISettingElementProps,
|
||||
SettingBooleanComponent,
|
||||
SettingCustomComponent,
|
||||
SettingNumericComponent,
|
||||
SettingSelectComponent,
|
||||
SettingSliderComponent,
|
||||
SettingTextComponent
|
||||
} from "./components";
|
||||
|
||||
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
||||
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
||||
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
||||
|
||||
interface PluginModalProps extends ModalProps {
|
||||
plugin: Plugin;
|
||||
onRestartNeeded(): void;
|
||||
}
|
||||
|
||||
/** To stop discord making unwanted requests... */
|
||||
function makeDummyUser(user: { name: string, id: BigInt; }) {
|
||||
const newUser = new UserRecord({
|
||||
username: user.name,
|
||||
id: generateId(),
|
||||
bot: true,
|
||||
});
|
||||
FluxDispatcher.dispatch({
|
||||
type: "USER_UPDATE",
|
||||
user: newUser,
|
||||
});
|
||||
return newUser;
|
||||
}
|
||||
|
||||
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any>>> = {
|
||||
[OptionType.STRING]: SettingTextComponent,
|
||||
[OptionType.NUMBER]: SettingNumericComponent,
|
||||
[OptionType.BIGINT]: SettingNumericComponent,
|
||||
[OptionType.BOOLEAN]: SettingBooleanComponent,
|
||||
[OptionType.SELECT]: SettingSelectComponent,
|
||||
[OptionType.SLIDER]: SettingSliderComponent,
|
||||
[OptionType.COMPONENT]: SettingCustomComponent
|
||||
};
|
||||
|
||||
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
||||
const [authors, setAuthors] = React.useState<Partial<User>[]>([]);
|
||||
|
||||
const pluginSettings = useSettings().plugins[plugin.name];
|
||||
|
||||
const [tempSettings, setTempSettings] = React.useState<Record<string, any>>({});
|
||||
|
||||
const [errors, setErrors] = React.useState<Record<string, boolean>>({});
|
||||
const [saveError, setSaveError] = React.useState<string | null>(null);
|
||||
|
||||
const canSubmit = () => Object.values(errors).every(e => !e);
|
||||
|
||||
const hasSettings = Boolean(pluginSettings && plugin.options);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
for (const user of plugin.authors.slice(0, 6)) {
|
||||
const author = user.id
|
||||
? await UserUtils.fetchUser(`${user.id}`).catch(() => makeDummyUser(user))
|
||||
: makeDummyUser(user);
|
||||
setAuthors(a => [...a, author]);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function saveAndClose() {
|
||||
if (!plugin.options) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (plugin.beforeSave) {
|
||||
const result = await Promise.resolve(plugin.beforeSave(tempSettings));
|
||||
if (result !== true) {
|
||||
setSaveError(result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let restartNeeded = false;
|
||||
for (const [key, value] of Object.entries(tempSettings)) {
|
||||
const option = plugin.options[key];
|
||||
pluginSettings[key] = value;
|
||||
option?.onChange?.(value);
|
||||
if (option?.restartNeeded) restartNeeded = true;
|
||||
}
|
||||
if (restartNeeded) onRestartNeeded();
|
||||
onClose();
|
||||
}
|
||||
|
||||
function renderSettings() {
|
||||
if (!hasSettings || !plugin.options) {
|
||||
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>;
|
||||
}
|
||||
}
|
||||
|
||||
function renderMoreUsers(_label: string, count: number) {
|
||||
const sliceCount = plugin.authors.length - count;
|
||||
const sliceStart = plugin.authors.length - sliceCount;
|
||||
const sliceEnd = sliceStart + plugin.authors.length - count;
|
||||
|
||||
return (
|
||||
<Tooltip text={plugin.authors.slice(sliceStart, sliceEnd).map(u => u.name).join(", ")}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<div
|
||||
className={AvatarStyles.moreUsers}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
+{sliceCount}
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
|
||||
<ModalHeader separator={false}>
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent style={{ marginBottom: 8, marginTop: 8 }}>
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
|
||||
<Forms.FormText>{plugin.description}</Forms.FormText>
|
||||
<Forms.FormTitle tag="h3" style={{ marginTop: 8, marginBottom: 0 }}>Authors</Forms.FormTitle>
|
||||
<div style={{ width: "fit-content", marginBottom: 8 }}>
|
||||
<UserSummaryItem
|
||||
users={authors}
|
||||
count={plugin.authors.length}
|
||||
guildId={undefined}
|
||||
renderIcon={false}
|
||||
max={6}
|
||||
showDefaultAvatarsForNullUsers
|
||||
showUserPopout
|
||||
renderMoreUsers={renderMoreUsers}
|
||||
/>
|
||||
</div>
|
||||
</Forms.FormSection>
|
||||
{!!plugin.settingsAboutComponent && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Forms.FormSection>
|
||||
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
|
||||
<plugin.settingsAboutComponent tempSettings={tempSettings} />
|
||||
</ErrorBoundary>
|
||||
</Forms.FormSection>
|
||||
</div>
|
||||
)}
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">Settings</Forms.FormTitle>
|
||||
{renderSettings()}
|
||||
</Forms.FormSection>
|
||||
</ModalContent>
|
||||
{hasSettings && <ModalFooter>
|
||||
<Flex flexDirection="column" style={{ width: "100%" }}>
|
||||
<Flex style={{ marginLeft: "auto" }}>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.WHITE}
|
||||
look={Button.Looks.LINK}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.BRAND}
|
||||
onClick={saveAndClose}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
disabled={!canSubmit()}
|
||||
>
|
||||
Save & Close
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>}
|
||||
</Flex>
|
||||
</ModalFooter>}
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { PluginOptionBoolean } from "@utils/types";
|
||||
import { Forms, React, Select } from "@webpack/common";
|
||||
|
||||
import { ISettingElementProps } from ".";
|
||||
|
||||
export function SettingBooleanComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
|
||||
const def = pluginSettings[id] ?? option.default;
|
||||
|
||||
const [state, setState] = React.useState(def ?? false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
onError(error !== null);
|
||||
}, [error]);
|
||||
|
||||
const options = [
|
||||
{ label: "Enabled", value: true, default: def === true },
|
||||
{ label: "Disabled", value: false, default: typeof def === "undefined" || def === false },
|
||||
];
|
||||
|
||||
function handleChange(newValue: boolean): void {
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
if (typeof isValid === "string") setError(isValid);
|
||||
else if (!isValid) setError("Invalid input provided.");
|
||||
else {
|
||||
setError(null);
|
||||
setState(newValue);
|
||||
onChange(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<Select
|
||||
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
||||
options={options}
|
||||
placeholder={option.placeholder ?? "Select an option"}
|
||||
maxVisibleItems={5}
|
||||
closeOnSelect={true}
|
||||
select={handleChange}
|
||||
isSelected={v => v === state}
|
||||
serialize={v => String(v)}
|
||||
{...option.componentProps}
|
||||
/>
|
||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||
</Forms.FormSection>
|
||||
);
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { PluginOptionComponent } from "@utils/types";
|
||||
|
||||
import { ISettingElementProps } from ".";
|
||||
|
||||
export function SettingCustomComponent({ option, onChange, onError }: ISettingElementProps<PluginOptionComponent>) {
|
||||
return option.component({ setValue: onChange, setError: onError, option });
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { OptionType, PluginOptionNumber } from "@utils/types";
|
||||
import { Forms, React, TextInput } from "@webpack/common";
|
||||
|
||||
import { ISettingElementProps } from ".";
|
||||
|
||||
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||
|
||||
export function SettingNumericComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
|
||||
function serialize(value: any) {
|
||||
if (option.type === OptionType.BIGINT) return BigInt(value);
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
const [state, setState] = React.useState<any>(`${pluginSettings[id] ?? option.default ?? 0}`);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
onError(error !== null);
|
||||
}, [error]);
|
||||
|
||||
function handleChange(newValue) {
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
if (typeof isValid === "string") setError(isValid);
|
||||
else if (!isValid) setError("Invalid input provided.");
|
||||
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
||||
setState(`${Number.MAX_SAFE_INTEGER}`);
|
||||
onChange(serialize(newValue));
|
||||
} else {
|
||||
setState(newValue);
|
||||
onChange(serialize(newValue));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<TextInput
|
||||
type="number"
|
||||
pattern="-?[0-9]+"
|
||||
value={state}
|
||||
onChange={handleChange}
|
||||
placeholder={option.placeholder ?? "Enter a number"}
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
{...option.componentProps}
|
||||
/>
|
||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||
</Forms.FormSection>
|
||||
);
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { PluginOptionSelect } from "@utils/types";
|
||||
import { Forms, React, Select } from "@webpack/common";
|
||||
|
||||
import { ISettingElementProps } from ".";
|
||||
|
||||
export function SettingSelectComponent({ option, pluginSettings, definedSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
|
||||
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
|
||||
|
||||
const [state, setState] = React.useState<any>(def ?? null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
onError(error !== null);
|
||||
}, [error]);
|
||||
|
||||
function handleChange(newValue) {
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
if (typeof isValid === "string") setError(isValid);
|
||||
else if (!isValid) setError("Invalid input provided.");
|
||||
else {
|
||||
setState(newValue);
|
||||
onChange(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<Select
|
||||
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
||||
options={option.options}
|
||||
placeholder={option.placeholder ?? "Select an option"}
|
||||
maxVisibleItems={5}
|
||||
closeOnSelect={true}
|
||||
select={handleChange}
|
||||
isSelected={v => v === state}
|
||||
serialize={v => String(v)}
|
||||
{...option.componentProps}
|
||||
/>
|
||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||
</Forms.FormSection>
|
||||
);
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { PluginOptionSlider } from "@utils/types";
|
||||
import { Forms, React, Slider } from "@webpack/common";
|
||||
|
||||
import { ISettingElementProps } from ".";
|
||||
|
||||
export function makeRange(start: number, end: number, step = 1) {
|
||||
const ranges: number[] = [];
|
||||
for (let value = start; value <= end; value += step) {
|
||||
ranges.push(Math.round(value * 100) / 100);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
export function SettingSliderComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) {
|
||||
const def = pluginSettings[id] ?? option.default;
|
||||
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
onError(error !== null);
|
||||
}, [error]);
|
||||
|
||||
function handleChange(newValue: number): void {
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
if (typeof isValid === "string") setError(isValid);
|
||||
else if (!isValid) setError("Invalid input provided.");
|
||||
else {
|
||||
setError(null);
|
||||
onChange(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<Slider
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
markers={option.markers}
|
||||
minValue={option.markers[0]}
|
||||
maxValue={option.markers[option.markers.length - 1]}
|
||||
initialValue={def}
|
||||
onValueChange={handleChange}
|
||||
onValueRender={(v: number) => String(v.toFixed(2))}
|
||||
stickToMarkers={option.stickToMarkers ?? true}
|
||||
{...option.componentProps}
|
||||
/>
|
||||
</Forms.FormSection>
|
||||
);
|
||||
}
|
||||
|
@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { PluginOptionString } from "@utils/types";
|
||||
import { Forms, React, TextInput } from "@webpack/common";
|
||||
|
||||
import { ISettingElementProps } from ".";
|
||||
|
||||
export function SettingTextComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
|
||||
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
onError(error !== null);
|
||||
}, [error]);
|
||||
|
||||
function handleChange(newValue) {
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
if (typeof isValid === "string") setError(isValid);
|
||||
else if (!isValid) setError("Invalid input provided.");
|
||||
else {
|
||||
setState(newValue);
|
||||
onChange(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<TextInput
|
||||
type="text"
|
||||
value={state}
|
||||
onChange={handleChange}
|
||||
placeholder={option.placeholder ?? "Enter a value"}
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
{...option.componentProps}
|
||||
/>
|
||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||
</Forms.FormSection>
|
||||
);
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { DefinedSettings, PluginOptionBase } from "@utils/types";
|
||||
|
||||
export interface ISettingElementProps<T extends PluginOptionBase> {
|
||||
option: T;
|
||||
onChange(newValue: any): void;
|
||||
pluginSettings: {
|
||||
[setting: string]: any;
|
||||
enabled: boolean;
|
||||
};
|
||||
id: string;
|
||||
onError(hasError: boolean): void;
|
||||
definedSettings?: DefinedSettings;
|
||||
}
|
||||
|
||||
export * from "../../Badge";
|
||||
export * from "./SettingBooleanComponent";
|
||||
export * from "./SettingCustomComponent";
|
||||
export * from "./SettingNumericComponent";
|
||||
export * from "./SettingSelectComponent";
|
||||
export * from "./SettingSliderComponent";
|
||||
export * from "./SettingTextComponent";
|
||||
|
@ -1,352 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { showNotice } from "@api/Notices";
|
||||
import { useSettings } from "@api/settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { Badge } from "@components/PluginSettings/components";
|
||||
import PluginModal from "@components/PluginSettings/PluginModal";
|
||||
import { Switch } from "@components/Switch";
|
||||
import { ChangeList } from "@utils/ChangeList";
|
||||
import Logger from "@utils/Logger";
|
||||
import { classes, LazyComponent, useAwaiter } from "@utils/misc";
|
||||
import { openModalLazy } from "@utils/modal";
|
||||
import { Plugin } from "@utils/types";
|
||||
import { findByCode, findByPropsLazy } from "@webpack";
|
||||
import { Alerts, Button, Card, Forms, Margins, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
|
||||
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
||||
|
||||
|
||||
const cl = classNameFactory("vc-plugins-");
|
||||
const logger = new Logger("PluginSettings", "#a6d189");
|
||||
|
||||
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
|
||||
|
||||
const CogWheel = LazyComponent(() => findByCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
|
||||
const InfoIcon = LazyComponent(() => findByCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
|
||||
|
||||
function showErrorToast(message: string) {
|
||||
Toasts.show({
|
||||
message,
|
||||
type: Toasts.Type.FAILURE,
|
||||
id: Toasts.genId(),
|
||||
options: {
|
||||
position: Toasts.Position.BOTTOM
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ReloadRequiredCard({ required }: { required: boolean; }) {
|
||||
return (
|
||||
<Card className={cl("info-card", { "restart-card": required })}>
|
||||
{required ? (
|
||||
<>
|
||||
<Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle>
|
||||
<Forms.FormText className={cl("dep-text")}>
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
||||
plugin: Plugin;
|
||||
disabled: boolean;
|
||||
onRestartNeeded(name: string): void;
|
||||
isNew?: boolean;
|
||||
}
|
||||
|
||||
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
||||
const settings = useSettings([`plugins.${plugin.name}`]).plugins[plugin.name];
|
||||
|
||||
const isEnabled = () => settings.enabled ?? false;
|
||||
|
||||
function openModal() {
|
||||
openModalLazy(async () => {
|
||||
return modalProps => {
|
||||
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function toggleEnabled() {
|
||||
const wasEnabled = isEnabled();
|
||||
|
||||
// If we're enabling a plugin, make sure all deps are enabled recursively.
|
||||
if (!wasEnabled) {
|
||||
const { restartNeeded, failures } = startDependenciesRecursive(plugin);
|
||||
if (failures.length) {
|
||||
logger.error(`Failed to start dependencies for ${plugin.name}: ${failures.join(", ")}`);
|
||||
showNotice("Failed to start dependencies: " + failures.join(", "), "Close", () => null);
|
||||
return;
|
||||
} else if (restartNeeded) {
|
||||
// If any dependencies have patches, don't start the plugin yet.
|
||||
settings.enabled = true;
|
||||
onRestartNeeded(plugin.name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
|
||||
if (plugin.patches) {
|
||||
settings.enabled = !wasEnabled;
|
||||
onRestartNeeded(plugin.name);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the plugin is enabled, but hasn't been started, then we can just toggle it off.
|
||||
if (wasEnabled && !plugin.started) {
|
||||
settings.enabled = !wasEnabled;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = wasEnabled ? stopPlugin(plugin) : startPlugin(plugin);
|
||||
const action = wasEnabled ? "stop" : "start";
|
||||
|
||||
if (!result) {
|
||||
logger.error(`Failed to ${action} plugin ${plugin.name}`);
|
||||
showErrorToast(`Failed to ${action} plugin: ${plugin.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
settings.enabled = !wasEnabled;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex className={cl("card", { "card-disabled": disabled })} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<div className={cl("card-header")}>
|
||||
<Text variant="text-md/bold" className={cl("name")}>
|
||||
{plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||
</Text>
|
||||
<button role="switch" onClick={() => openModal()} className={classes("button-12Fmur", cl("info-button"))}>
|
||||
{plugin.options
|
||||
? <CogWheel />
|
||||
: <InfoIcon width="24" height="24" />}
|
||||
</button>
|
||||
<Switch
|
||||
checked={isEnabled()}
|
||||
onChange={toggleEnabled}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<Text className={cl("note")} variant="text-sm/normal">{plugin.description}</Text>
|
||||
</Flex >
|
||||
);
|
||||
}
|
||||
|
||||
enum SearchStatus {
|
||||
ALL,
|
||||
ENABLED,
|
||||
DISABLED
|
||||
}
|
||||
|
||||
export default ErrorBoundary.wrap(function PluginSettings() {
|
||||
const settings = useSettings();
|
||||
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => void (changes.hasChanges && Alerts.show({
|
||||
title: "Restart required",
|
||||
body: (
|
||||
<>
|
||||
<p>The following plugins require a restart:</p>
|
||||
<div>{changes.map((s, i) => (
|
||||
<>
|
||||
{i > 0 && ", "}
|
||||
{Parser.parse("`" + s + "`")}
|
||||
</>
|
||||
))}</div>
|
||||
</>
|
||||
),
|
||||
confirmText: "Restart now",
|
||||
cancelText: "Later!",
|
||||
onConfirm: () => location.reload()
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const depMap = React.useMemo(() => {
|
||||
const o = {} as Record<string, string[]>;
|
||||
for (const plugin in Plugins) {
|
||||
const deps = Plugins[plugin].dependencies;
|
||||
if (deps) {
|
||||
for (const dep of deps) {
|
||||
o[dep] ??= [];
|
||||
o[dep].push(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}, []);
|
||||
|
||||
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
|
||||
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
||||
|
||||
const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL });
|
||||
|
||||
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
||||
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
|
||||
|
||||
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
|
||||
const enabled = settings.plugins[plugin.name]?.enabled;
|
||||
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
|
||||
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
|
||||
if (!searchValue.value.length) return true;
|
||||
return (
|
||||
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 (
|
||||
<Forms.FormSection className={Margins.marginTop16}>
|
||||
<ReloadRequiredCard required={changes.hasChanges} />
|
||||
|
||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||
Filters
|
||||
</Forms.FormTitle>
|
||||
|
||||
<div className={cl("filter-controls")}>
|
||||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.marginBottom20} />
|
||||
<div className={InputStyles.inputWrapper}>
|
||||
<Select
|
||||
className={InputStyles.inputDefault}
|
||||
options={[
|
||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||
{ label: "Show Disabled", value: SearchStatus.DISABLED }
|
||||
]}
|
||||
serialize={String}
|
||||
select={onStatusChange}
|
||||
isSelected={v => v === searchValue.status}
|
||||
closeOnSelect={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
|
||||
|
||||
<div className={cl("grid")}>
|
||||
{plugins}
|
||||
</div>
|
||||
|
||||
<Forms.FormDivider className={Margins.marginTop20} />
|
||||
|
||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||
Required Plugins
|
||||
</Forms.FormTitle>
|
||||
<div className={cl("grid")}>
|
||||
{requiredPlugins}
|
||||
</div>
|
||||
</Forms.FormSection >
|
||||
);
|
||||
}, {
|
||||
message: "Failed to render the Plugin Settings. If this persists, try using the installer to reinstall!",
|
||||
onError: handleComponentFailed,
|
||||
});
|
||||
|
||||
function makeDependencyList(deps: string[]) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
||||
{deps.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
@ -1,138 +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/>.
|
||||
*/
|
||||
|
||||
.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);
|
||||
}
|
173
src/components/Settings.tsx
Normal file
173
src/components/Settings.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
import { classes, humanFriendlyJoin, useAwaiter } from "../utils/misc";
|
||||
import Plugins from 'plugins';
|
||||
import { useSettings } from "../api/settings";
|
||||
import IpcEvents from "../utils/IpcEvents";
|
||||
|
||||
import { Button, Switch, Forms, React, Margins, Toasts, Alerts, Parser } from "../webpack/common";
|
||||
import ErrorBoundary from "./ErrorBoundary";
|
||||
import { startPlugin } from "../plugins";
|
||||
import { stopPlugin } from '../plugins/index';
|
||||
import { Flex } from './Flex';
|
||||
import { ChangeList } from '../utils/ChangeList';
|
||||
|
||||
function showErrorToast(message: string) {
|
||||
Toasts.show({
|
||||
message,
|
||||
type: Toasts.Type.FAILURE,
|
||||
id: Toasts.genId(),
|
||||
options: {
|
||||
position: Toasts.Position.BOTTOM
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default ErrorBoundary.wrap(function Settings() {
|
||||
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading...");
|
||||
const settings = useSettings();
|
||||
const changes = React.useMemo(() => new ChangeList<string>, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => void (changes.hasChanges && Alerts.show({
|
||||
title: "Restart required",
|
||||
body: (
|
||||
<>
|
||||
<p>The following plugins require a restart:</p>
|
||||
<div>{changes.map((s, i) => (
|
||||
<>
|
||||
{i > 0 && ", "}
|
||||
{Parser.parse('`' + s + '`')}
|
||||
</>
|
||||
))}</div>
|
||||
</>
|
||||
),
|
||||
confirmText: "Restart now",
|
||||
cancelText: "Later!",
|
||||
onConfirm: () => location.reload()
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const depMap = React.useMemo(() => {
|
||||
const o = {} as Record<string, string[]>;
|
||||
for (const plugin in Plugins) {
|
||||
const deps = Plugins[plugin].dependencies;
|
||||
if (deps) {
|
||||
for (const dep of deps) {
|
||||
o[dep] ??= [];
|
||||
o[dep].push(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}, []);
|
||||
|
||||
const sortedPlugins = React.useMemo(() => Object.values(Plugins).sort((a, b) => a.name.localeCompare(b.name)), []);
|
||||
|
||||
return (
|
||||
<Forms.FormSection tag="h1" title="Vencord">
|
||||
<Forms.FormTitle tag="h5">
|
||||
Settings
|
||||
</Forms.FormTitle>
|
||||
|
||||
<Forms.FormText>
|
||||
SettingsDir: <code style={{ userSelect: 'text', cursor: 'text' }}>{settingsDir}</code>
|
||||
</Forms.FormText>
|
||||
|
||||
<Flex className={classes(Margins.marginBottom20)}>
|
||||
<Button
|
||||
onClick={() => window.DiscordNative.app.relaunch()}
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.GREEN}
|
||||
>
|
||||
Reload
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDirPending}
|
||||
>
|
||||
Launch Directory
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_PATH, settingsDir, "quickCss.css")}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDir === "Loading..."}
|
||||
>
|
||||
Open QuickCSS File
|
||||
</Button>
|
||||
</Flex>
|
||||
<Forms.FormDivider />
|
||||
<Forms.FormTitle tag="h5">Settings</Forms.FormTitle>
|
||||
<Switch
|
||||
value={settings.useQuickCss}
|
||||
onChange={(v: boolean) => settings.useQuickCss = v}
|
||||
note="Enable QuickCSS"
|
||||
>
|
||||
Use QuickCss
|
||||
</Switch>
|
||||
<Switch
|
||||
value={settings.notifyAboutUpdates}
|
||||
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||
note="Shows a Toast on StartUp"
|
||||
>
|
||||
Get notified about new Updates
|
||||
</Switch>
|
||||
<Switch
|
||||
value={settings.unsafeRequire}
|
||||
onChange={(v: boolean) => settings.unsafeRequire = v}
|
||||
note="Enables VencordNative.require. Useful for testing, very bad for security. Leave this off unless you need it."
|
||||
>
|
||||
Enable Unsafe Require
|
||||
</Switch>
|
||||
|
||||
<Forms.FormDivider />
|
||||
|
||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||
Plugins
|
||||
</Forms.FormTitle>
|
||||
|
||||
{sortedPlugins.map(p => {
|
||||
const enabledDependants = depMap[p.name]?.filter(d => settings.plugins[d].enabled);
|
||||
const dependency = enabledDependants?.length;
|
||||
|
||||
return (
|
||||
<Switch
|
||||
disabled={p.required || dependency}
|
||||
key={p.name}
|
||||
value={settings.plugins[p.name].enabled || p.required || dependency}
|
||||
onChange={v => {
|
||||
settings.plugins[p.name].enabled = v;
|
||||
if (v) {
|
||||
p.dependencies?.forEach(d => {
|
||||
settings.plugins[d].enabled = true;
|
||||
if (!Plugins[d].started && !stopPlugin) {
|
||||
settings.plugins[p.name].enabled = false;
|
||||
showErrorToast(`Failed to start dependency ${d}. Check the console for more info.`);
|
||||
}
|
||||
});
|
||||
if (!p.started && !startPlugin(p)) {
|
||||
showErrorToast(`Failed to start plugin ${p.name}. Check the console for more info.`);
|
||||
}
|
||||
} else {
|
||||
if (p.started && !stopPlugin(p)) {
|
||||
showErrorToast(`Failed to stop plugin ${p.name}. Check the console for more info.`);
|
||||
}
|
||||
}
|
||||
if (p.patches) changes.handleChange(p.name);
|
||||
}}
|
||||
note={p.description}
|
||||
tooltipNote={
|
||||
p.required ?
|
||||
"This plugin is required. Thus you cannot disable it."
|
||||
: dependency ?
|
||||
`${humanFriendlyJoin(enabledDependants)} ${enabledDependants.length === 1 ? "depends" : "depend"} on this plugin. Thus you cannot disable it.`
|
||||
: null
|
||||
}
|
||||
>
|
||||
{p.name}
|
||||
</Switch>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Forms.FormSection >
|
||||
);
|
||||
});
|
@ -1,3 +0,0 @@
|
||||
.vc-switch-slider {
|
||||
transition: 100ms transform ease-in-out;
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
/*
|
||||
* 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>
|
||||
);
|
||||
}
|
@ -1,32 +1,12 @@
|
||||
/*
|
||||
* 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 gitHash from "git-hash";
|
||||
import { changes, checkForUpdates, getRepo, rebuild, update, UpdateLogger, updateError } from '../utils/updater';
|
||||
import { React, Forms, Button, Margins, Alerts, Card, Parser, Toasts } from '../webpack/common';
|
||||
import { Flex } from "./Flex";
|
||||
import { useAwaiter } from '../utils/misc';
|
||||
import { Link } from "./Link";
|
||||
import ErrorBoundary from "./ErrorBoundary";
|
||||
import { ErrorCard } from "./ErrorCard";
|
||||
|
||||
import { useSettings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { Link } from "@components/Link";
|
||||
import { classes, useAwaiter } from "@utils/misc";
|
||||
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
||||
import { Alerts, Button, Card, Forms, Margins, Parser, React, Switch, Toasts } from "@webpack/common";
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
|
||||
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
||||
return async () => {
|
||||
@ -63,44 +43,35 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
|
||||
dispatcher(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
interface CommonProps {
|
||||
repo: string;
|
||||
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; }) {
|
||||
return (
|
||||
<Card style={{ padding: ".5em" }}>
|
||||
{updates.map(({ hash, author, message }) => (
|
||||
<div>
|
||||
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
|
||||
<span style={{
|
||||
marginLeft: "0.5em",
|
||||
color: "var(--text-normal)"
|
||||
}}>{message} - {author}</span>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function Updatable(props: CommonProps) {
|
||||
const [updates, setUpdates] = React.useState(changes);
|
||||
export default ErrorBoundary.wrap(function Updater() {
|
||||
const [repo, err, repoPending] = useAwaiter(getRepo, "Loading...");
|
||||
const [isChecking, setIsChecking] = React.useState(false);
|
||||
const [isUpdating, setIsUpdating] = React.useState(false);
|
||||
const [updates, setUpdates] = React.useState(changes);
|
||||
|
||||
const isOutdated = (updates?.length ?? 0) > 0;
|
||||
React.useEffect(() => {
|
||||
if (err)
|
||||
UpdateLogger.error("Failed to retrieve repo", err);
|
||||
}, [err]);
|
||||
|
||||
const isOutdated = updates?.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Forms.FormSection tag="h1" title="Vencord Updater">
|
||||
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
|
||||
|
||||
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
|
||||
<Link href={repo}>
|
||||
{repo.split("/").slice(-2).join("/")}
|
||||
</Link>
|
||||
)} ({gitHash})</Forms.FormText>
|
||||
|
||||
<Forms.FormDivider />
|
||||
|
||||
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
||||
|
||||
{!updates && updateError ? (
|
||||
<>
|
||||
<Forms.FormText>Failed to check updates. Check the console for more info</Forms.FormText>
|
||||
@ -108,15 +79,31 @@ function Updatable(props: CommonProps) {
|
||||
<p>{updateError.stderr || updateError.stdout || "An unknown error occurred"}</p>
|
||||
</ErrorCard>
|
||||
</>
|
||||
) : (
|
||||
) :
|
||||
(
|
||||
<Forms.FormText className={Margins.marginBottom8}>
|
||||
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
|
||||
</Forms.FormText>
|
||||
)
|
||||
}
|
||||
|
||||
{isOutdated && (
|
||||
<Card style={{ padding: ".5em" }}>
|
||||
{updates.map(({ hash, author, message }) => (
|
||||
<div>
|
||||
<Link href={`${repo}/commit/${hash}`} disabled={repoPending}>
|
||||
<code>{hash}</code>
|
||||
</Link>
|
||||
<span style={{
|
||||
marginLeft: "0.5em",
|
||||
color: "var(--text-normal)"
|
||||
}}>{message} - {author}</span>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{isOutdated && <Changes updates={updates} {...props} />}
|
||||
|
||||
<Flex className={classes(Margins.marginBottom8, Margins.marginTop8)}>
|
||||
<Flex className={`${Margins.marginBottom8} ${Margins.marginTop8}`}>
|
||||
{isOutdated && <Button
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={isUpdating || isChecking}
|
||||
@ -168,73 +155,6 @@ function Updatable(props: CommonProps) {
|
||||
Check for Updates
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Newer(props: CommonProps) {
|
||||
return (
|
||||
<>
|
||||
<Forms.FormText className={Margins.marginBottom8}>
|
||||
Your local copy has more recent commits. Please stash or reset them.
|
||||
</Forms.FormText>
|
||||
<Changes {...props} updates={changes} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Updater() {
|
||||
const settings = useSettings(["notifyAboutUpdates", "autoUpdate"]);
|
||||
|
||||
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
||||
|
||||
React.useEffect(() => {
|
||||
if (err)
|
||||
UpdateLogger.error("Failed to retrieve repo", err);
|
||||
}, [err]);
|
||||
|
||||
const commonProps: CommonProps = {
|
||||
repo,
|
||||
repoPending
|
||||
};
|
||||
|
||||
return (
|
||||
<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.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
|
||||
<Link href={repo}>
|
||||
{repo.split("/").slice(-2).join("/")}
|
||||
</Link>
|
||||
)} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText>
|
||||
|
||||
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
||||
|
||||
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
||||
|
||||
{isNewer ? <Newer {...commonProps} /> : <Updatable {...commonProps} />}
|
||||
</Forms.FormSection >
|
||||
);
|
||||
}
|
||||
|
||||
export default IS_WEB ? null : ErrorBoundary.wrap(Updater, {
|
||||
message: "Failed to render the Updater. If this persists, try using the installer to reinstall!",
|
||||
onError: handleComponentFailed,
|
||||
});
|
@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { classes } from "@utils/misc";
|
||||
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
||||
import { Button, Card, Forms, Margins, Text } from "@webpack/common";
|
||||
|
||||
function BackupRestoreTab() {
|
||||
return (
|
||||
<Forms.FormSection title="Settings Sync" className={Margins.marginTop16}>
|
||||
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
||||
<Flex flexDirection="column">
|
||||
<strong>Warning</strong>
|
||||
<span>Importing a settings file will overwrite your current settings.</span>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
||||
You can import and export your Vencord settings as a JSON file.
|
||||
This allows you to easily transfer your settings to another device,
|
||||
or recover your settings after reinstalling Vencord or Discord.
|
||||
</Text>
|
||||
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
||||
Settings Export contains:
|
||||
<ul>
|
||||
<li>— Custom QuickCSS</li>
|
||||
<li>— Plugin Settings</li>
|
||||
</ul>
|
||||
</Text>
|
||||
<Flex>
|
||||
<Button
|
||||
onClick={() => uploadSettingsBackup()}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Import Settings
|
||||
</Button>
|
||||
<Button
|
||||
onClick={downloadSettingsBackup}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Export Settings
|
||||
</Button>
|
||||
</Flex>
|
||||
</Forms.FormSection>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorBoundary.wrap(BackupRestoreTab);
|
@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import PluginSettings from "@components/PluginSettings";
|
||||
|
||||
export default ErrorBoundary.wrap(PluginSettings);
|
@ -1,132 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useSettings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Link } from "@components/Link";
|
||||
import { useAwaiter } from "@utils/misc";
|
||||
import { findLazy } from "@webpack";
|
||||
import { Card, Forms, Margins, React, TextArea } from "@webpack/common";
|
||||
|
||||
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
||||
|
||||
function Validator({ link }: { link: string; }) {
|
||||
const [res, err, pending] = useAwaiter(() => fetch(link).then(res => {
|
||||
if (res.status > 300) throw `${res.status} ${res.statusText}`;
|
||||
const contentType = res.headers.get("Content-Type");
|
||||
if (!contentType?.startsWith("text/css") && !contentType?.startsWith("text/plain"))
|
||||
throw "Not a CSS file. Remember to use the raw link!";
|
||||
|
||||
return "Okay!";
|
||||
}));
|
||||
|
||||
const text = pending
|
||||
? "Checking..."
|
||||
: err
|
||||
? `Error: ${err instanceof Error ? err.message : String(err)}`
|
||||
: "Valid!";
|
||||
|
||||
return <Forms.FormText style={{
|
||||
color: pending ? "var(--text-muted)" : err ? "var(--text-danger)" : "var(--text-positive)"
|
||||
}}>{text}</Forms.FormText>;
|
||||
}
|
||||
|
||||
function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
||||
if (!themeLinks.length) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Forms.FormTitle className={Margins.marginTop20} tag="h5">Validator</Forms.FormTitle>
|
||||
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
||||
<div>
|
||||
{themeLinks.map(link => (
|
||||
<Card style={{
|
||||
padding: ".5em",
|
||||
marginBottom: ".5em",
|
||||
marginTop: ".5em"
|
||||
}} key={link}>
|
||||
<Forms.FormTitle tag="h5" style={{
|
||||
overflowWrap: "break-word"
|
||||
}}>
|
||||
{link}
|
||||
</Forms.FormTitle>
|
||||
<Validator link={link} />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorBoundary.wrap(function () {
|
||||
const settings = useSettings();
|
||||
const [themeText, setThemeText] = React.useState(settings.themeLinks.join("\n"));
|
||||
|
||||
function onBlur() {
|
||||
settings.themeLinks = [...new Set(
|
||||
themeText
|
||||
.trim()
|
||||
.split(/\n+/)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
)];
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="vc-settings-card">
|
||||
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
|
||||
<Forms.FormText>One link per line</Forms.FormText>
|
||||
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
|
||||
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
||||
<div style={{ marginBottom: ".5em" }}>
|
||||
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
||||
BetterDiscord Themes
|
||||
</Link>
|
||||
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
|
||||
</div>
|
||||
<Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText>
|
||||
<Forms.FormText>In the GitHub repository of your theme, find X.theme.css / X.css, click on it, then click the "Raw" button</Forms.FormText>
|
||||
<Forms.FormText>
|
||||
If the theme has configuration that requires you to edit the file:
|
||||
<ul>
|
||||
<li>• Make a <Link href="https://github.com/signup">GitHub</Link> account</li>
|
||||
<li>• Click the fork button on the top right</li>
|
||||
<li>• Edit the file</li>
|
||||
<li>• Use the link to your own repository instead</li>
|
||||
</ul>
|
||||
</Forms.FormText>
|
||||
</Card>
|
||||
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
|
||||
<TextArea
|
||||
style={{
|
||||
padding: ".5em",
|
||||
border: "1px solid var(--background-modifier-accent)"
|
||||
}}
|
||||
value={themeText}
|
||||
onChange={e => setThemeText(e.currentTarget.value)}
|
||||
className={TextAreaProps.textarea}
|
||||
placeholder="Theme Links"
|
||||
spellCheck={false}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<Validators themeLinks={settings.themeLinks} />
|
||||
</>
|
||||
);
|
||||
});
|
@ -1,225 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
import { useSettings } from "@api/settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import DonateButton from "@components/DonateButton";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity, useAwaiter } from "@utils/misc";
|
||||
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
|
||||
|
||||
const cl = classNameFactory("vc-settings-");
|
||||
|
||||
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
|
||||
const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png";
|
||||
|
||||
type KeysOfType<Object, Type> = {
|
||||
[K in keyof Object]: Object[K] extends Type ? K : never;
|
||||
}[keyof Object];
|
||||
|
||||
function VencordSettings() {
|
||||
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
|
||||
fallbackValue: "Loading..."
|
||||
});
|
||||
const settings = useSettings();
|
||||
const notifSettings = settings.notifications;
|
||||
|
||||
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
|
||||
|
||||
const isWindows = navigator.platform.toLowerCase().startsWith("win");
|
||||
|
||||
const Switches: Array<false | {
|
||||
key: KeysOfType<typeof settings, boolean>;
|
||||
title: string;
|
||||
note: string;
|
||||
}> =
|
||||
[
|
||||
{
|
||||
key: "useQuickCss",
|
||||
title: "Enable Custom CSS",
|
||||
note: "Loads your Custom CSS"
|
||||
},
|
||||
!IS_WEB && {
|
||||
key: "enableReactDevtools",
|
||||
title: "Enable React Developer Tools",
|
||||
note: "Requires a full restart"
|
||||
},
|
||||
!IS_WEB && !isWindows && {
|
||||
key: "frameless",
|
||||
title: "Disable the window frame",
|
||||
note: "Requires a full restart"
|
||||
},
|
||||
!IS_WEB && {
|
||||
key: "transparent",
|
||||
title: "Enable window transparency",
|
||||
note: "Requires a full restart"
|
||||
},
|
||||
!IS_WEB && isWindows && {
|
||||
key: "winCtrlQ",
|
||||
title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
|
||||
note: "Requires a full restart"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DonateCard image={donateImage} />
|
||||
<Forms.FormSection title="Quick Actions">
|
||||
<Card className={cl("quick-actions-card")}>
|
||||
{IS_WEB ? (
|
||||
<Button
|
||||
onClick={() => require("../Monaco").launchMonacoEditor()}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDir === "Loading..."}>
|
||||
Open QuickCSS File
|
||||
</Button>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
onClick={() => window.DiscordNative.app.relaunch()}
|
||||
size={Button.Sizes.SMALL}>
|
||||
Restart Client
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_MONACO_EDITOR)}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDir === "Loading..."}>
|
||||
Open QuickCSS File
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDirPending}>
|
||||
Open Settings Folder
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/Vendicated/Vencord")}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDirPending}>
|
||||
Open in GitHub
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Card>
|
||||
</Forms.FormSection>
|
||||
|
||||
<Forms.FormDivider />
|
||||
|
||||
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
|
||||
<Forms.FormText className={Margins.bottom20}>
|
||||
Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
|
||||
</Forms.FormText>
|
||||
{Switches.map(s => s && (
|
||||
<Switch
|
||||
key={s.key}
|
||||
value={settings[s.key]}
|
||||
onChange={v => settings[s.key] = v}
|
||||
note={s.note}
|
||||
>
|
||||
{s.title}
|
||||
</Switch>
|
||||
))}
|
||||
</Forms.FormSection>
|
||||
|
||||
|
||||
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
|
||||
{notifSettings.useNative !== "never" && Notification.permission === "denied" && (
|
||||
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
|
||||
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
|
||||
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
|
||||
</ErrorCard>
|
||||
)}
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
Some plugins may show you notifications. These come in two styles:
|
||||
<ul>
|
||||
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
|
||||
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
|
||||
</ul>
|
||||
</Forms.FormText>
|
||||
<Select
|
||||
placeholder="Notification Style"
|
||||
options={[
|
||||
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
|
||||
{ label: "Always use Desktop notifications", value: "always" },
|
||||
{ label: "Always use Vencord notifications", value: "never" },
|
||||
]satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
|
||||
closeOnSelect={true}
|
||||
select={v => notifSettings.useNative = v}
|
||||
isSelected={v => v === notifSettings.useNative}
|
||||
serialize={identity}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
|
||||
<Select
|
||||
isDisabled={notifSettings.useNative === "always"}
|
||||
placeholder="Notification Position"
|
||||
options={[
|
||||
{ label: "Bottom Right", value: "bottom-right", default: true },
|
||||
{ label: "Top Right", value: "top-right" },
|
||||
]satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
|
||||
select={v => notifSettings.position = v}
|
||||
isSelected={v => v === notifSettings.position}
|
||||
serialize={identity}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
|
||||
<Slider
|
||||
disabled={notifSettings.useNative === "always"}
|
||||
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
|
||||
minValue={0}
|
||||
maxValue={20_000}
|
||||
initialValue={notifSettings.timeout}
|
||||
onValueChange={v => notifSettings.timeout = v}
|
||||
onValueRender={v => (v / 1000).toFixed(2) + "s"}
|
||||
onMarkerRender={v => (v / 1000) + "s"}
|
||||
stickToMarkers={false}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
interface DonateCardProps {
|
||||
image: string;
|
||||
}
|
||||
|
||||
function DonateCard({ image }: DonateCardProps) {
|
||||
return (
|
||||
<Card className={cl("card", "donate")}>
|
||||
<div>
|
||||
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle>
|
||||
<Forms.FormText>Please consider supporting the development of Vencord by donating!</Forms.FormText>
|
||||
<DonateButton style={{ transform: "translateX(-1em)" }} />
|
||||
</div>
|
||||
<img
|
||||
role="presentation"
|
||||
src={image}
|
||||
alt=""
|
||||
height={128}
|
||||
style={{ marginLeft: "auto", transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : "" }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorBoundary.wrap(VencordSettings);
|
@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./settingsStyles.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { Forms, SettingsRouter, Text } from "@webpack/common";
|
||||
|
||||
import BackupRestoreTab from "./BackupRestoreTab";
|
||||
import PluginsTab from "./PluginsTab";
|
||||
import ThemesTab from "./ThemesTab";
|
||||
import Updater from "./Updater";
|
||||
import VencordSettings from "./VencordTab";
|
||||
|
||||
const cl = classNameFactory("vc-settings-");
|
||||
|
||||
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');
|
||||
|
||||
interface SettingsProps {
|
||||
tab: string;
|
||||
}
|
||||
|
||||
interface SettingsTab {
|
||||
name: string;
|
||||
component?: React.ComponentType;
|
||||
}
|
||||
|
||||
const SettingsTabs: Record<string, SettingsTab> = {
|
||||
VencordSettings: { name: "Vencord", component: () => <VencordSettings /> },
|
||||
VencordPlugins: { name: "Plugins", component: () => <PluginsTab /> },
|
||||
VencordThemes: { name: "Themes", component: () => <ThemesTab /> },
|
||||
VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false
|
||||
VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> },
|
||||
};
|
||||
|
||||
if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater />;
|
||||
|
||||
function Settings(props: SettingsProps) {
|
||||
const { tab = "VencordSettings" } = props;
|
||||
|
||||
const CurrentTab = SettingsTabs[tab]?.component;
|
||||
|
||||
return <Forms.FormSection>
|
||||
<Text variant="heading-md/normal" tag="h2">Vencord Settings</Text>
|
||||
|
||||
<TabBar
|
||||
type={TabBar.Types.TOP}
|
||||
look={TabBar.Looks.BRAND}
|
||||
className={cl("tab-bar")}
|
||||
selectedItem={tab}
|
||||
onItemSelect={SettingsRouter.open}
|
||||
>
|
||||
{Object.entries(SettingsTabs).map(([key, { name, component }]) => {
|
||||
if (!component) return null;
|
||||
return <TabBar.Item
|
||||
id={key}
|
||||
className={cl("tab-bar-item")}
|
||||
key={key}>
|
||||
{name}
|
||||
</TabBar.Item>;
|
||||
})}
|
||||
</TabBar>
|
||||
<Forms.FormDivider />
|
||||
{CurrentTab && <CurrentTab />}
|
||||
</Forms.FormSection >;
|
||||
}
|
||||
|
||||
export default function (props: SettingsProps) {
|
||||
return <ErrorBoundary>
|
||||
<Settings tab={props.tab} />
|
||||
</ErrorBoundary>;
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
.vc-settings-tab-bar {
|
||||
margin-top: 20px;
|
||||
margin-bottom: -2px;
|
||||
border-bottom: 2px solid var(--background-modifier-accent);
|
||||
}
|
||||
|
||||
.vc-settings-tab-bar-item {
|
||||
margin-right: 32px;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.vc-settings-quick-actions-card {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
flex-flow: row wrap;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.vc-settings-donate {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.vc-settings-card {
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.vc-backup-restore-card {
|
||||
background-color: var(--info-warning-background);
|
||||
border-color: var(--info-warning-foreground);
|
||||
color: var(--info-warning-text);
|
||||
margin-top: 0;
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { isOutdated, rebuild, update } from "@utils/updater";
|
||||
|
||||
export async function handleComponentFailed() {
|
||||
if (isOutdated) {
|
||||
setImmediate(async () => {
|
||||
const wantsUpdate = confirm(
|
||||
"Uh Oh! Failed to render this Page." +
|
||||
" However, there is an update available that might fix it." +
|
||||
" Would you like to update and restart now?"
|
||||
);
|
||||
if (wantsUpdate) {
|
||||
try {
|
||||
await update();
|
||||
await rebuild();
|
||||
if (IS_WEB)
|
||||
location.reload();
|
||||
else
|
||||
DiscordNative.app.relaunch();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("That also failed :( Try updating or reinstalling with the installer!");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,21 +1,2 @@
|
||||
/*
|
||||
* 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 { default as PatchHelper } from "./PatchHelper";
|
||||
export { default as PluginSettings } from "./PluginSettings";
|
||||
export { default as VencordSettings } from "./VencordSettings";
|
||||
export { default as Settings } from "./Settings";
|
||||
export { default as Updater } from "./Updater";
|
||||
|
@ -1,52 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>QuickCss Editor</title>
|
||||
<link rel="stylesheet" data-name="vs/editor/editor.main"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.min.css">
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/loader.min.js"></script>
|
||||
|
||||
<script>
|
||||
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs' } });
|
||||
require(["vs/editor/editor.main"], () => {
|
||||
getCurrentCss().then(css => {
|
||||
var editor = monaco.editor.create(document.getElementById('container'), {
|
||||
value: css,
|
||||
language: 'css',
|
||||
theme: getTheme(),
|
||||
});
|
||||
editor.onDidChangeModelContent(() =>
|
||||
setCss(editor.getValue())
|
||||
);
|
||||
window.addEventListener("resize", () => {
|
||||
// make monaco re-layout
|
||||
editor.layout();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user