Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0bfe6f069e |
114
.eslintrc.json
114
.eslintrc.json
@ -1,114 +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) 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/>.",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
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",
|
|
||||||
"consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }],
|
|
||||||
"dot-notation": "error",
|
|
||||||
"no-useless-escape": "error",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
13
.github/FUNDING.yml
vendored
13
.github/FUNDING.yml
vendored
@ -1,13 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: Vendicated
|
|
||||||
patreon: Aliucord
|
|
||||||
open_collective: # Replace with a single Open Collective username
|
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
otechie: # Replace with a single Otechie username
|
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
64
.github/workflows/build.yml
vendored
64
.github/workflows/build.yml
vendored
@ -1,64 +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: Sign firefox extension
|
|
||||||
run: |
|
|
||||||
pnpx web-ext sign --api-key $WEBEXT_USER --api-secret $WEBEXT_SECRET --channel=unlisted
|
|
||||||
env:
|
|
||||||
WEBEXT_USER: ${{ secrets.WEBEXT_USER }}
|
|
||||||
WEBEXT_SECRET: ${{ secrets.WEBEXT_SECRET }}
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: pnpm build --standalone
|
|
||||||
|
|
||||||
- name: Rename extensions for more user friendliness
|
|
||||||
run: |
|
|
||||||
mv dist/*.xpi dist/Vencord-for-Firefox.xpi
|
|
||||||
mv dist/extension-v3.zip dist/Vencord-for-Chrome-and-Edge.zip
|
|
||||||
rm -rf dist/extension-v2-unpacked
|
|
||||||
|
|
||||||
- name: Get some values needed for the release
|
|
||||||
id: release_values
|
|
||||||
run: |
|
|
||||||
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Upload Devbuild
|
|
||||||
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 }}
|
|
43
.github/workflows/reportBrokenPlugins.yml
vendored
43
.github/workflows/reportBrokenPlugins.yml
vendored
@ -1,43 +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 }}
|
|
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
|
|
21
.gitignore
vendored
21
.gitignore
vendored
@ -1,23 +1,2 @@
|
|||||||
dist
|
dist
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
*.exe
|
|
||||||
vencord_installer
|
|
||||||
|
|
||||||
.idea
|
|
||||||
|
|
||||||
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/
|
|
||||||
|
10
.vscode/extensions.json
vendored
10
.vscode/extensions.json
vendored
@ -1,11 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [ "EditorConfig.EditorConfig" ]
|
||||||
"EditorConfig.EditorConfig",
|
|
||||||
"pmneo.tsimporter",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"formulahendry.auto-rename-tag",
|
|
||||||
"GregorBiswanger.json2ts",
|
|
||||||
"eamodio.gitlens",
|
|
||||||
"kamikillerto.vscode-colorize"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
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!
|
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
|
## Style Guide
|
||||||
|
|
||||||
- This project has a very minimal .editorconfig. Make sure your editor supports this!
|
- 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
|
- 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
|
or React Component, in which case they should be PascalCase
|
||||||
|
|
||||||
|
|
||||||
## Contributing a Plugin
|
## Contributing a Plugin
|
||||||
|
|
||||||
Because plugins modify code directly, incompatibilities are a problem.
|
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!
|
Follow the below guide to make your first plugin!
|
||||||
|
|
||||||
|
|
||||||
### Finding the right module to patch
|
### 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.
|
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.
|
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:
|
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
|
- 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).
|
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 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
|
- Now either test regexes on this string in the console or use a tool like https://regex101.com
|
||||||
|
|
||||||
Also pay attention to the following:
|
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
|
- 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.
|
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:
|
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.
|
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
|
This includes spaces, tabs and especially newlines
|
||||||
|
|
||||||
---
|
___
|
||||||
|
|
||||||
And that's it! Now open a Pull Request with your Plugin
|
And that's it! Now open a Pull Request with your Plugin
|
||||||
|
54
README.md
54
README.md
@ -1,58 +1,34 @@
|
|||||||
# Vencord
|
# Vencord
|
||||||
|
|
||||||
A Discord client mod that does things differently
|
My own Discord Desktop mod :)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Super easy to install, no git or node or anything else required
|
- Works on Discord's latest swc update that breaks all other mods
|
||||||
- Many plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
- Proper context isolation -> Works in newer Electron versions (Confirmed working on versions 13-21)
|
||||||
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, custom slash commands, ShowHiddenChannels
|
- 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!)
|
||||||
- Browser Support: Run Vencord in your Browser via extension or UserScript
|
- Experiments
|
||||||
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
- 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
|
||||||
- Works in all Electron versions (Confirmed working on versions 13-23)
|
- Many Useful™ plugins - [List](https://github.com/Vendicated/Vencord/tree/main/src/plugins)
|
||||||
|
|
||||||
## Installing / Uninstalling
|
## Installing
|
||||||
|
|
||||||
If you're just a normal user, use [our simple gui installer!](https://github.com/Vendicated/VencordInstaller#usage)
|
|
||||||
|
|
||||||
If you're a power user who wants to contribute and make plugins or just want to build from source and install manually, read [Megu's Installation Guide!](docs/1_INSTALLING.md)
|
|
||||||
|
|
||||||
## Installing on Browser
|
|
||||||
|
|
||||||
Install the browser extension for [](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip), [](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Firefox.xpi) or [UserScript](https://github.com/Vendicated/Vencord/releases/download/devbuild/Vencord.user.js). Please note that they aren't automatically updated for now, so you will regularely have to reinstall it.
|
|
||||||
|
|
||||||
|
|
||||||
You may also build them from source, to do that do the same steps as in the manual regular install method,
|
|
||||||
except run `pnpm buildWeb` instead of `pnpm build`, and your outputs will be in the dist folder
|
|
||||||
|
|
||||||
|
Make sure you have NodeJs and git installed. I will be using pnpm, you can use npm instead
|
||||||
```sh
|
```sh
|
||||||
pnpm buildWeb
|
git clone https://github.com/Vendicated/Vencord
|
||||||
|
cd Vencord
|
||||||
|
pnpm i
|
||||||
|
pnpm build
|
||||||
```
|
```
|
||||||
|
The builds are now in the dist/ folder (Vencord/dist).
|
||||||
You will find the built extension at dist/extension.zip. Now just install this extension in your Browser
|
Now install with either the powershell/bash script or use [X1nto's installer](https://github.com/X1nto/VencordInstaller/releases/latest)
|
||||||
|
|
||||||
## Installing Plugins
|
|
||||||
|
|
||||||
> **Note**
|
|
||||||
> You can only use 3rd party plugins in the manual Vencord install for now.
|
|
||||||
|
|
||||||
Vencord comes with a bunch of plugins out of the box!
|
|
||||||
|
|
||||||
However, if you want to install your own ones, create a `userplugins` folder in the `src` directory and create or clone your plugins in there.
|
|
||||||
Don't forget to rebuild!
|
|
||||||
|
|
||||||
Want to learn how to create your own plugin, and maybe PR it into Vencord? See the [Contributing](#contributing) section below!
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md) and [Megu's Plugin Guide!](docs/2_PLUGINS.md)
|
|
||||||
|
|
||||||
[contribute]: CONTRIBUTING.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]
|
[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
|
||||||
|
|
||||||
[join]: https://discord.gg/D9uwnFnqmd
|
[join]: https://discord.gg/D9uwnFnqmd
|
||||||
|
|
||||||
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
|
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
|
||||||
|
@ -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,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Linnea Gräf
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function setContentTypeOnStylesheets(details) {
|
|
||||||
if (details.type === "stylesheet") {
|
|
||||||
details.responseHeaders = details.responseHeaders.filter(it => it.name.toLowerCase() !== 'content-type');
|
|
||||||
details.responseHeaders.push({ name: "Content-Type", value: "text/css" });
|
|
||||||
}
|
|
||||||
return { responseHeaders: details.responseHeaders };
|
|
||||||
}
|
|
||||||
|
|
||||||
var cspHeaders = [
|
|
||||||
"content-security-policy",
|
|
||||||
"content-security-policy-report-only",
|
|
||||||
];
|
|
||||||
|
|
||||||
function removeCSPHeaders(details) {
|
|
||||||
return {
|
|
||||||
responseHeaders: details.responseHeaders.filter(header =>
|
|
||||||
!cspHeaders.includes(header.name.toLowerCase()))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
browser.webRequest.onHeadersReceived.addListener(
|
|
||||||
setContentTypeOnStylesheets, { urls: ["https://raw.githubusercontent.com/*"] }, ["blocking", "responseHeaders"]
|
|
||||||
);
|
|
||||||
|
|
||||||
browser.webRequest.onHeadersReceived.addListener(
|
|
||||||
removeCSPHeaders, { urls: ["https://raw.githubusercontent.com/*", "*://*.discord.com/*"] }, ["blocking", "responseHeaders"]
|
|
||||||
);
|
|
@ -1,8 +0,0 @@
|
|||||||
if (typeof browser === "undefined") {
|
|
||||||
var browser = chrome;
|
|
||||||
}
|
|
||||||
|
|
||||||
var script = document.createElement("script");
|
|
||||||
script.src = browser.runtime.getURL("dist/Vencord.js");
|
|
||||||
// documentElement because we load before body/head are ready
|
|
||||||
document.documentElement.appendChild(script);
|
|
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 2,
|
|
||||||
"name": "Vencord Web",
|
|
||||||
"description": "The Vencord Client Mod for Discord Web.",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Vendicated",
|
|
||||||
"homepage_url": "https://github.com/Vendicated/Vencord",
|
|
||||||
"permissions": [
|
|
||||||
"webRequest",
|
|
||||||
"webRequestBlocking",
|
|
||||||
"*://*.discord.com/*",
|
|
||||||
"https://raw.githubusercontent.com/*"
|
|
||||||
],
|
|
||||||
"content_scripts": [
|
|
||||||
{
|
|
||||||
"run_at": "document_start",
|
|
||||||
"matches": ["*://*.discord.com/*"],
|
|
||||||
"js": ["content.js"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"web_accessible_resources": ["dist/Vencord.js"],
|
|
||||||
"background": {
|
|
||||||
"scripts": ["background.js"]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 3,
|
|
||||||
"name": "Vencord Web",
|
|
||||||
"description": "Yeee",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Vendicated",
|
|
||||||
"homepage_url": "https://github.com/Vendicated/Vencord",
|
|
||||||
|
|
||||||
"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"],
|
|
||||||
"matches": ["*://*.discord.com/*"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"declarative_net_request": {
|
|
||||||
"rule_resources": [
|
|
||||||
{
|
|
||||||
"id": "modifyResponseHeaders",
|
|
||||||
"enabled": true,
|
|
||||||
"path": "modifyResponseHeaders.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 none
|
|
||||||
// @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
|
|
123
build.mjs
Normal file → Executable file
123
build.mjs
Normal file → Executable file
@ -1,3 +1,122 @@
|
|||||||
// FIXME: Delete this soon, for now it is needed so people can update
|
#!/usr/bin/node
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import esbuild from "esbuild";
|
||||||
|
import { readdirSync } from "fs";
|
||||||
|
import { performance } from "perf_hooks";
|
||||||
|
|
||||||
import("./scripts/build/build.mjs");
|
/**
|
||||||
|
* @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,203 +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");
|
|
||||||
require("../app.asar");
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
81
package.json
81
package.json
@ -1,75 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "vencord",
|
"devDependencies": {
|
||||||
"private": "true",
|
"@types/flux": "^3.1.11",
|
||||||
"version": "1.0.1",
|
"@types/node": "^18.7.13",
|
||||||
"description": "A Discord client mod that does things differently",
|
"@types/react": "^18.0.17",
|
||||||
"keywords": [],
|
"electron": "^20.1.0",
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"esbuild": "^0.15.5"
|
||||||
"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/patcher/install.js",
|
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
|
||||||
"lint:fix": "pnpm lint --fix",
|
|
||||||
"test": "pnpm lint && pnpm build && pnpm testTsc",
|
|
||||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
|
||||||
"testTsc": "tsc --noEmit",
|
|
||||||
"uninject": "node scripts/patcher/uninstall.js",
|
|
||||||
"watch": "node scripts/build/build.mjs --watch"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fflate": "^0.7.4"
|
"@swc/wasm-web": "^1.3.4",
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/diff": "^5.0.2",
|
|
||||||
"@types/node": "^18.11.9",
|
|
||||||
"@types/react": "^18.0.25",
|
|
||||||
"@types/react-dom": "^18.0.9",
|
|
||||||
"@types/yazl": "^2.4.2",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
|
||||||
"@typescript-eslint/parser": "^5.44.0",
|
|
||||||
"@vap/core": "0.0.12",
|
|
||||||
"@vap/shiki": "0.10.3",
|
|
||||||
"console-menu": "^0.1.0",
|
|
||||||
"diff": "^5.1.0",
|
|
||||||
"discord-types": "^1.3.26",
|
"discord-types": "^1.3.26",
|
||||||
"esbuild": "^0.15.16",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"eslint": "^8.28.0",
|
"jsposed": "^1.0.2",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"prettier": "^2.7.1"
|
||||||
"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.3.0",
|
|
||||||
"standalone-electron-types": "^1.0.0",
|
|
||||||
"type-fest": "^3.3.0",
|
|
||||||
"typescript": "^4.9.3"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@7.13.4",
|
"scripts": {
|
||||||
"pnpm": {
|
"build": "node build.mjs",
|
||||||
"patchedDependencies": {
|
"watch": "node build.mjs --watch"
|
||||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"webExt": {
|
|
||||||
"artifactsDir": "./dist",
|
|
||||||
"build": {
|
|
||||||
"overwriteDest": true
|
|
||||||
},
|
|
||||||
"sourceDir": "./dist/extension-v2-unpacked"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
2162
pnpm-lock.yaml
generated
2162
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,110 +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 { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
||||||
import { readFile } from "fs/promises";
|
|
||||||
import { join, resolve } from "path";
|
|
||||||
|
|
||||||
// wtf is this assert syntax
|
|
||||||
import PackageJSON from "../../package.json" assert { type: "json" };
|
|
||||||
import { commonOpts, fileIncludePlugin, gitHashPlugin, gitRemotePlugin, 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,
|
|
||||||
gitHashPlugin,
|
|
||||||
gitRemotePlugin,
|
|
||||||
fileIncludePlugin
|
|
||||||
],
|
|
||||||
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,
|
|
||||||
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(window,'Vencord',{get:()=>Vencord});"
|
|
||||||
},
|
|
||||||
})
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
async function buildPluginZip(target, files, shouldZip) {
|
|
||||||
const entries = {
|
|
||||||
"dist/Vencord.js": readFileSync("dist/browser.js"),
|
|
||||||
...Object.fromEntries(await Promise.all(files.map(async f => [
|
|
||||||
(f.startsWith("manifest") ? "manifest.json" : f),
|
|
||||||
await readFile(join("browser", f))
|
|
||||||
]))),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (shouldZip) {
|
|
||||||
zip(entries, {}, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
|
||||||
process.exitCode = 1;
|
|
||||||
} else {
|
|
||||||
writeFileSync("dist/" + target, data);
|
|
||||||
console.info("Extension written to dist/" + target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (existsSync(target))
|
|
||||||
rmSync(target, { recursive: true });
|
|
||||||
for (const entry in entries) {
|
|
||||||
const destination = "dist/" + target + "/" + entry;
|
|
||||||
const parentDirectory = resolve(destination, "..");
|
|
||||||
mkdirSync(parentDirectory, { recursive: true });
|
|
||||||
writeFileSync(destination, entries[entry]);
|
|
||||||
}
|
|
||||||
console.info("Unpacked Extension written to dist/" + target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await buildPluginZip("extension-v3.zip", ["modifyResponseHeaders.json", "content.js", "manifestv3.json"], true);
|
|
||||||
await buildPluginZip("extension-v2.zip", ["background.js", "content.js", "manifestv2.json"], true);
|
|
||||||
await buildPluginZip("extension-v2-unpacked", ["background.js", "content.js", "manifestv2.json"], false);
|
|
||||||
|
|
@ -1,168 +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 } from "fs";
|
|
||||||
import { readdir, readFile } from "fs/promises";
|
|
||||||
import { join } 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 {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 {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 === "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 {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 {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 {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"))}`
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {import("esbuild").BuildOptions}
|
|
||||||
*/
|
|
||||||
export const commonOpts = {
|
|
||||||
logLevel: "info",
|
|
||||||
bundle: true,
|
|
||||||
watch,
|
|
||||||
minify: !watch,
|
|
||||||
sourcemap: watch ? "inline" : "",
|
|
||||||
legalComments: "linked",
|
|
||||||
banner,
|
|
||||||
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin],
|
|
||||||
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 = Symbol.for("react.fragment");
|
|
||||||
export let VencordCreateElement =
|
|
||||||
(...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);
|
|
@ -1,7 +0,0 @@
|
|||||||
// Work around https://github.com/evanw/esbuild/issues/2460
|
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "react"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,342 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const path = require("path");
|
|
||||||
const readline = require("readline");
|
|
||||||
const fs = require("fs");
|
|
||||||
const menu = require("console-menu");
|
|
||||||
|
|
||||||
const BRANCH_NAMES = [
|
|
||||||
"Discord",
|
|
||||||
"DiscordPTB",
|
|
||||||
"DiscordCanary",
|
|
||||||
"DiscordDevelopment",
|
|
||||||
"discord",
|
|
||||||
"discordptb",
|
|
||||||
"discordcanary",
|
|
||||||
"discorddevelopment",
|
|
||||||
"discord-ptb",
|
|
||||||
"discord-canary",
|
|
||||||
"discord-development",
|
|
||||||
// Flatpak
|
|
||||||
"com.discordapp.Discord",
|
|
||||||
"com.discordapp.DiscordPTB",
|
|
||||||
"com.discordapp.DiscordCanary",
|
|
||||||
"com.discordapp.DiscordDevelopment",
|
|
||||||
];
|
|
||||||
|
|
||||||
const MACOS_DISCORD_DIRS = [
|
|
||||||
"Discord.app",
|
|
||||||
"Discord PTB.app",
|
|
||||||
"Discord Canary.app",
|
|
||||||
"Discord Development.app",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (process.platform === "linux" && process.env.SUDO_USER) {
|
|
||||||
process.env.HOME = fs
|
|
||||||
.readFileSync("/etc/passwd", "utf-8")
|
|
||||||
.match(new RegExp(`^${process.env.SUDO_USER}.+$`, "m"))[0]
|
|
||||||
.split(":")[5];
|
|
||||||
}
|
|
||||||
|
|
||||||
const LINUX_DISCORD_DIRS = [
|
|
||||||
"/usr/share",
|
|
||||||
"/usr/lib64",
|
|
||||||
"/opt",
|
|
||||||
`${process.env.HOME}/.local/share`,
|
|
||||||
`${process.env.HOME}/.dvm`,
|
|
||||||
"/var/lib/flatpak/app",
|
|
||||||
`${process.env.HOME}/.local/share/flatpak/app`,
|
|
||||||
];
|
|
||||||
|
|
||||||
const FLATPAK_NAME_MAPPING = {
|
|
||||||
DiscordCanary: "discord-canary",
|
|
||||||
DiscordPTB: "discord-ptb",
|
|
||||||
DiscordDevelopment: "discord-development",
|
|
||||||
Discord: "discord",
|
|
||||||
};
|
|
||||||
|
|
||||||
const ENTRYPOINT = path
|
|
||||||
.join(process.cwd(), "dist", "patcher.js")
|
|
||||||
.replace(/\\/g, "/");
|
|
||||||
|
|
||||||
function question(question) {
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout,
|
|
||||||
terminal: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise(resolve => {
|
|
||||||
rl.question(question, answer => {
|
|
||||||
rl.close();
|
|
||||||
resolve(answer);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMenuItem(installations) {
|
|
||||||
const menuItems = installations.map(info => ({
|
|
||||||
title: info.patched ? "[MODIFIED] " + info.location : info.location,
|
|
||||||
info,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const result = await menu(
|
|
||||||
[
|
|
||||||
...menuItems,
|
|
||||||
{ title: "Specify custom path", info: "custom" },
|
|
||||||
{ title: "Exit without patching", exit: true }
|
|
||||||
],
|
|
||||||
{
|
|
||||||
header: "Select a Discord installation to patch:",
|
|
||||||
border: true,
|
|
||||||
helpMessage:
|
|
||||||
"Use the up/down arrow keys to select an option. " +
|
|
||||||
"Press ENTER to confirm.",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!result || !result.info || result.exit) {
|
|
||||||
console.log("No installation selected.");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.info === "custom") {
|
|
||||||
const customPath = await question("Please enter the path: ");
|
|
||||||
if (!customPath || !fs.existsSync(customPath)) {
|
|
||||||
console.log("No such Path or not specifed.");
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceDir = path.join(customPath, "resources");
|
|
||||||
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
|
||||||
console.log("Unsupported Install. resources/app.asar not found");
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
const appDir = path.join(resourceDir, "app");
|
|
||||||
result.info = {
|
|
||||||
branch: "unknown",
|
|
||||||
patched: fs.existsSync(appDir),
|
|
||||||
location: customPath,
|
|
||||||
versions: [{
|
|
||||||
path: appDir,
|
|
||||||
name: null
|
|
||||||
}],
|
|
||||||
arch: process.platform === "linux" ? "linux" : "win32",
|
|
||||||
isFlatpak: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.info.patched) {
|
|
||||||
const answer = await question(
|
|
||||||
"This installation has already been modified. Overwrite? [Y/n]: "
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!["y", "yes", "yeah", ""].includes(answer.toLowerCase())) {
|
|
||||||
console.log("Not patching.");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.info;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWindowsDirs() {
|
|
||||||
const dirs = [];
|
|
||||||
for (const dir of fs.readdirSync(process.env.LOCALAPPDATA)) {
|
|
||||||
if (!BRANCH_NAMES.includes(dir)) continue;
|
|
||||||
|
|
||||||
const location = path.join(process.env.LOCALAPPDATA, dir);
|
|
||||||
if (!fs.statSync(location).isDirectory()) continue;
|
|
||||||
|
|
||||||
const appDirs = fs
|
|
||||||
.readdirSync(location, { withFileTypes: true })
|
|
||||||
.filter(file => file.isDirectory())
|
|
||||||
.filter(file => file.name.startsWith("app-"))
|
|
||||||
.map(file => path.join(location, file.name));
|
|
||||||
|
|
||||||
const versions = [];
|
|
||||||
let patched = false;
|
|
||||||
|
|
||||||
for (const fqAppDir of appDirs) {
|
|
||||||
const resourceDir = path.join(fqAppDir, "resources");
|
|
||||||
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const appDir = path.join(resourceDir, "app");
|
|
||||||
if (fs.existsSync(appDir)) {
|
|
||||||
patched = true;
|
|
||||||
}
|
|
||||||
versions.push({
|
|
||||||
path: appDir,
|
|
||||||
name: /app-([0-9.]+)/.exec(fqAppDir)[1],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appDirs.length) {
|
|
||||||
dirs.push({
|
|
||||||
branch: dir,
|
|
||||||
patched,
|
|
||||||
location,
|
|
||||||
versions,
|
|
||||||
arch: "win32",
|
|
||||||
flatpak: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dirs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDarwinDirs() {
|
|
||||||
const dirs = [];
|
|
||||||
for (const dir of fs.readdirSync("/Applications")) {
|
|
||||||
if (!MACOS_DISCORD_DIRS.includes(dir)) continue;
|
|
||||||
|
|
||||||
const location = path.join("/Applications", dir, "Contents");
|
|
||||||
if (!fs.existsSync(location)) continue;
|
|
||||||
if (!fs.statSync(location).isDirectory()) continue;
|
|
||||||
|
|
||||||
const appDirs = fs
|
|
||||||
.readdirSync(location, { withFileTypes: true })
|
|
||||||
.filter(file => file.isDirectory())
|
|
||||||
.filter(file => file.name.startsWith("Resources"))
|
|
||||||
.map(file => path.join(location, file.name));
|
|
||||||
|
|
||||||
const versions = [];
|
|
||||||
let patched = false;
|
|
||||||
|
|
||||||
for (const resourceDir of appDirs) {
|
|
||||||
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const appDir = path.join(resourceDir, "app");
|
|
||||||
if (fs.existsSync(appDir)) {
|
|
||||||
patched = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
versions.push({
|
|
||||||
path: appDir,
|
|
||||||
name: null, // MacOS installs have no version number
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appDirs.length) {
|
|
||||||
dirs.push({
|
|
||||||
branch: dir,
|
|
||||||
patched,
|
|
||||||
location,
|
|
||||||
versions,
|
|
||||||
arch: "win32",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dirs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLinuxDirs() {
|
|
||||||
const dirs = [];
|
|
||||||
for (const dir of LINUX_DISCORD_DIRS) {
|
|
||||||
if (!fs.existsSync(dir)) continue;
|
|
||||||
for (const branch of fs.readdirSync(dir)) {
|
|
||||||
if (!BRANCH_NAMES.includes(branch)) continue;
|
|
||||||
|
|
||||||
const location = path.join(dir, branch);
|
|
||||||
if (!fs.statSync(location).isDirectory()) continue;
|
|
||||||
|
|
||||||
const isFlatpak = location.includes("/flatpak/");
|
|
||||||
|
|
||||||
let appDirs = [];
|
|
||||||
|
|
||||||
if (isFlatpak) {
|
|
||||||
const fqDir = path.join(location, "current", "active", "files");
|
|
||||||
if (!/com\.discordapp\.(\w+)\//.test(fqDir)) continue;
|
|
||||||
const branchName = /com\.discordapp\.(\w+)\//.exec(fqDir)[1];
|
|
||||||
if (!Object.keys(FLATPAK_NAME_MAPPING).includes(branchName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const appDir = path.join(
|
|
||||||
fqDir,
|
|
||||||
FLATPAK_NAME_MAPPING[branchName]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fs.existsSync(appDir)) continue;
|
|
||||||
if (!fs.statSync(appDir).isDirectory()) continue;
|
|
||||||
|
|
||||||
const resourceDir = path.join(appDir, "resources");
|
|
||||||
|
|
||||||
appDirs.push(resourceDir);
|
|
||||||
} else {
|
|
||||||
appDirs = fs
|
|
||||||
.readdirSync(location, { withFileTypes: true })
|
|
||||||
.filter(file => file.isDirectory())
|
|
||||||
.filter(
|
|
||||||
file =>
|
|
||||||
file.name.startsWith("app-") ||
|
|
||||||
file.name === "resources"
|
|
||||||
)
|
|
||||||
.map(file => path.join(location, file.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
const versions = [];
|
|
||||||
let patched = false;
|
|
||||||
|
|
||||||
for (const resourceDir of appDirs) {
|
|
||||||
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const appDir = path.join(resourceDir, "app");
|
|
||||||
if (fs.existsSync(appDir)) {
|
|
||||||
patched = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const version = /app-([0-9.]+)/.exec(resourceDir);
|
|
||||||
|
|
||||||
versions.push({
|
|
||||||
path: appDir,
|
|
||||||
name: version && version.length > 1 ? version[1] : null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appDirs.length) {
|
|
||||||
dirs.push({
|
|
||||||
branch,
|
|
||||||
patched,
|
|
||||||
location,
|
|
||||||
versions,
|
|
||||||
arch: "linux",
|
|
||||||
isFlatpak,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dirs;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
BRANCH_NAMES,
|
|
||||||
MACOS_DISCORD_DIRS,
|
|
||||||
LINUX_DISCORD_DIRS,
|
|
||||||
FLATPAK_NAME_MAPPING,
|
|
||||||
ENTRYPOINT,
|
|
||||||
question,
|
|
||||||
getMenuItem,
|
|
||||||
getWindowsDirs,
|
|
||||||
getDarwinDirs,
|
|
||||||
getLinuxDirs,
|
|
||||||
};
|
|
@ -1,153 +0,0 @@
|
|||||||
#!/usr/bin/node
|
|
||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
|
||||||
const { execSync } = require("child_process");
|
|
||||||
|
|
||||||
console.log("\nVencord Installer\n");
|
|
||||||
|
|
||||||
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
|
|
||||||
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(path.join(process.cwd(), "dist", "patcher.js"))) {
|
|
||||||
console.log("You need to build the project first. Run:", "pnpm build");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
getMenuItem,
|
|
||||||
getWindowsDirs,
|
|
||||||
getDarwinDirs,
|
|
||||||
getLinuxDirs,
|
|
||||||
ENTRYPOINT,
|
|
||||||
question
|
|
||||||
} = require("./common");
|
|
||||||
|
|
||||||
switch (process.platform) {
|
|
||||||
case "win32":
|
|
||||||
install(getWindowsDirs());
|
|
||||||
break;
|
|
||||||
case "darwin":
|
|
||||||
install(getDarwinDirs());
|
|
||||||
break;
|
|
||||||
case "linux":
|
|
||||||
install(getLinuxDirs());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("Unknown OS");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function install(installations) {
|
|
||||||
const selected = await getMenuItem(installations);
|
|
||||||
|
|
||||||
// Attempt to give flatpak perms
|
|
||||||
if (selected.isFlatpak) {
|
|
||||||
try {
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const globalCmd = `flatpak override ${selected.branch} --filesystem=${cwd}`;
|
|
||||||
const userCmd = `flatpak override --user ${selected.branch} --filesystem=${cwd}`;
|
|
||||||
const cmd = selected.location.startsWith("/home")
|
|
||||||
? userCmd
|
|
||||||
: globalCmd;
|
|
||||||
execSync(cmd);
|
|
||||||
console.log("Gave write perms to Discord Flatpak.");
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Failed to give write perms to Discord Flatpak.");
|
|
||||||
console.log(
|
|
||||||
"Try running this script as an administrator:",
|
|
||||||
"sudo pnpm inject"
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const answer = await question(
|
|
||||||
`Would you like to allow ${selected.branch} to talk to org.freedesktop.Flatpak?\n` +
|
|
||||||
"This is essentially full host access but necessary to spawn git. Without it, the updater will not work\n" +
|
|
||||||
"Consider using the http based updater (using the gui installer) instead if you want to maintain the sandbox.\n" +
|
|
||||||
"[y/N]: "
|
|
||||||
);
|
|
||||||
|
|
||||||
if (["y", "yes", "yeah"].includes(answer.toLowerCase())) {
|
|
||||||
try {
|
|
||||||
const globalCmd = `flatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
|
|
||||||
const userCmd = `flatpak override --user ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
|
|
||||||
const cmd = selected.location.startsWith("/home")
|
|
||||||
? userCmd
|
|
||||||
: globalCmd;
|
|
||||||
execSync(cmd);
|
|
||||||
console.log("Sucessfully gave talk permission");
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to give talk permission\n", err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`Not giving full host access. If you change your mind later, you can run:\nflatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const version of selected.versions) {
|
|
||||||
const dir = version.path;
|
|
||||||
// Check if we have write perms to the install directory...
|
|
||||||
try {
|
|
||||||
fs.accessSync(selected.location, fs.constants.W_OK);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("No write access to", selected.location);
|
|
||||||
console.error(
|
|
||||||
"Try running this script as an administrator:",
|
|
||||||
"sudo pnpm inject"
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (fs.existsSync(dir) && fs.lstatSync(dir).isDirectory()) {
|
|
||||||
fs.rmSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(dir)) {
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(dir, "index.js"),
|
|
||||||
`require("${ENTRYPOINT}");`
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(dir, "package.json"),
|
|
||||||
JSON.stringify({
|
|
||||||
name: "discord",
|
|
||||||
main: "index.js",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const requiredFiles = ["index.js", "package.json"];
|
|
||||||
|
|
||||||
if (requiredFiles.every(f => fs.existsSync(path.join(dir, f)))) {
|
|
||||||
console.log(
|
|
||||||
"Successfully patched",
|
|
||||||
version.name
|
|
||||||
? `${selected.branch} ${version.name}`
|
|
||||||
: selected.branch
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log("Failed to patch", dir);
|
|
||||||
console.log("Files in directory:", fs.readdirSync(dir));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
#!/usr/bin/node
|
|
||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
console.log("\nVencord Uninstaller\n");
|
|
||||||
|
|
||||||
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
|
|
||||||
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
getMenuItem,
|
|
||||||
getWindowsDirs,
|
|
||||||
getDarwinDirs,
|
|
||||||
getLinuxDirs,
|
|
||||||
} = require("./common");
|
|
||||||
|
|
||||||
switch (process.platform) {
|
|
||||||
case "win32":
|
|
||||||
uninstall(getWindowsDirs());
|
|
||||||
break;
|
|
||||||
case "darwin":
|
|
||||||
uninstall(getDarwinDirs());
|
|
||||||
break;
|
|
||||||
case "linux":
|
|
||||||
uninstall(getLinuxDirs());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("Unknown OS");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uninstall(installations) {
|
|
||||||
const selected = await getMenuItem(installations);
|
|
||||||
|
|
||||||
for (const version of selected.versions) {
|
|
||||||
const dir = version.path;
|
|
||||||
// Check if we have write perms to the install directory...
|
|
||||||
try {
|
|
||||||
fs.accessSync(selected.location, fs.constants.W_OK);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("No write access to", selected.location);
|
|
||||||
console.error(
|
|
||||||
"Try running this script as an administrator:",
|
|
||||||
"sudo pnpm uninject"
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (fs.existsSync(dir)) {
|
|
||||||
fs.rmSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
"Successfully unpatched",
|
|
||||||
version.name
|
|
||||||
? `${selected.branch} ${version.name}`
|
|
||||||
: selected.branch
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,48 +1,32 @@
|
|||||||
/*!
|
|
||||||
* 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 Plugins from "./plugins";
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
||||||
export * as Util from "./utils";
|
|
||||||
export * as QuickCss from "./utils/quickCss";
|
|
||||||
export * as Updater from "./utils/updater";
|
|
||||||
export * as Webpack from "./webpack";
|
export * as Webpack from "./webpack";
|
||||||
export { PlainSettings, Settings };
|
export * as Api from "./api";
|
||||||
|
export * as Updater from "./utils/updater";
|
||||||
import "./utils/quickCss";
|
export * as QuickCss from "./utils/quickCss";
|
||||||
import "./webpack/patchWebpack";
|
export * as Swc from "@swc/wasm-web";
|
||||||
|
import initSwc from "@swc/wasm-web";
|
||||||
|
|
||||||
import { popNotice, showNotice } from "./api/Notices";
|
import { popNotice, showNotice } from "./api/Notices";
|
||||||
import { PlainSettings, Settings } from "./api/settings";
|
import { Settings } from "./api/settings";
|
||||||
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
import { startAllPlugins } from "./plugins";
|
||||||
import { checkForUpdates, UpdateLogger } from "./utils/updater";
|
|
||||||
|
export { Settings };
|
||||||
|
|
||||||
|
import "./webpack/patchWebpack";
|
||||||
|
import "./utils/quickCss";
|
||||||
|
import { checkForUpdates, UpdateLogger } from './utils/updater';
|
||||||
import { onceReady } from "./webpack";
|
import { onceReady } from "./webpack";
|
||||||
import { Router } from "./webpack/common";
|
import { Router } from "./webpack/common";
|
||||||
|
import IpcEvents from "./utils/IpcEvents";
|
||||||
|
|
||||||
export let Components: any;
|
export let Components;
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
await initSwc(await VencordNative.ipc.invoke(IpcEvents.GET_SWC_WASM));
|
||||||
await onceReady;
|
await onceReady;
|
||||||
startAllPlugins();
|
startAllPlugins();
|
||||||
Components = await import("./components");
|
Components = await import("./components");
|
||||||
|
|
||||||
if (!IS_WEB) {
|
|
||||||
try {
|
try {
|
||||||
const isOutdated = await checkForUpdates();
|
const isOutdated = await checkForUpdates();
|
||||||
if (isOutdated && Settings.notifyAboutUpdates)
|
if (isOutdated && Settings.notifyAboutUpdates)
|
||||||
@ -59,20 +43,6 @@ async function init() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
UpdateLogger.error("Failed to check for updates", err);
|
UpdateLogger.error("Failed to check for updates", err);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_DEV) {
|
|
||||||
const pendingPatches = patches.filter(p => !p.all && p.predicate?.() !== false);
|
|
||||||
if (pendingPatches.length)
|
|
||||||
PMLogger.warn(
|
|
||||||
"Webpack has finished initialising, but some patches haven't been applied yet.",
|
|
||||||
"This might be expected since some Modules are lazy loaded, but please verify",
|
|
||||||
"that all plugins are working as intended.",
|
|
||||||
"You are seeing this warning because this is a Development build of Vencord.",
|
|
||||||
"\nThe following patches have not been applied:",
|
|
||||||
"\n\n" + pendingPatches.map(p => `${p.plugin}: ${p.find}`).join("\n")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
@ -1,23 +1,5 @@
|
|||||||
/*
|
import IPC_EVENTS from './utils/IpcEvents';
|
||||||
* Vencord, a modification for Discord's desktop app
|
import { IpcRenderer, ipcRenderer } from 'electron';
|
||||||
* 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";
|
|
||||||
|
|
||||||
function assertEventAllowed(event: string) {
|
function assertEventAllowed(event: string) {
|
||||||
if (!(event in IPC_EVENTS)) throw new Error(`Event ${event} not allowed.`);
|
if (!(event in IPC_EVENTS)) throw new Error(`Event ${event} not allowed.`);
|
||||||
@ -45,5 +27,14 @@ export default {
|
|||||||
assertEventAllowed(event);
|
assertEventAllowed(event);
|
||||||
return ipcRenderer.invoke(event, ...args);
|
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,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 { User } from "discord-types/general";
|
|
||||||
import { HTMLProps } from "react";
|
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
|
||||||
|
|
||||||
export enum BadgePosition {
|
|
||||||
START,
|
|
||||||
END
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProfileBadge {
|
|
||||||
/** The tooltip to show on hover */
|
|
||||||
tooltip: string;
|
|
||||||
/** 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 */
|
|
||||||
props?: HTMLProps<HTMLImageElement>;
|
|
||||||
/** Insert at start or end? */
|
|
||||||
position?: BadgePosition;
|
|
||||||
|
|
||||||
/** The badge name to display. Discord uses this, but we don't. */
|
|
||||||
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) {
|
|
||||||
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)
|
|
||||||
: badgeArray.push(badge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(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,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 { mergeDefaults } from "@utils/misc";
|
|
||||||
import { findByCodeLazy, findByPropsLazy, waitFor } from "@webpack";
|
|
||||||
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");
|
|
||||||
|
|
||||||
let SnowflakeUtils: any;
|
|
||||||
waitFor("fromTimestamp", m => SnowflakeUtils = m);
|
|
||||||
|
|
||||||
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,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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type AccessoryCallback = (props: Record<string, any>) => JSX.Element;
|
|
||||||
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()) {
|
|
||||||
elements.splice(
|
|
||||||
accessory.position != null
|
|
||||||
? accessory.position < 0
|
|
||||||
? elements.length + accessory.position
|
|
||||||
: accessory.position
|
|
||||||
: elements.length,
|
|
||||||
0,
|
|
||||||
accessory.callback(props)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return elements;
|
|
||||||
}
|
|
@ -1,24 +1,5 @@
|
|||||||
/*
|
import type { Message, Channel } from 'discord-types/general';
|
||||||
* Vencord, a modification for Discord's desktop app
|
import Logger from '../utils/logger';
|
||||||
* 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";
|
|
||||||
|
|
||||||
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
||||||
|
|
||||||
@ -37,37 +18,25 @@ export interface MessageObject {
|
|||||||
validNonShortcutEmojis: Emoji[];
|
validNonShortcutEmojis: Emoji[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageExtra {
|
export type SendListener = (channelId: string, messageObj: MessageObject, extra: any) => void;
|
||||||
stickerIds?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => void | { cancel: boolean; };
|
|
||||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
|
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
|
||||||
|
|
||||||
const sendListeners = new Set<SendListener>();
|
const sendListeners = new Set<SendListener>();
|
||||||
const editListeners = new Set<EditListener>();
|
const editListeners = new Set<EditListener>();
|
||||||
|
|
||||||
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
|
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: any) {
|
||||||
for (const listener of sendListeners) {
|
for (const listener of sendListeners) {
|
||||||
try {
|
try {
|
||||||
const result = listener(channelId, messageObj, extra);
|
listener(channelId, messageObj, extra);
|
||||||
if (result && result.cancel === true) {
|
} catch (e) { MessageEventsLogger.error(`MessageSendHandler: Listener encoutered an unknown error. (${e})`); }
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
} 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) {
|
for (const listener of editListeners) {
|
||||||
try {
|
try {
|
||||||
listener(channelId, messageId, messageObj);
|
listener(channeld, messageId, messageObj);
|
||||||
} catch (e) {
|
} catch (e) { MessageEventsLogger.error(`MessageEditHandler: Listener encoutered an unknown error. (${e})`); }
|
||||||
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,15 +67,11 @@ type ClickListener = (message: Message, channel: Channel, event: MouseEvent) =>
|
|||||||
|
|
||||||
const listeners = new Set<ClickListener>();
|
const listeners = new Set<ClickListener>();
|
||||||
|
|
||||||
export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
|
export function _handleClick(message, channel, event) {
|
||||||
// message object may be outdated, so (try to) fetch latest one
|
|
||||||
message = MessageStore.getMessage(channel.id, message.id) ?? message;
|
|
||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
try {
|
try {
|
||||||
listener(message, channel, event);
|
listener(message, channel, event);
|
||||||
} catch (e) {
|
} catch (e) { MessageEventsLogger.error(`MessageClickHandler: Listener encoutered an unknown error. (${e})`); }
|
||||||
MessageEventsLogger.error("MessageClickHandler: Listener encountered an unknown error\n", 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 @@
|
|||||||
/*
|
import { waitFor } from "../webpack";
|
||||||
* 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";
|
|
||||||
|
|
||||||
let NoticesModule: any;
|
let NoticesModule: any;
|
||||||
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
|
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
|
||||||
|
@ -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,76 +1,2 @@
|
|||||||
/*
|
export * as MessageEvents from "./MessageEvents";
|
||||||
* Vencord, a modification for Discord's desktop app
|
export * as Notices from "./Notices";
|
||||||
* 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 $MessageAccessories from "./MessageAccessories";
|
|
||||||
import * as $MessageEventsAPI from "./MessageEvents";
|
|
||||||
import * as $MessagePopover from "./MessagePopover";
|
|
||||||
import * as $Notices from "./Notices";
|
|
||||||
import * as $ServerList from "./ServerList";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
const MessageEvents = $MessageEventsAPI;
|
|
||||||
/**
|
|
||||||
* An API allowing you to create custom notices
|
|
||||||
* (snackbars on the top, like the Update prompt)
|
|
||||||
*/
|
|
||||||
const Notices = $Notices;
|
|
||||||
/**
|
|
||||||
* An API allowing you to register custom commands
|
|
||||||
*/
|
|
||||||
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}
|
|
||||||
*/
|
|
||||||
const DataStore = $DataStore;
|
|
||||||
/**
|
|
||||||
* An API allowing you to add custom components as message accessories
|
|
||||||
*/
|
|
||||||
const MessageAccessories = $MessageAccessories;
|
|
||||||
/**
|
|
||||||
* An API allowing you to add custom buttons in the message popover
|
|
||||||
*/
|
|
||||||
const MessagePopover = $MessagePopover;
|
|
||||||
/**
|
|
||||||
* An API allowing you to add badges to user profiles
|
|
||||||
*/
|
|
||||||
const Badges = $Badges;
|
|
||||||
/**
|
|
||||||
* An API allowing you to add custom elements to the server list
|
|
||||||
*/
|
|
||||||
const ServerList = $ServerList;
|
|
||||||
|
|
||||||
export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, MessagePopover, Notices, ServerList };
|
|
||||||
|
@ -1,35 +1,12 @@
|
|||||||
/*
|
import plugins from "plugins";
|
||||||
* Vencord, a modification for Discord's desktop app
|
import IpcEvents from "../utils/IpcEvents";
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
import { React } from "../webpack/common";
|
||||||
*
|
import { mergeDefaults } from '../utils/misc';
|
||||||
* 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";
|
interface Settings {
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import { mergeDefaults } from "@utils/misc";
|
|
||||||
import { OptionType } from "@utils/types";
|
|
||||||
import { React } from "@webpack/common";
|
|
||||||
|
|
||||||
import plugins from "~plugins";
|
|
||||||
|
|
||||||
const logger = new Logger("Settings");
|
|
||||||
export interface Settings {
|
|
||||||
notifyAboutUpdates: boolean;
|
notifyAboutUpdates: boolean;
|
||||||
|
unsafeRequire: boolean;
|
||||||
useQuickCss: boolean;
|
useQuickCss: boolean;
|
||||||
enableReactDevtools: boolean;
|
|
||||||
themeLinks: string[];
|
|
||||||
plugins: {
|
plugins: {
|
||||||
[plugin: string]: {
|
[plugin: string]: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -40,100 +17,58 @@ export interface Settings {
|
|||||||
|
|
||||||
const DefaultSettings: Settings = {
|
const DefaultSettings: Settings = {
|
||||||
notifyAboutUpdates: true,
|
notifyAboutUpdates: true,
|
||||||
|
unsafeRequire: false,
|
||||||
useQuickCss: true,
|
useQuickCss: true,
|
||||||
themeLinks: [],
|
|
||||||
enableReactDevtools: false,
|
|
||||||
plugins: {}
|
plugins: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const plugin in plugins) {
|
||||||
|
DefaultSettings.plugins[plugin] = {
|
||||||
|
enabled: plugins[plugin].required ?? false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings;
|
var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings;
|
||||||
|
for (const key in DefaultSettings) {
|
||||||
|
settings[key] ??= DefaultSettings[key];
|
||||||
|
}
|
||||||
mergeDefaults(settings, DefaultSettings);
|
mergeDefaults(settings, DefaultSettings);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error("Corrupt settings file. ", err);
|
||||||
var settings = mergeDefaults({} as Settings, DefaultSettings);
|
var settings = mergeDefaults({} as Settings, DefaultSettings);
|
||||||
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
|
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
|
||||||
const subscriptions = new Set<SubscriptionCallback>();
|
const subscriptions = new Set<SubscriptionCallback>();
|
||||||
|
|
||||||
const proxyCache = {} as Record<string, any>;
|
function makeProxy(settings: Settings, root = settings, path = ""): Settings {
|
||||||
|
return new Proxy(settings, {
|
||||||
// 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, {
|
|
||||||
get(target, p: string) {
|
get(target, p: string) {
|
||||||
const v = target[p];
|
const v = target[p];
|
||||||
|
if (typeof v === "object" && !Array.isArray(v))
|
||||||
// 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)
|
|
||||||
return makeProxy(v, root, `${path}${path && "."}${p}`);
|
return makeProxy(v, root, `${path}${path && "."}${p}`);
|
||||||
|
|
||||||
// primitive or similar, no need to proxy further
|
|
||||||
return v;
|
return v;
|
||||||
},
|
},
|
||||||
|
|
||||||
set(target, p: string, v) {
|
set(target, p: string, v) {
|
||||||
// avoid unnecessary updates to React Components and other listeners
|
|
||||||
if (target[p] === v) return true;
|
if (target[p] === v) return true;
|
||||||
|
|
||||||
target[p] = v;
|
target[p] = v;
|
||||||
// Call any listeners that are listening to a setting of this path
|
|
||||||
const setPath = `${path}${path && "."}${p}`;
|
const setPath = `${path}${path && "."}${p}`;
|
||||||
for (const subscription of subscriptions) {
|
for (const subscription of subscriptions) {
|
||||||
if (!subscription._path || subscription._path === setPath) {
|
if (!subscription._path || subscription._path === setPath) {
|
||||||
subscription(v, setPath);
|
subscription(v, setPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// And don't forget to persist the settings!
|
|
||||||
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
|
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
|
||||||
return true;
|
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
|
* A smart settings object. Altering props automagically saves
|
||||||
* the updated settings to disk.
|
* the updated settings to disk.
|
||||||
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}
|
|
||||||
*/
|
*/
|
||||||
export const Settings = makeProxy(settings);
|
export const Settings = makeProxy(settings);
|
||||||
|
|
||||||
@ -141,19 +76,14 @@ export const Settings = makeProxy(settings);
|
|||||||
* Settings hook for React components. Returns a smart settings
|
* Settings hook for React components. Returns a smart settings
|
||||||
* object that automagically triggers a rerender if any properties
|
* object that automagically triggers a rerender if any properties
|
||||||
* are altered
|
* are altered
|
||||||
* @param paths An optional list of paths to whitelist for rerenders
|
|
||||||
* @returns Settings
|
* @returns Settings
|
||||||
*/
|
*/
|
||||||
export function useSettings(paths?: string[]) {
|
export function useSettings() {
|
||||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||||
|
|
||||||
const onUpdate: SubscriptionCallback = paths
|
|
||||||
? (value, path) => paths.includes(path) && forceUpdate()
|
|
||||||
: forceUpdate;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
subscriptions.add(onUpdate);
|
subscriptions.add(forceUpdate);
|
||||||
return () => void subscriptions.delete(onUpdate);
|
return () => void subscriptions.delete(forceUpdate);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return Settings;
|
return Settings;
|
||||||
@ -180,21 +110,3 @@ export function addSettingsListener(path: string, onUpdate: (newValue: any, path
|
|||||||
(onUpdate as SubscriptionCallback)._path = path;
|
(onUpdate as SubscriptionCallback)._path = path;
|
||||||
subscriptions.add(onUpdate);
|
subscriptions.add(onUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
|
||||||
const { plugins } = settings;
|
|
||||||
if (name in plugins) return;
|
|
||||||
|
|
||||||
for (const oldName of oldNames) {
|
|
||||||
if (oldName in plugins) {
|
|
||||||
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
|
|
||||||
plugins[name] = plugins[oldName];
|
|
||||||
delete plugins[oldName];
|
|
||||||
VencordNative.ipc.invoke(
|
|
||||||
IpcEvents.SET_SETTINGS,
|
|
||||||
JSON.stringify(settings, null, 4)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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 @@
|
|||||||
/*
|
import Logger from "../utils/logger";
|
||||||
* Vencord, a modification for Discord's desktop app
|
import { Card, React } from "../webpack/common";
|
||||||
* 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 { ErrorCard } from "./ErrorCard";
|
import { ErrorCard } from "./ErrorCard";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Render nothing if an error occurs */
|
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; }>>;
|
||||||
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 */
|
|
||||||
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
||||||
/** Custom error message */
|
|
||||||
message?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = "#e78284";
|
const color = "#e78284";
|
||||||
@ -39,29 +13,25 @@ const logger = new Logger("React ErrorBoundary", color);
|
|||||||
|
|
||||||
const NO_ERROR = {};
|
const NO_ERROR = {};
|
||||||
|
|
||||||
// We might want to import this in a place where React isn't ready yet.
|
export default class ErrorBoundary extends React.Component<React.PropsWithChildren<Props>> {
|
||||||
// Thus, wrap in a LazyComponent
|
static wrap<T = any>(Component: React.ComponentType<T>): (props: T) => React.ReactElement {
|
||||||
const ErrorBoundary = LazyComponent(() => {
|
return (props) => (
|
||||||
return class ErrorBoundary extends React.PureComponent<React.PropsWithChildren<Props>> {
|
<ErrorBoundary>
|
||||||
|
<Component {...props as any/* I hate react typings ??? */} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
error: NO_ERROR as any,
|
error: NO_ERROR as any,
|
||||||
stack: "",
|
|
||||||
message: ""
|
message: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
static getDerivedStateFromError(error: any) {
|
static getDerivedStateFromError(error: any) {
|
||||||
let stack = error?.stack ?? "";
|
|
||||||
let message = error?.message || String(error);
|
|
||||||
|
|
||||||
if (error instanceof Error && stack) {
|
return {
|
||||||
const eolIdx = stack.indexOf("\n");
|
error: error?.stack?.replace(/https:\/\/\S+\/assets\//g, "") || error?.message || String(error)
|
||||||
if (eolIdx !== -1) {
|
};
|
||||||
message = stack.slice(0, eolIdx);
|
|
||||||
stack = stack.slice(eolIdx + 1).replace(/https:\/\/\S+\/assets\//g, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { error, stack, message };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
@ -73,43 +43,27 @@ const ErrorBoundary = LazyComponent(() => {
|
|||||||
render() {
|
render() {
|
||||||
if (this.state.error === NO_ERROR) return this.props.children;
|
if (this.state.error === NO_ERROR) return this.props.children;
|
||||||
|
|
||||||
if (this.props.noop) return null;
|
|
||||||
|
|
||||||
if (this.props.fallback)
|
if (this.props.fallback)
|
||||||
return <this.props.fallback
|
return <this.props.fallback
|
||||||
children={this.props.children}
|
children={this.props.children}
|
||||||
{...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 (
|
return (
|
||||||
<ErrorCard style={{
|
<ErrorCard style={{
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}>
|
}}>
|
||||||
<h1>Oh no!</h1>
|
<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>
|
<code>
|
||||||
{this.state.message}
|
<pre>
|
||||||
{!!this.state.stack && (
|
{this.state.error}
|
||||||
<pre className={Margins.marginTop8}>
|
|
||||||
{this.state.stack}
|
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
|
||||||
</code>
|
</code>
|
||||||
</ErrorCard>
|
</ErrorCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}) as
|
|
||||||
React.ComponentType<React.PropsWithChildren<Props>> & {
|
|
||||||
wrap<T extends JSX.IntrinsicAttributes = 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 @@
|
|||||||
/*
|
import { Card } from "../webpack/common";
|
||||||
* 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";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
@ -1,34 +1,15 @@
|
|||||||
/*
|
import { PropsWithChildren } from "react";
|
||||||
* Vencord, a modification for Discord's desktop app
|
import type { React } from '../webpack/common';
|
||||||
* 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";
|
|
||||||
|
|
||||||
export function Flex(props: React.PropsWithChildren<{
|
export function Flex(props: React.PropsWithChildren<{
|
||||||
flexDirection?: React.CSSProperties["flexDirection"];
|
flexDirection?: React.CSSProperties["flexDirection"];
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
} & React.HTMLProps<HTMLDivElement>>) {
|
}>) {
|
||||||
props.style ??= {};
|
props.style ??= {};
|
||||||
props.style.display = "flex";
|
|
||||||
// TODO(ven): Remove me, what was I thinking??
|
|
||||||
props.style.gap ??= "1em";
|
|
||||||
props.style.flexDirection ||= props.flexDirection;
|
props.style.flexDirection ||= props.flexDirection;
|
||||||
delete props.flexDirection;
|
props.style.gap ??= "1em";
|
||||||
|
props.style.display = "flex";
|
||||||
return (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
{props.children}
|
{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 @@
|
|||||||
/*
|
import { React } from "../webpack/common";
|
||||||
* 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";
|
interface Props {
|
||||||
|
href: string;
|
||||||
interface Props extends React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> {
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Link(props: React.PropsWithChildren<Props>) {
|
export function Link(props: React.PropsWithChildren<Props>) {
|
||||||
if (props.disabled) {
|
if (props.disabled) {
|
||||||
props.style ??= {};
|
props.style ??= {};
|
||||||
props.style.pointerEvents = "none";
|
props.style.pointerEvents = "none";
|
||||||
props["aria-disabled"] = true;
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<a role="link" target="_blank" {...props}>
|
<a href={props.href} target="_blank" style={props.style}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
@ -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 { 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 win = open("about:blank", void 0, "popup,width=1000,height=1000")!;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
@ -1,297 +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 { 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]]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
function ReplacementComponent({ module, match, replacement, setReplacementError }) {
|
|
||||||
const [id, fact] = module;
|
|
||||||
const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
|
|
||||||
|
|
||||||
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
|
||||||
const src: string = fact.toString().replaceAll("\n", "");
|
|
||||||
try {
|
|
||||||
var patched = src.replace(match, replacement);
|
|
||||||
setReplacementError(void 0);
|
|
||||||
} catch (e) {
|
|
||||||
setReplacementError((e as Error).message);
|
|
||||||
return ["", [], []];
|
|
||||||
}
|
|
||||||
const m = src.match(match);
|
|
||||||
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",
|
|
||||||
"$`": "Insert the substring before the match",
|
|
||||||
"$'": "Insert the substring after the match",
|
|
||||||
"$n": "Insert the nth capturing group ($1, $2...)"
|
|
||||||
}).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 | Function>("");
|
|
||||||
|
|
||||||
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,239 +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 { 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);
|
|
||||||
|
|
||||||
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 (!pluginSettings || !plugin.options) {
|
|
||||||
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
|
||||||
function onChange(newValue: any) {
|
|
||||||
setTempSettings(s => ({ ...s, [key]: newValue }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError(hasError: boolean) {
|
|
||||||
setErrors(e => ({ ...e, [key]: hasError }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const Component = Components[setting.type];
|
|
||||||
return (
|
|
||||||
<Component
|
|
||||||
id={key}
|
|
||||||
key={key}
|
|
||||||
option={setting}
|
|
||||||
onChange={onChange}
|
|
||||||
onError={onError}
|
|
||||||
pluginSettings={pluginSettings}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMoreUsers(_label: string, count: number) {
|
|
||||||
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>
|
|
||||||
<Text variant="heading-md/bold">{plugin.name}</Text>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalContent style={{ marginBottom: 8, marginTop: 8 }}>
|
|
||||||
<Forms.FormSection>
|
|
||||||
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
|
|
||||||
<Forms.FormText>{plugin.description}</Forms.FormText>
|
|
||||||
<div style={{ marginTop: 8, marginBottom: 8, width: "fit-content" }}>
|
|
||||||
<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 />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</Forms.FormSection>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Forms.FormSection>
|
|
||||||
<Forms.FormTitle tag="h3">Settings</Forms.FormTitle>
|
|
||||||
{renderSettings()}
|
|
||||||
</Forms.FormSection>
|
|
||||||
</ModalContent>
|
|
||||||
<ModalFooter>
|
|
||||||
<Flex flexDirection="column" style={{ width: "100%" }}>
|
|
||||||
<Flex style={{ marginLeft: "auto" }}>
|
|
||||||
<Button
|
|
||||||
onClick={onClose}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
color={Button.Colors.RED}
|
|
||||||
>
|
|
||||||
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, 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 && option.isValid(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?.() ?? 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, 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 && option.isValid(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?.() ?? 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, 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 && option.isValid(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?.() ?? 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, 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 && option.isValid(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?.() ?? 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, 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 && option.isValid(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?.() ?? false}
|
|
||||||
{...option.componentProps}
|
|
||||||
/>
|
|
||||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
|
||||||
</Forms.FormSection>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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 { 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from "./SettingBooleanComponent";
|
|
||||||
export * from "./SettingCustomComponent";
|
|
||||||
export * from "./SettingNumericComponent";
|
|
||||||
export * from "./SettingSelectComponent";
|
|
||||||
export * from "./SettingSliderComponent";
|
|
||||||
export * from "./SettingTextComponent";
|
|
||||||
|
|
@ -1,336 +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 { showNotice } from "@api/Notices";
|
|
||||||
import { Settings, 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 { ChangeList } from "@utils/ChangeList";
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import { classes, LazyComponent } from "@utils/misc";
|
|
||||||
import { openModalLazy } from "@utils/modal";
|
|
||||||
import { Plugin } from "@utils/types";
|
|
||||||
import { findByCode, findByPropsLazy } from "@webpack";
|
|
||||||
import { Alerts, Button, Forms, Margins, Parser, React, Select, Switch, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
|
||||||
|
|
||||||
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
|
||||||
import PluginModal from "./PluginModal";
|
|
||||||
import * as styles from "./styles";
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReloadRequiredCardProps extends React.HTMLProps<HTMLDivElement> {
|
|
||||||
plugins: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function ReloadRequiredCard({ plugins, ...props }: ReloadRequiredCardProps) {
|
|
||||||
if (plugins.length === 0) return null;
|
|
||||||
|
|
||||||
const pluginPrefix = plugins.length === 1 ? "The plugin" : "The following plugins require a reload to apply changes:";
|
|
||||||
const pluginSuffix = plugins.length === 1 ? " requires a reload to apply changes." : ".";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ErrorCard {...props} style={{ padding: "1em", display: "grid", gridTemplateColumns: "1fr auto", gap: 8, ...props.style }}>
|
|
||||||
<span style={{ margin: "auto 0" }}>
|
|
||||||
{pluginPrefix} <code>{plugins.join(", ")}</code>{pluginSuffix}
|
|
||||||
</span>
|
|
||||||
<Button look={Button.Looks.INVERTED} onClick={() => location.reload()}>Reload</Button>
|
|
||||||
</ErrorCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
|
||||||
plugin: Plugin;
|
|
||||||
disabled: boolean;
|
|
||||||
onRestartNeeded(name: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave }: PluginCardProps) {
|
|
||||||
const settings = useSettings();
|
|
||||||
const pluginSettings = settings.plugins[plugin.name];
|
|
||||||
|
|
||||||
const [iconHover, setIconHover] = React.useState(false);
|
|
||||||
|
|
||||||
function isEnabled() {
|
|
||||||
return pluginSettings?.enabled || plugin.started;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
|
||||||
pluginSettings.enabled = true;
|
|
||||||
onRestartNeeded(plugin.name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
|
|
||||||
if (plugin.patches) {
|
|
||||||
pluginSettings.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) {
|
|
||||||
pluginSettings.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginSettings.enabled = !wasEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex style={styles.PluginsGridItem} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
|
||||||
<Switch
|
|
||||||
onChange={toggleEnabled}
|
|
||||||
disabled={disabled}
|
|
||||||
value={isEnabled()}
|
|
||||||
note={<Text variant="text-md/normal" style={{
|
|
||||||
height: 40,
|
|
||||||
overflow: "hidden",
|
|
||||||
// mfw css is so bad you need whatever this is to get multi line overflow ellipsis to work
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
display: "-webkit-box", // firefox users will cope (it doesn't support it)
|
|
||||||
WebkitLineClamp: 2,
|
|
||||||
lineClamp: 2,
|
|
||||||
WebkitBoxOrient: "vertical",
|
|
||||||
boxOrient: "vertical"
|
|
||||||
}}>
|
|
||||||
{plugin.description}
|
|
||||||
</Text>}
|
|
||||||
hideBorder={true}
|
|
||||||
>
|
|
||||||
<Flex style={{ marginTop: "auto", width: "100%", height: "100%", alignItems: "center" }}>
|
|
||||||
<Text variant="text-md/bold" style={{ flexGrow: "1" }}>{plugin.name}</Text>
|
|
||||||
<button role="switch" onClick={() => openModal()} style={styles.SettingsIcon} className="button-12Fmur">
|
|
||||||
{plugin.options
|
|
||||||
? <CogWheel
|
|
||||||
style={{ color: iconHover ? "" : "var(--text-muted)" }}
|
|
||||||
onMouseEnter={() => setIconHover(true)}
|
|
||||||
onMouseLeave={() => setIconHover(false)}
|
|
||||||
/>
|
|
||||||
: <InfoIcon
|
|
||||||
width="24" height="24"
|
|
||||||
style={{ color: iconHover ? "" : "var(--text-muted)" }}
|
|
||||||
onMouseEnter={() => setIconHover(true)}
|
|
||||||
onMouseLeave={() => setIconHover(false)}
|
|
||||||
/>}
|
|
||||||
</button>
|
|
||||||
</Flex>
|
|
||||||
</Switch>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ErrorBoundary.wrap(function Settings() {
|
|
||||||
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: "all" });
|
|
||||||
|
|
||||||
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
|
||||||
const onStatusChange = (status: string) => setSearchValue(prev => ({ ...prev, status }));
|
|
||||||
|
|
||||||
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
|
|
||||||
const showEnabled = searchValue.status === "enabled" || searchValue.status === "all";
|
|
||||||
const showDisabled = searchValue.status === "disabled" || searchValue.status === "all";
|
|
||||||
const enabled = settings.plugins[plugin.name]?.enabled || plugin.started;
|
|
||||||
return (
|
|
||||||
((showEnabled && enabled) || (showDisabled && !enabled)) &&
|
|
||||||
(
|
|
||||||
plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) ||
|
|
||||||
plugin.description.toLowerCase().includes(searchValue.value.toLowerCase())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Forms.FormSection>
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
|
||||||
Filters
|
|
||||||
</Forms.FormTitle>
|
|
||||||
|
|
||||||
<ReloadRequiredCard plugins={[...changes.getChanges()]} style={{ marginBottom: 16 }} />
|
|
||||||
|
|
||||||
<div style={styles.FiltersBar}>
|
|
||||||
<TextInput value={searchValue.value} placeholder={"Search for a plugin..."} onChange={onSearch} style={{ marginBottom: 24 }} />
|
|
||||||
<div className={InputStyles.inputWrapper}>
|
|
||||||
<Select
|
|
||||||
className={InputStyles.inputDefault}
|
|
||||||
options={[
|
|
||||||
{ label: "Show All", value: "all", default: true },
|
|
||||||
{ label: "Show Enabled", value: "enabled" },
|
|
||||||
{ label: "Show Disabled", value: "disabled" }
|
|
||||||
]}
|
|
||||||
serialize={String}
|
|
||||||
select={onStatusChange}
|
|
||||||
isSelected={v => v === searchValue.status}
|
|
||||||
closeOnSelect={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
|
|
||||||
|
|
||||||
<div style={styles.PluginsGrid}>
|
|
||||||
{sortedPlugins?.length ? sortedPlugins
|
|
||||||
.filter(a => !a.required && !dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
|
||||||
.map(plugin => {
|
|
||||||
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
|
||||||
const dependency = enabledDependants?.length;
|
|
||||||
return <PluginCard
|
|
||||||
onRestartNeeded={name => changes.add(name)}
|
|
||||||
disabled={plugin.required || !!dependency}
|
|
||||||
plugin={plugin}
|
|
||||||
key={plugin.name}
|
|
||||||
/>;
|
|
||||||
})
|
|
||||||
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<Forms.FormDivider />
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
|
||||||
Required Plugins
|
|
||||||
</Forms.FormTitle>
|
|
||||||
<div style={styles.PluginsGrid}>
|
|
||||||
{sortedPlugins?.length ? sortedPlugins
|
|
||||||
.filter(a => a.required || dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
|
||||||
.map(plugin => {
|
|
||||||
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
|
||||||
const dependency = enabledDependants?.length;
|
|
||||||
const tooltipText = plugin.required
|
|
||||||
? "This plugin is required for Vencord to function."
|
|
||||||
: makeDependencyList(dependencyCheck(plugin.name, depMap));
|
|
||||||
return <Tooltip text={tooltipText} key={plugin.name}>
|
|
||||||
{({ onMouseLeave, onMouseEnter }) => (
|
|
||||||
<PluginCard
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onRestartNeeded={name => changes.handleChange(name)}
|
|
||||||
disabled={plugin.required || !!dependency}
|
|
||||||
plugin={plugin}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Tooltip>;
|
|
||||||
})
|
|
||||||
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</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 style={{ margin: "0 auto" }}>{dep}</Forms.FormText>)}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dependencyCheck(pluginName: string, depMap: Record<string, string[]>): string[] {
|
|
||||||
return depMap[pluginName]?.filter(d => Settings.plugins[d].enabled) || [];
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const PluginsGrid: React.CSSProperties = {
|
|
||||||
marginTop: 16,
|
|
||||||
display: "grid",
|
|
||||||
gridGap: 16,
|
|
||||||
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PluginsGridItem: React.CSSProperties = {
|
|
||||||
backgroundColor: "var(--background-modifier-selected)",
|
|
||||||
color: "var(--interactive-active)",
|
|
||||||
borderRadius: 3,
|
|
||||||
cursor: "pointer",
|
|
||||||
display: "block",
|
|
||||||
height: "min-content",
|
|
||||||
padding: 10,
|
|
||||||
width: "100%",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FiltersBar: React.CSSProperties = {
|
|
||||||
gap: 10,
|
|
||||||
height: 40,
|
|
||||||
gridTemplateColumns: "1fr 150px",
|
|
||||||
display: "grid"
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SettingsIcon: React.CSSProperties = {
|
|
||||||
height: "24px",
|
|
||||||
width: "24px",
|
|
||||||
padding: "0",
|
|
||||||
background: "transparent",
|
|
||||||
marginRight: 8
|
|
||||||
};
|
|
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,31 +1,12 @@
|
|||||||
/*
|
import gitHash from "git-hash";
|
||||||
* Vencord, a modification for Discord's desktop app
|
import { changes, checkForUpdates, getRepo, rebuild, update, UpdateLogger, updateError } from '../utils/updater';
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
import { React, Forms, Button, Margins, Alerts, Card, Parser, Toasts } from '../webpack/common';
|
||||||
*
|
import { Flex } from "./Flex";
|
||||||
* This program is free software: you can redistribute it and/or modify
|
import { useAwaiter } from '../utils/misc';
|
||||||
* it under the terms of the GNU General Public License as published by
|
import { Link } from "./Link";
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
* (at your option) any later version.
|
import { ErrorCard } from "./ErrorCard";
|
||||||
*
|
|
||||||
* 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 { 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, Toasts } from "@webpack/common";
|
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
|
||||||
|
|
||||||
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
||||||
return async () => {
|
return async () => {
|
||||||
@ -62,15 +43,51 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
|
|||||||
dispatcher(false);
|
dispatcher(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
interface CommonProps {
|
export default ErrorBoundary.wrap(function Updater() {
|
||||||
repo: string;
|
const [repo, err, repoPending] = useAwaiter(getRepo, "Loading...");
|
||||||
repoPending: boolean;
|
const [isChecking, setIsChecking] = React.useState(false);
|
||||||
}
|
const [isUpdating, setIsUpdating] = React.useState(false);
|
||||||
|
const [updates, setUpdates] = React.useState(changes);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (err)
|
||||||
|
UpdateLogger.error("Failed to retrieve repo", err);
|
||||||
|
}, [err]);
|
||||||
|
|
||||||
|
const isOutdated = updates?.length > 0;
|
||||||
|
|
||||||
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
|
|
||||||
return (
|
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>
|
||||||
|
<ErrorCard style={{ padding: "1em" }}>
|
||||||
|
<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" }}>
|
<Card style={{ padding: ".5em" }}>
|
||||||
{updates.map(({ hash, author, message }) => (
|
{updates.map(({ hash, author, message }) => (
|
||||||
<div>
|
<div>
|
||||||
@ -84,34 +101,9 @@ function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Updatable(props: CommonProps) {
|
|
||||||
const [updates, setUpdates] = React.useState(changes);
|
|
||||||
const [isChecking, setIsChecking] = React.useState(false);
|
|
||||||
const [isUpdating, setIsUpdating] = React.useState(false);
|
|
||||||
|
|
||||||
const isOutdated = (updates?.length ?? 0) > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!updates && updateError ? (
|
|
||||||
<>
|
|
||||||
<Forms.FormText>Failed to check updates. Check the console for more info</Forms.FormText>
|
|
||||||
<ErrorCard style={{ padding: "1em" }}>
|
|
||||||
<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 && <Changes updates={updates} {...props} />}
|
<Flex className={`${Margins.marginBottom8} ${Margins.marginTop8}`}>
|
||||||
|
|
||||||
<Flex className={classes(Margins.marginBottom8, Margins.marginTop8)}>
|
|
||||||
{isOutdated && <Button
|
{isOutdated && <Button
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
disabled={isUpdating || isChecking}
|
disabled={isUpdating || isChecking}
|
||||||
@ -163,54 +155,6 @@ function Updatable(props: CommonProps) {
|
|||||||
Check for Updates
|
Check for Updates
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</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 [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>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{isNewer ? <Newer {...commonProps} /> : <Updatable {...commonProps} />}
|
|
||||||
</Forms.FormSection >
|
</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,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 ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Flex } from "@components/Flex";
|
|
||||||
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
|
||||||
import { Button, Card, Forms, Margins, Text } from "@webpack/common";
|
|
||||||
|
|
||||||
function BackupRestoreTab() {
|
|
||||||
return (
|
|
||||||
<Forms.FormSection title="Settings Sync">
|
|
||||||
<Card style={{
|
|
||||||
backgroundColor: "var(--info-warning-background)",
|
|
||||||
borderColor: "var(--info-warning-foreground)",
|
|
||||||
color: "var(--info-warning-text)",
|
|
||||||
padding: "1em",
|
|
||||||
marginBottom: "0.5em",
|
|
||||||
}}>
|
|
||||||
<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,135 +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"
|
|
||||||
}} 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 ref = React.useRef<HTMLTextAreaElement>();
|
|
||||||
|
|
||||||
function onBlur() {
|
|
||||||
settings.themeLinks = [...new Set(
|
|
||||||
ref.current!.value
|
|
||||||
.trim()
|
|
||||||
.split(/\n+/)
|
|
||||||
.map(s => s.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card style={{
|
|
||||||
padding: "1em",
|
|
||||||
marginBottom: "1em",
|
|
||||||
marginTop: "1em"
|
|
||||||
}}>
|
|
||||||
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
|
|
||||||
<Forms.FormText>One link per line</Forms.FormText>
|
|
||||||
<Forms.FormText>Be careful to use the raw links or github.io links!</Forms.FormText>
|
|
||||||
<Forms.FormDivider />
|
|
||||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
|
||||||
<div>
|
|
||||||
<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 github 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)"
|
|
||||||
}}
|
|
||||||
ref={ref}
|
|
||||||
defaultValue={settings.themeLinks.join("\n")}
|
|
||||||
className={TextAreaProps.textarea}
|
|
||||||
placeholder="Theme Links"
|
|
||||||
spellCheck={false}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
<Validators themeLinks={settings.themeLinks} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,149 +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 DonateButton from "@components/DonateButton";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
|
||||||
import { useAwaiter } from "@utils/misc";
|
|
||||||
import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common";
|
|
||||||
|
|
||||||
const st = (style: string) => `vcSettings${style}`;
|
|
||||||
|
|
||||||
function VencordSettings() {
|
|
||||||
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
|
|
||||||
fallbackValue: "Loading..."
|
|
||||||
});
|
|
||||||
const settings = useSettings();
|
|
||||||
|
|
||||||
const [donateImage] = React.useState(
|
|
||||||
Math.random() > 0.5
|
|
||||||
? "https://cdn.discordapp.com/emojis/1026533090627174460.png"
|
|
||||||
: "https://media.discordapp.net/stickers/1039992459209490513.png"
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<DonateCard image={donateImage} />
|
|
||||||
<Forms.FormSection title="Quick Actions">
|
|
||||||
<Card className={st("QuickActionCard")}>
|
|
||||||
{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 title="Settings">
|
|
||||||
<Forms.FormText className={Margins.marginBottom20}>
|
|
||||||
Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
|
|
||||||
</Forms.FormText>
|
|
||||||
<Switch
|
|
||||||
value={settings.useQuickCss}
|
|
||||||
onChange={(v: boolean) => settings.useQuickCss = v}
|
|
||||||
note="Loads styles from your QuickCss file">
|
|
||||||
Use QuickCss
|
|
||||||
</Switch>
|
|
||||||
{!IS_WEB && (
|
|
||||||
<React.Fragment>
|
|
||||||
<Switch
|
|
||||||
value={settings.enableReactDevtools}
|
|
||||||
onChange={(v: boolean) => settings.enableReactDevtools = v}
|
|
||||||
note="Requires a full restart">
|
|
||||||
Enable React Developer Tools
|
|
||||||
</Switch>
|
|
||||||
<Switch
|
|
||||||
value={settings.notifyAboutUpdates}
|
|
||||||
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
|
||||||
note="Shows a Toast on StartUp">
|
|
||||||
Get notified about new Updates
|
|
||||||
</Switch>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</Forms.FormSection>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface DonateCardProps {
|
|
||||||
image: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DonateCard({ image }: DonateCardProps) {
|
|
||||||
return (
|
|
||||||
<Card style={{
|
|
||||||
padding: "1em",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
marginBottom: "1em",
|
|
||||||
marginTop: "1em"
|
|
||||||
}}>
|
|
||||||
<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: "rotate(10deg)" }}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ErrorBoundary.wrap(VencordSettings);
|
|
@ -1,92 +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 { findByCodeLazy } from "@webpack";
|
|
||||||
import { Forms, Router, Text } from "@webpack/common";
|
|
||||||
|
|
||||||
import cssText from "~fileContent/settingsStyles.css";
|
|
||||||
|
|
||||||
import BackupRestoreTab from "./BackupRestoreTab";
|
|
||||||
import PluginsTab from "./PluginsTab";
|
|
||||||
import ThemesTab from "./ThemesTab";
|
|
||||||
import Updater from "./Updater";
|
|
||||||
import VencordSettings from "./VencordTab";
|
|
||||||
|
|
||||||
const style = document.createElement("style");
|
|
||||||
style.textContent = cssText;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
|
|
||||||
const st = (style: string) => `vcSettings${style}`;
|
|
||||||
|
|
||||||
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={st("TabBar")}
|
|
||||||
selectedItem={tab}
|
|
||||||
onItemSelect={Router.open}
|
|
||||||
>
|
|
||||||
{Object.entries(SettingsTabs).map(([key, { name, component }]) => {
|
|
||||||
if (!component) return null;
|
|
||||||
return <TabBar.Item
|
|
||||||
id={key}
|
|
||||||
className={st("TabBarItem")}
|
|
||||||
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,23 +0,0 @@
|
|||||||
.vcSettingsTabBar {
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-bottom: -2px;
|
|
||||||
border-bottom: 2px solid var(--background-modifier-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vcSettingsTabBarItem {
|
|
||||||
margin-right: 32px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
margin-bottom: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vcSettingsQuickActionCard {
|
|
||||||
padding: 1em;
|
|
||||||
display: flex;
|
|
||||||
gap: 1em;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
@ -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 @@
|
|||||||
/*
|
export { default as Settings } from "./Settings";
|
||||||
* Vencord, a modification for Discord's desktop app
|
export { default as Updater } from "./Updater";
|
||||||
* 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";
|
|
||||||
|
@ -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>
|
|
@ -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 Logger from "@utils/Logger";
|
|
||||||
|
|
||||||
if (IS_DEV) {
|
|
||||||
var traces = {} as Record<string, [number, any[]]>;
|
|
||||||
var logger = new Logger("Tracer", "#FFD166");
|
|
||||||
}
|
|
||||||
|
|
||||||
const noop = function () { };
|
|
||||||
|
|
||||||
export const beginTrace = !IS_DEV ? noop :
|
|
||||||
function beginTrace(name: string, ...args: any[]) {
|
|
||||||
if (name in traces)
|
|
||||||
throw new Error(`Trace ${name} already exists!`);
|
|
||||||
|
|
||||||
traces[name] = [performance.now(), args];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const finishTrace = !IS_DEV ? noop : function finishTrace(name: string) {
|
|
||||||
const end = performance.now();
|
|
||||||
|
|
||||||
const [start, args] = traces[name];
|
|
||||||
delete traces[name];
|
|
||||||
|
|
||||||
logger.debug(`${name} took ${end - start}ms`, args);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Func = (...args: any[]) => any;
|
|
||||||
type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;
|
|
||||||
|
|
||||||
const noopTracer =
|
|
||||||
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
|
|
||||||
|
|
||||||
export const traceFunction = !IS_DEV
|
|
||||||
? noopTracer
|
|
||||||
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
|
|
||||||
return function (this: any, ...args: Parameters<F>) {
|
|
||||||
const traceName = mapper?.(...args) ?? name;
|
|
||||||
|
|
||||||
beginTrace(traceName, ...arguments);
|
|
||||||
try {
|
|
||||||
return f.apply(this, args);
|
|
||||||
} finally {
|
|
||||||
finishTrace(traceName);
|
|
||||||
}
|
|
||||||
} as F;
|
|
||||||
};
|
|
46
src/globals.d.ts
vendored
46
src/globals.d.ts
vendored
@ -1,60 +1,16 @@
|
|||||||
/*
|
|
||||||
* 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
/**
|
|
||||||
* This exists only at build time, so references to it in patches should insert it
|
|
||||||
* via String interpolation OR use different replacement code based on this
|
|
||||||
* but NEVER reference it inside the patched code
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // BAD
|
|
||||||
* replace: "IS_WEB?foo:bar"
|
|
||||||
* // GOOD
|
|
||||||
* replace: IS_WEB ? "foo" : "bar"
|
|
||||||
* // also good
|
|
||||||
* replace: `${IS_WEB}?foo:bar`
|
|
||||||
*/
|
|
||||||
export var IS_WEB: boolean;
|
|
||||||
export var IS_DEV: boolean;
|
|
||||||
export var IS_STANDALONE: boolean;
|
|
||||||
|
|
||||||
export var VencordNative: typeof import("./VencordNative").default;
|
export var VencordNative: typeof import("./VencordNative").default;
|
||||||
export var Vencord: typeof import("./Vencord");
|
export var Vencord: typeof import("./Vencord");
|
||||||
export var appSettings: {
|
export var appSettings: {
|
||||||
set(setting: string, v: any): void;
|
set(setting: string, v: any): void;
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* Only available when running in Electron, undefined on web.
|
|
||||||
* Thus, avoid using this or only use it inside an {@link IS_WEB} guard.
|
|
||||||
*
|
|
||||||
* If you really must use it, mark your plugin as Desktop App only via
|
|
||||||
* `target: "DESKTOP"`
|
|
||||||
*/
|
|
||||||
export var DiscordNative: any;
|
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
webpackChunkdiscord_app: {
|
webpackChunkdiscord_app: {
|
||||||
push(chunk: any): any;
|
push(chunk: any): any;
|
||||||
pop(): any;
|
pop(): any;
|
||||||
};
|
};
|
||||||
[k: string]: any;
|
[k: PropertyKey]: any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { app } from "electron";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR ?? (
|
|
||||||
process.env.DISCORD_USER_DATA_DIR
|
|
||||||
? join(process.env.DISCORD_USER_DATA_DIR, "..", "VencordData")
|
|
||||||
: join(app.getPath("userData"), "..", "Vencord")
|
|
||||||
);
|
|
||||||
export const SETTINGS_DIR = join(DATA_DIR, "settings");
|
|
||||||
export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
|
|
||||||
export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
|
|
||||||
export const ALLOWED_PROTOCOLS = [
|
|
||||||
"https:",
|
|
||||||
"http:",
|
|
||||||
"steam:",
|
|
||||||
"spotify:"
|
|
||||||
];
|
|
@ -1,57 +0,0 @@
|
|||||||
/* eslint-disable header/header */
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* crxToZip
|
|
||||||
* Copyright (c) 2013 Rob Wu <rob@robwu.nl>
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function crxToZip(buf: Buffer) {
|
|
||||||
function calcLength(a: number, b: number, c: number, d: number) {
|
|
||||||
let length = 0;
|
|
||||||
|
|
||||||
length += a << 0;
|
|
||||||
length += b << 8;
|
|
||||||
length += c << 16;
|
|
||||||
length += d << 24 >>> 0;
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 50 4b 03 04
|
|
||||||
// This is actually a zip file
|
|
||||||
if (buf[0] === 80 && buf[1] === 75 && buf[2] === 3 && buf[3] === 4) {
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 43 72 32 34 (Cr24)
|
|
||||||
if (buf[0] !== 67 || buf[1] !== 114 || buf[2] !== 50 || buf[3] !== 52) {
|
|
||||||
throw new Error("Invalid header: Does not start with Cr24");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 02 00 00 00
|
|
||||||
// or
|
|
||||||
// 03 00 00 00
|
|
||||||
const isV3 = buf[4] === 3;
|
|
||||||
const isV2 = buf[4] === 2;
|
|
||||||
|
|
||||||
if ((!isV2 && !isV3) || buf[5] || buf[6] || buf[7]) {
|
|
||||||
throw new Error("Unexpected crx format version number.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isV2) {
|
|
||||||
const publicKeyLength = calcLength(buf[8], buf[9], buf[10], buf[11]);
|
|
||||||
const signatureLength = calcLength(buf[12], buf[13], buf[14], buf[15]);
|
|
||||||
|
|
||||||
// 16 = Magic number (4), CRX format version (4), lengths (2x4)
|
|
||||||
const zipStartOffset = 16 + publicKeyLength + signatureLength;
|
|
||||||
|
|
||||||
return buf.subarray(zipStartOffset, buf.length);
|
|
||||||
}
|
|
||||||
// v3 format has header size and then header
|
|
||||||
const headerSize = calcLength(buf[8], buf[9], buf[10], buf[11]);
|
|
||||||
const zipStartOffset = 12 + headerSize;
|
|
||||||
|
|
||||||
return buf.subarray(zipStartOffset, buf.length);
|
|
||||||
}
|
|
@ -1,76 +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 { session } from "electron";
|
|
||||||
import { unzip } from "fflate";
|
|
||||||
import { constants as fsConstants } from "fs";
|
|
||||||
import { access, mkdir, rm, writeFile } from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
import { DATA_DIR } from "./constants";
|
|
||||||
import { crxToZip } from "./crxToZip";
|
|
||||||
import { get } from "./simpleGet";
|
|
||||||
|
|
||||||
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
|
|
||||||
|
|
||||||
async function extract(data: Buffer, outDir: string) {
|
|
||||||
await mkdir(outDir, { recursive: true });
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
unzip(data, (err, files) => {
|
|
||||||
if (err) return void reject(err);
|
|
||||||
Promise.all(Object.keys(files).map(async f => {
|
|
||||||
// Signature stuff
|
|
||||||
// 'Cannot load extension with file or directory name
|
|
||||||
// _metadata. Filenames starting with "_" are reserved for use by the system.';
|
|
||||||
if (f.startsWith("_metadata/")) return;
|
|
||||||
|
|
||||||
if (f.endsWith("/")) return void mkdir(join(outDir, f), { recursive: true });
|
|
||||||
|
|
||||||
const pathElements = f.split("/");
|
|
||||||
const name = pathElements.pop()!;
|
|
||||||
const directories = pathElements.join("/");
|
|
||||||
const dir = join(outDir, directories);
|
|
||||||
|
|
||||||
if (directories) {
|
|
||||||
await mkdir(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeFile(join(dir, name), files[f]);
|
|
||||||
}))
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(err => {
|
|
||||||
rm(outDir, { recursive: true, force: true });
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function installExt(id: string) {
|
|
||||||
const extDir = join(extensionCacheDir, `${id}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await access(extDir, fsConstants.F_OK);
|
|
||||||
} catch (err) {
|
|
||||||
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`;
|
|
||||||
const buf = await get(url);
|
|
||||||
await extract(crxToZip(buf), extDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
session.defaultSession.loadExtension(extDir);
|
|
||||||
}
|
|
@ -1,34 +1,16 @@
|
|||||||
/*
|
import { app, BrowserWindow, desktopCapturer, ipcMain, shell } from "electron";
|
||||||
* Vencord, a modification for Discord's desktop app
|
import { mkdirSync, readFileSync, watch } from "fs";
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
import { open, readFile, writeFile } from "fs/promises";
|
||||||
*
|
import { join } from 'path';
|
||||||
* This program is free software: you can redistribute it and/or modify
|
import { debounce } from "../utils/debounce";
|
||||||
* it under the terms of the GNU General Public License as published by
|
import IpcEvents from '../utils/IpcEvents';
|
||||||
* 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 "./updater";
|
import "./updater";
|
||||||
|
|
||||||
import { debounce } from "@utils/debounce";
|
const DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
const SETTINGS_DIR = join(DATA_DIR, "settings");
|
||||||
import { Queue } from "@utils/Queue";
|
const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
|
||||||
import { BrowserWindow, desktopCapturer, ipcMain, shell } from "electron";
|
const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
|
||||||
import { mkdirSync, readFileSync, watch } from "fs";
|
|
||||||
import { open, readFile, writeFile } from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
|
|
||||||
|
|
||||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
|
|
||||||
|
|
||||||
mkdirSync(SETTINGS_DIR, { recursive: true });
|
mkdirSync(SETTINGS_DIR, { recursive: true });
|
||||||
|
|
||||||
@ -36,7 +18,7 @@ function readCss() {
|
|||||||
return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
|
return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readSettings() {
|
function readSettings() {
|
||||||
try {
|
try {
|
||||||
return readFileSync(SETTINGS_FILE, "utf-8");
|
return readFileSync(SETTINGS_FILE, "utf-8");
|
||||||
} catch {
|
} catch {
|
||||||
@ -47,33 +29,18 @@ export function readSettings() {
|
|||||||
// Fix for screensharing in Electron >= 17
|
// Fix for screensharing in Electron >= 17
|
||||||
ipcMain.handle(IpcEvents.GET_DESKTOP_CAPTURE_SOURCES, (_, opts) => desktopCapturer.getSources(opts));
|
ipcMain.handle(IpcEvents.GET_DESKTOP_CAPTURE_SOURCES, (_, opts) => desktopCapturer.getSources(opts));
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
|
ipcMain.handle(IpcEvents.OPEN_PATH, (_, ...pathElements) => shell.openPath(join(...pathElements)));
|
||||||
|
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => shell.openExternal(url));
|
||||||
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
|
||||||
try {
|
|
||||||
var { protocol } = new URL(url);
|
|
||||||
} catch {
|
|
||||||
throw "Malformed URL";
|
|
||||||
}
|
|
||||||
if (!ALLOWED_PROTOCOLS.includes(protocol))
|
|
||||||
throw "Disallowed protocol.";
|
|
||||||
|
|
||||||
shell.openExternal(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
const cssWriteQueue = new Queue();
|
|
||||||
const settingsWriteQueue = new Queue();
|
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
||||||
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
|
|
||||||
cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css))
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
||||||
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
|
ipcMain.on(IpcEvents.GET_SETTINGS, (e) => e.returnValue = readSettings());
|
||||||
|
|
||||||
|
ipcMain.handle(IpcEvents.GET_SWC_WASM, () => readFile(join(__dirname, "swc.wasm")));
|
||||||
|
let settingsWriteQueue = Promise.resolve();
|
||||||
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
|
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
|
||||||
settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s));
|
settingsWriteQueue = settingsWriteQueue.then(() => writeFile(SETTINGS_FILE, s));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -85,17 +52,3 @@ export function initIpc(mainWindow: BrowserWindow) {
|
|||||||
}, 50));
|
}, 50));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
|
||||||
const win = new BrowserWindow({
|
|
||||||
title: "QuickCss Editor",
|
|
||||||
autoHideMenuBar: true,
|
|
||||||
darkTheme: true,
|
|
||||||
webPreferences: {
|
|
||||||
preload: join(__dirname, "preload.js"),
|
|
||||||
contextIsolation: true,
|
|
||||||
nodeIntegration: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
|
|
||||||
});
|
|
||||||
|
@ -1,37 +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 https from "https";
|
|
||||||
|
|
||||||
export function get(url: string, options: https.RequestOptions = {}) {
|
|
||||||
return new Promise<Buffer>((resolve, reject) => {
|
|
||||||
https.get(url, options, res => {
|
|
||||||
const { statusCode, statusMessage, headers } = res;
|
|
||||||
if (statusCode! >= 400)
|
|
||||||
return void reject(`${statusCode}: ${statusMessage} - ${url}`);
|
|
||||||
if (statusCode! >= 300)
|
|
||||||
return void resolve(get(headers.location!, options));
|
|
||||||
|
|
||||||
const chunks = [] as Buffer[];
|
|
||||||
res.on("error", reject);
|
|
||||||
|
|
||||||
res.on("data", chunk => chunks.push(chunk));
|
|
||||||
res.once("end", () => resolve(Buffer.concat(chunks)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
94
src/ipcMain/updater.ts
Normal file
94
src/ipcMain/updater.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { ipcMain } from 'electron';
|
||||||
|
import { promisify } from "util";
|
||||||
|
import IpcEvents from "../utils/IpcEvents";
|
||||||
|
import { execFile as cpExecFile } from 'child_process';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { createReadStream } from 'fs';
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
|
const VENCORD_SRC_DIR = join(__dirname, "..");
|
||||||
|
|
||||||
|
const execFile = promisify(cpExecFile);
|
||||||
|
|
||||||
|
function git(...args: string[]) {
|
||||||
|
return execFile("git", args, {
|
||||||
|
cwd: VENCORD_SRC_DIR
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calculateHashes() {
|
||||||
|
const hashes = {} as Record<string, string>;
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
["patcher.js", "preload.js", "renderer.js"].map(file => new Promise<void>(r => {
|
||||||
|
const fis = createReadStream(join(__dirname, file));
|
||||||
|
const hash = createHash("sha1", { encoding: "hex" });
|
||||||
|
fis.once("end", () => {
|
||||||
|
hash.end();
|
||||||
|
hashes[file] = hash.read();
|
||||||
|
r();
|
||||||
|
});
|
||||||
|
fis.pipe(hash);
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return hashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeErrors(func: (...args: any[]) => any) {
|
||||||
|
return async function () {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
value: await func(...arguments)
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: e instanceof Error ? {
|
||||||
|
// prototypes get lost, so turn error into plain object
|
||||||
|
...e
|
||||||
|
} : e
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRepo() {
|
||||||
|
const res = await git("remote", "get-url", "origin");
|
||||||
|
return res.stdout.trim()
|
||||||
|
.replace(/git@(.+):/, "https://$1/")
|
||||||
|
.replace(/\.git$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calculateGitChanges() {
|
||||||
|
await git("fetch");
|
||||||
|
|
||||||
|
const res = await git("log", `HEAD...origin/main`, "--pretty=format:%an/%h/%s");
|
||||||
|
|
||||||
|
const commits = res.stdout.trim();
|
||||||
|
return commits ? commits.split("\n").map(line => {
|
||||||
|
const [author, hash, ...rest] = line.split("/");
|
||||||
|
return {
|
||||||
|
hash, author, message: rest.join("/")
|
||||||
|
};
|
||||||
|
}) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pull() {
|
||||||
|
const res = await git("pull");
|
||||||
|
return res.stdout.includes("Fast-forward");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function build() {
|
||||||
|
const res = await execFile("node", ["build.mjs"], {
|
||||||
|
cwd: VENCORD_SRC_DIR
|
||||||
|
});
|
||||||
|
return !res.stderr.includes("Build failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
|
||||||
|
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo));
|
||||||
|
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
|
||||||
|
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull));
|
||||||
|
ipcMain.handle(IpcEvents.BUILD, serializeErrors(build));
|
@ -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 { createHash } from "crypto";
|
|
||||||
import { createReadStream } from "fs";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
export async function calculateHashes() {
|
|
||||||
const hashes = {} as Record<string, string>;
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
["patcher.js", "preload.js", "renderer.js"].map(file => new Promise<void>(r => {
|
|
||||||
const fis = createReadStream(join(__dirname, file));
|
|
||||||
const hash = createHash("sha1", { encoding: "hex" });
|
|
||||||
fis.once("end", () => {
|
|
||||||
hash.end();
|
|
||||||
hashes[file] = hash.read();
|
|
||||||
r();
|
|
||||||
});
|
|
||||||
fis.pipe(hash);
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
return hashes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function serializeErrors(func: (...args: any[]) => any) {
|
|
||||||
return async function () {
|
|
||||||
try {
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
value: await func(...arguments)
|
|
||||||
};
|
|
||||||
} catch (e: any) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
error: e instanceof Error ? {
|
|
||||||
// prototypes get lost, so turn error into plain object
|
|
||||||
...e
|
|
||||||
} : e
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,81 +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 { execFile as cpExecFile } from "child_process";
|
|
||||||
import { ipcMain } from "electron";
|
|
||||||
import { join } from "path";
|
|
||||||
import { promisify } from "util";
|
|
||||||
|
|
||||||
import { calculateHashes, serializeErrors } from "./common";
|
|
||||||
|
|
||||||
const VENCORD_SRC_DIR = join(__dirname, "..");
|
|
||||||
|
|
||||||
const execFile = promisify(cpExecFile);
|
|
||||||
|
|
||||||
const isFlatpak = Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
|
|
||||||
|
|
||||||
function git(...args: string[]) {
|
|
||||||
const opts = { cwd: VENCORD_SRC_DIR };
|
|
||||||
|
|
||||||
if (isFlatpak) return execFile("flatpak-spawn", ["--host", "git", ...args], opts);
|
|
||||||
else return execFile("git", args, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRepo() {
|
|
||||||
const res = await git("remote", "get-url", "origin");
|
|
||||||
return res.stdout.trim()
|
|
||||||
.replace(/git@(.+):/, "https://$1/")
|
|
||||||
.replace(/\.git$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function calculateGitChanges() {
|
|
||||||
await git("fetch");
|
|
||||||
|
|
||||||
const res = await git("log", "HEAD...origin/main", "--pretty=format:%an/%h/%s");
|
|
||||||
|
|
||||||
const commits = res.stdout.trim();
|
|
||||||
return commits ? commits.split("\n").map(line => {
|
|
||||||
const [author, hash, ...rest] = line.split("/");
|
|
||||||
return {
|
|
||||||
hash, author, message: rest.join("/")
|
|
||||||
};
|
|
||||||
}) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pull() {
|
|
||||||
const res = await git("pull");
|
|
||||||
return res.stdout.includes("Fast-forward");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function build() {
|
|
||||||
const opts = { cwd: VENCORD_SRC_DIR };
|
|
||||||
|
|
||||||
let res;
|
|
||||||
|
|
||||||
if (isFlatpak) res = await execFile("flatpak-spawn", ["--host", "node", "scripts/build/build.mjs"], opts);
|
|
||||||
else res = await execFile("node", ["scripts/build/build.mjs"], opts);
|
|
||||||
|
|
||||||
return !res.stderr.includes("Build failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
|
|
||||||
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo));
|
|
||||||
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
|
|
||||||
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull));
|
|
||||||
ipcMain.handle(IpcEvents.BUILD, serializeErrors(build));
|
|
@ -1,91 +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 { VENCORD_USER_AGENT } from "@utils/constants";
|
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
|
||||||
import { ipcMain } from "electron";
|
|
||||||
import { writeFile } from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
|
||||||
import gitRemote from "~git-remote";
|
|
||||||
|
|
||||||
import { get } from "../simpleGet";
|
|
||||||
import { calculateHashes, serializeErrors } from "./common";
|
|
||||||
|
|
||||||
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
|
|
||||||
let PendingUpdates = [] as [string, string][];
|
|
||||||
|
|
||||||
async function githubGet(endpoint: string) {
|
|
||||||
return get(API_BASE + endpoint, {
|
|
||||||
headers: {
|
|
||||||
Accept: "application/vnd.github+json",
|
|
||||||
// "All API requests MUST include a valid User-Agent header.
|
|
||||||
// Requests with no User-Agent header will be rejected."
|
|
||||||
"User-Agent": VENCORD_USER_AGENT,
|
|
||||||
// todo: perhaps add support for (optional) api token?
|
|
||||||
// unauthorised rate limit is 60 reqs/h
|
|
||||||
// https://github.com/settings/tokens/new?description=Vencord%20Updater
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function calculateGitChanges() {
|
|
||||||
const isOutdated = await fetchUpdates();
|
|
||||||
if (!isOutdated) return [];
|
|
||||||
|
|
||||||
const res = await githubGet(`/compare/${gitHash}...HEAD`);
|
|
||||||
|
|
||||||
const data = JSON.parse(res.toString("utf-8"));
|
|
||||||
return data.commits.map(c => ({
|
|
||||||
// github api only sends the long sha
|
|
||||||
hash: c.sha.slice(0, 7),
|
|
||||||
author: c.author.login,
|
|
||||||
message: c.commit.message
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchUpdates() {
|
|
||||||
const release = await githubGet("/releases/latest");
|
|
||||||
|
|
||||||
const data = JSON.parse(release.toString());
|
|
||||||
const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
|
|
||||||
if (hash === gitHash)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
data.assets.forEach(({ name, browser_download_url }) => {
|
|
||||||
if (["patcher.js", "preload.js", "renderer.js"].some(s => name.startsWith(s))) {
|
|
||||||
PendingUpdates.push([name, browser_download_url]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function applyUpdates() {
|
|
||||||
await Promise.all(PendingUpdates.map(
|
|
||||||
async ([name, data]) => writeFile(join(__dirname, name), await get(data)))
|
|
||||||
);
|
|
||||||
PendingUpdates = [];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
|
|
||||||
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(() => `https://github.com/${gitRemote}`));
|
|
||||||
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
|
|
||||||
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(fetchUpdates));
|
|
||||||
ipcMain.handle(IpcEvents.BUILD, serializeErrors(applyUpdates));
|
|
@ -1,19 +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(IS_STANDALONE ? "./http" : "./git");
|
|
39
src/modules.d.ts
vendored
39
src/modules.d.ts
vendored
@ -1,39 +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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// eslint-disable-next-line spaced-comment
|
|
||||||
/// <reference types="standalone-electron-types"/>
|
|
||||||
|
|
||||||
declare module "~plugins" {
|
|
||||||
const plugins: Record<string, import("@utils/types").Plugin>;
|
|
||||||
export default plugins;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "~git-hash" {
|
|
||||||
const hash: string;
|
|
||||||
export default hash;
|
|
||||||
}
|
|
||||||
declare module "~git-remote" {
|
|
||||||
const remote: string;
|
|
||||||
export default remote;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "~fileContent/*" {
|
|
||||||
const content: string;
|
|
||||||
export default content;
|
|
||||||
}
|
|
@ -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 { app, autoUpdater } from "electron";
|
|
||||||
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
|
|
||||||
import { basename, dirname, join } from "path";
|
|
||||||
|
|
||||||
const { setAppUserModelId } = app;
|
|
||||||
|
|
||||||
// Apparently requiring Discords updater too early leads into issues,
|
|
||||||
// copied this workaround from powerCord
|
|
||||||
app.setAppUserModelId = function (id: string) {
|
|
||||||
app.setAppUserModelId = setAppUserModelId;
|
|
||||||
|
|
||||||
setAppUserModelId.call(this, id);
|
|
||||||
|
|
||||||
patchUpdater();
|
|
||||||
};
|
|
||||||
|
|
||||||
function isNewer($new: string, old: string) {
|
|
||||||
const newParts = $new.slice(4).split(".").map(Number);
|
|
||||||
const oldParts = old.slice(4).split(".").map(Number);
|
|
||||||
|
|
||||||
for (let i = 0; i < oldParts.length; i++) {
|
|
||||||
if (newParts[i] > oldParts[i]) return true;
|
|
||||||
if (newParts[i] < oldParts[i]) return false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function patchLatest() {
|
|
||||||
const currentAppPath = dirname(process.execPath);
|
|
||||||
const currentVersion = basename(currentAppPath);
|
|
||||||
const discordPath = join(currentAppPath, "..");
|
|
||||||
|
|
||||||
const latestVersion = readdirSync(discordPath).reduce((prev, curr) => {
|
|
||||||
return (curr.startsWith("app-") && isNewer(curr, prev))
|
|
||||||
? curr
|
|
||||||
: prev;
|
|
||||||
}, currentVersion as string);
|
|
||||||
|
|
||||||
if (latestVersion === currentVersion) return;
|
|
||||||
|
|
||||||
const app = join(discordPath, latestVersion, "resources", "app");
|
|
||||||
if (existsSync(app)) return;
|
|
||||||
|
|
||||||
console.info("[Vencord] Detected Host Update. Repatching...");
|
|
||||||
|
|
||||||
const patcherPath = join(__dirname, "patcher.js");
|
|
||||||
mkdirSync(app);
|
|
||||||
writeFileSync(join(app, "package.json"), JSON.stringify({
|
|
||||||
name: "discord",
|
|
||||||
main: "index.js"
|
|
||||||
}));
|
|
||||||
writeFileSync(join(app, "index.js"), `require(${JSON.stringify(patcherPath)});`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we
|
|
||||||
// need to reinject
|
|
||||||
function patchUpdater() {
|
|
||||||
const main = require.main!;
|
|
||||||
const buildInfo = require(join(process.resourcesPath, "build_info.json"));
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (buildInfo?.newUpdater) {
|
|
||||||
const autoStartScript = join(main.filename, "..", "autoStart", "win32.js");
|
|
||||||
const { update } = require(autoStartScript);
|
|
||||||
|
|
||||||
// New Updater Injection
|
|
||||||
require.cache[autoStartScript]!.exports.update = function () {
|
|
||||||
patchLatest();
|
|
||||||
update.apply(this, arguments);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const hostUpdaterScript = join(main.filename, "..", "hostUpdater.js");
|
|
||||||
const { quitAndInstall } = require(hostUpdaterScript);
|
|
||||||
|
|
||||||
// Old Updater Injection
|
|
||||||
require.cache[hostUpdaterScript]!.exports.quitAndInstall = function () {
|
|
||||||
patchLatest();
|
|
||||||
quitAndInstall.apply(this, arguments);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// OpenAsar uses electrons autoUpdater on Windows
|
|
||||||
const { quitAndInstall } = autoUpdater;
|
|
||||||
autoUpdater.quitAndInstall = function () {
|
|
||||||
patchLatest();
|
|
||||||
quitAndInstall.call(this);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
189
src/patcher.ts
189
src/patcher.ts
@ -1,53 +1,11 @@
|
|||||||
/*
|
|
||||||
* 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 { onceDefined } from "@utils/onceDefined";
|
|
||||||
import electron, { app, BrowserWindowConstructorOptions } from "electron";
|
import electron, { app, BrowserWindowConstructorOptions } from "electron";
|
||||||
import { readFileSync } from "fs";
|
import installExt, { REACT_DEVELOPER_TOOLS } from "electron-devtools-installer";
|
||||||
import { dirname, join } from "path";
|
import { join } from "path";
|
||||||
|
import { initIpc } from './ipcMain';
|
||||||
import { initIpc } from "./ipcMain";
|
|
||||||
import { installExt } from "./ipcMain/extensions";
|
|
||||||
import { readSettings } from "./ipcMain/index";
|
|
||||||
|
|
||||||
console.log("[Vencord] Starting up...");
|
console.log("[Vencord] Starting up...");
|
||||||
|
|
||||||
// Our injector file at app/index.js
|
class BrowserWindow extends electron.BrowserWindow {
|
||||||
const injectorPath = require.main!.filename;
|
|
||||||
|
|
||||||
// special discord_arch_electron injection method
|
|
||||||
const asarName = require.main!.path.endsWith("app.asar") ? "_app.asar" : "app.asar";
|
|
||||||
|
|
||||||
// The original app.asar
|
|
||||||
const asarPath = join(dirname(injectorPath), "..", asarName);
|
|
||||||
|
|
||||||
const discordPkg = require(join(asarPath, "package.json"));
|
|
||||||
require.main!.filename = join(asarPath, discordPkg.main);
|
|
||||||
|
|
||||||
// @ts-ignore Untyped method? Dies from cringe
|
|
||||||
app.setAppPath(asarPath);
|
|
||||||
|
|
||||||
if (!process.argv.includes("--vanilla")) {
|
|
||||||
// Repatch after host updates on Windows
|
|
||||||
if (process.platform === "win32")
|
|
||||||
require("./patchWin32Updater");
|
|
||||||
|
|
||||||
class BrowserWindow extends electron.BrowserWindow {
|
|
||||||
constructor(options: BrowserWindowConstructorOptions) {
|
constructor(options: BrowserWindowConstructorOptions) {
|
||||||
if (options?.webPreferences?.preload && options.title) {
|
if (options?.webPreferences?.preload && options.title) {
|
||||||
const original = options.webPreferences.preload;
|
const original = options.webPreferences.preload;
|
||||||
@ -60,121 +18,56 @@ if (!process.argv.includes("--vanilla")) {
|
|||||||
initIpc(this);
|
initIpc(this);
|
||||||
} else super(options);
|
} else super(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object.assign(BrowserWindow, electron.BrowserWindow);
|
Object.assign(BrowserWindow, electron.BrowserWindow);
|
||||||
// esbuild may rename our BrowserWindow, which leads to it being excluded
|
// esbuild may rename our BrowserWindow, which leads to it being excluded
|
||||||
// from getFocusedWindow(), so this is necessary
|
// from getFocusedWindow(), so this is necessary
|
||||||
// https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62
|
// https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62
|
||||||
Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true });
|
Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true });
|
||||||
|
|
||||||
// Replace electrons exports with our custom BrowserWindow
|
// Replace electrons exports with our custom BrowserWindow
|
||||||
const electronPath = require.resolve("electron");
|
const electronPath = require.resolve("electron");
|
||||||
delete require.cache[electronPath]!.exports;
|
delete require.cache[electronPath]!.exports;
|
||||||
require.cache[electronPath]!.exports = {
|
require.cache[electronPath]!.exports = {
|
||||||
...electron,
|
...electron,
|
||||||
BrowserWindow
|
BrowserWindow
|
||||||
};
|
};
|
||||||
|
|
||||||
// Patch appSettings to force enable devtools
|
// Patch appSettings to force enable devtools
|
||||||
onceDefined(global, "appSettings", s =>
|
Object.defineProperty(global, "appSettings", {
|
||||||
s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true)
|
set: (v: typeof global.appSettings) => {
|
||||||
);
|
v.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true);
|
||||||
|
// @ts-ignore
|
||||||
|
delete global.appSettings;
|
||||||
|
global.appSettings = v;
|
||||||
|
},
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
|
||||||
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
||||||
|
|
||||||
electron.app.whenReady().then(() => {
|
|
||||||
// Source Maps! Maybe there's a better way but since the renderer is executed
|
|
||||||
// from a string I don't think any other form of sourcemaps would work
|
|
||||||
electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
|
||||||
let url = unsafeUrl.slice("vencord://".length);
|
|
||||||
if (url.endsWith("/")) url = url.slice(0, -1);
|
|
||||||
switch (url) {
|
|
||||||
case "renderer.js.map":
|
|
||||||
case "preload.js.map":
|
|
||||||
case "patcher.js.map": // doubt
|
|
||||||
cb(join(__dirname, url));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
cb({ statusCode: 403 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const settings = JSON.parse(readSettings());
|
|
||||||
if (settings.enableReactDevtools)
|
|
||||||
installExt("fmkadmapgofadopljbjfkapdkoienihi")
|
|
||||||
.then(() => console.info("[Vencord] Installed React Developer Tools"))
|
|
||||||
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
|
|
||||||
} catch { }
|
|
||||||
|
|
||||||
|
electron.app.whenReady().then(() => {
|
||||||
|
installExt(REACT_DEVELOPER_TOOLS)
|
||||||
|
.then(() => console.info("Installed React DevTools"))
|
||||||
|
.catch((err) => console.error("Failed to install React DevTools", err));
|
||||||
|
|
||||||
// Remove CSP
|
// Remove CSP
|
||||||
type PolicyResult = Record<string, string[]>;
|
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, url }, cb) => {
|
||||||
|
|
||||||
const parsePolicy = (policy: string): PolicyResult => {
|
|
||||||
const result: PolicyResult = {};
|
|
||||||
policy.split(";").forEach(directive => {
|
|
||||||
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
|
|
||||||
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
|
|
||||||
result[directiveKey] = directiveValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
const stringifyPolicy = (policy: PolicyResult): string =>
|
|
||||||
Object.entries(policy)
|
|
||||||
.filter(([, values]) => values?.length)
|
|
||||||
.map(directive => directive.flat().join(" "))
|
|
||||||
.join("; ");
|
|
||||||
|
|
||||||
function patchCsp(headers: Record<string, string[]>, header: string) {
|
|
||||||
if (header in headers) {
|
|
||||||
const csp = parsePolicy(headers[header][0]);
|
|
||||||
|
|
||||||
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
|
|
||||||
csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
|
|
||||||
}
|
|
||||||
// TODO: Restrict this to only imported packages with fixed version.
|
|
||||||
// Perhaps auto generate with esbuild
|
|
||||||
csp["script-src"] ??= [];
|
|
||||||
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
|
|
||||||
headers[header] = [stringifyPolicy(csp)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
|
|
||||||
if (responseHeaders) {
|
if (responseHeaders) {
|
||||||
if (resourceType === "mainFrame")
|
delete responseHeaders["content-security-policy-report-only"];
|
||||||
patchCsp(responseHeaders, "content-security-policy");
|
delete responseHeaders["content-security-policy"];
|
||||||
|
|
||||||
// Fix hosts that don't properly set the css content type, such as
|
// Fix hosts that don't properly set the content type, such as
|
||||||
// raw.githubusercontent.com
|
// raw.githubusercontent.com
|
||||||
if (resourceType === "stylesheet")
|
if (url.endsWith(".css"))
|
||||||
responseHeaders["content-type"] = ["text/css"];
|
responseHeaders["content-type"] = ["text/css"];
|
||||||
}
|
}
|
||||||
cb({ cancel: false, responseHeaders });
|
cb({ cancel: false, responseHeaders });
|
||||||
});
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[Vencord] Loading original Discord app.asar");
|
// Drop science and sentry requests
|
||||||
// Legacy Vencord Injector requires "../app.asar". However, because we
|
electron.session.defaultSession.webRequest.onBeforeRequest(
|
||||||
// restore the require.main above this is messed up, so monkey patch Module._load to
|
{ urls: ["https://*/api/v*/science", "https://sentry.io/*"] },
|
||||||
// redirect such requires
|
(_, callback) => callback({ cancel: true })
|
||||||
// FIXME: remove this eventually
|
);
|
||||||
if (readFileSync(injectorPath, "utf-8").includes('require("../app.asar")')) {
|
});
|
||||||
console.warn("[Vencord] [--> WARNING <--] You have a legacy Vencord install. Please reinject");
|
|
||||||
const Module = require("module");
|
|
||||||
const loadModule = Module._load;
|
|
||||||
Module._load = function (path: string) {
|
|
||||||
if (path === "../app.asar") {
|
|
||||||
Module._load = loadModule;
|
|
||||||
arguments[0] = require.main!.filename;
|
|
||||||
}
|
|
||||||
return loadModule.apply(this, arguments);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
require(require.main!.filename);
|
|
||||||
}
|
|
||||||
|
@ -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 { Settings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import { makeLazy } from "@utils/misc";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "BetterNotesBox",
|
|
||||||
description: "Hide notes or disable spellcheck (Configure in settings!!)",
|
|
||||||
authors: [Devs.Ven],
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "hideNote:",
|
|
||||||
all: true,
|
|
||||||
predicate: makeLazy(() => Vencord.Settings.plugins.BetterNotesBox.hide),
|
|
||||||
replacement: {
|
|
||||||
match: /hideNote:.+?(?=[,}])/g,
|
|
||||||
replace: "hideNote:true",
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
find: "Messages.NOTE_PLACEHOLDER",
|
|
||||||
replacement: {
|
|
||||||
match: /\.NOTE_PLACEHOLDER,/,
|
|
||||||
replace: "$&spellCheck:!Vencord.Settings.plugins.BetterNotesBox.noSpellCheck,"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
options: {
|
|
||||||
hide: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Hide notes",
|
|
||||||
default: false,
|
|
||||||
restartNeeded: true
|
|
||||||
},
|
|
||||||
noSpellCheck: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Disable spellcheck in notes",
|
|
||||||
disabled: () => Settings.plugins.BetterNotesBox.hide,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
15
src/plugins/STFU.ts
Normal file
15
src/plugins/STFU.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import definePlugin from "../utils/types";
|
||||||
|
import { Devs } from '../utils/constants';
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "STFU",
|
||||||
|
description: "Disables the 'HOLD UP' banner in the console",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
patches: [{
|
||||||
|
find: "setDevtoolsCallbacks",
|
||||||
|
replacement: {
|
||||||
|
match: /\.setDevtoolsCallbacks\(.+?else/,
|
||||||
|
replace: ".setDevtoolsCallbacks(null,null);else"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user