Compare commits
1 commit
main
...
ci/contrib
Author | SHA1 | Date | |
---|---|---|---|
|
fb963b6887 |
529 changed files with 9030 additions and 30914 deletions
10
.envrc
10
.envrc
|
@ -1,10 +0,0 @@
|
|||
if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then
|
||||
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs="
|
||||
fi
|
||||
|
||||
watch_file flake.nix
|
||||
watch_file flake.lock
|
||||
if ! use flake . --no-pure-eval
|
||||
then
|
||||
echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2
|
||||
fi
|
98
.eslintrc.json
Normal file
98
.eslintrc.json
Normal file
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"ignorePatterns": ["dist", "browser"],
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"simple-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
|
||||
"simple-header/header": [
|
||||
"error",
|
||||
{
|
||||
"files": ["scripts/header-new.txt", "scripts/header-old.txt"],
|
||||
"templates": { "author": [".*", "Vendicated and contributors"] }
|
||||
}
|
||||
],
|
||||
"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"],
|
||||
"@typescript-eslint/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", {
|
||||
"VariableDeclarator": { "array": false, "object": true },
|
||||
"AssignmentExpression": { "array": false, "object": false }
|
||||
}],
|
||||
"operator-assignment": ["error", "always"],
|
||||
"no-useless-computed-key": "error",
|
||||
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
|
||||
"no-invalid-regexp": "error",
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"no-duplicate-imports": "error",
|
||||
"no-extra-semi": "error",
|
||||
"dot-notation": "error",
|
||||
"no-useless-escape": [
|
||||
"error",
|
||||
{
|
||||
"extra": "i"
|
||||
}
|
||||
],
|
||||
"no-fallthrough": "error",
|
||||
"for-direction": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-dupe-else-if": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-loss-of-precision": "error",
|
||||
"no-misleading-character-class": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-unexpected-multiline": "error",
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-useless-backreference": "error",
|
||||
"use-isnan": "error",
|
||||
"prefer-const": "error",
|
||||
"prefer-spread": "error",
|
||||
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
|
||||
"path-alias/no-relative": "error"
|
||||
}
|
||||
}
|
3
.github/ISSUE_TEMPLATE/blank.yml
vendored
3
.github/ISSUE_TEMPLATE/blank.yml
vendored
|
@ -12,8 +12,7 @@ body:
|
|||
DO NOT USE THIS FORM, unless
|
||||
- you are a vencord contributor
|
||||
- you were given explicit permission to use this form by a moderator in our support server
|
||||
|
||||
DO NOT USE THIS FORM FOR SECURITY RELATED ISSUES. [CREATE A SECURITY ADVISORY INSTEAD.](https://github.com/Vendicated/Vencord/security/advisories/new)
|
||||
- you are filing a security related report
|
||||
|
||||
- type: textarea
|
||||
id: content
|
||||
|
|
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
|
@ -18,21 +18,21 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
- name: Use Node.js 19
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 19
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build web
|
||||
run: pnpm buildWebStandalone
|
||||
run: pnpm buildWeb --standalone
|
||||
|
||||
- name: Build
|
||||
run: pnpm build --standalone
|
||||
|
|
4
.github/workflows/codeberg-mirror.yml
vendored
4
.github/workflows/codeberg-mirror.yml
vendored
|
@ -13,10 +13,10 @@ jobs:
|
|||
if: github.repository == 'Vendicated/Vencord'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1
|
||||
with:
|
||||
target_repo_url: "git@codeberg.org:Vee/cord.git"
|
||||
target_repo_url: "git@codeberg.org:Ven/cord.git"
|
||||
ssh_private_key: ${{ secrets.CODEBERG_SSH_PRIVATE_KEY }}
|
||||
|
|
45
.github/workflows/contributor-issue-enforcement.yml
vendored
Normal file
45
.github/workflows/contributor-issue-enforcement.yml
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
name: Enforce contributor requirement
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- created
|
||||
|
||||
jobs:
|
||||
enforcement:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Delay to allow contributor comment
|
||||
run: sleep 180
|
||||
|
||||
- name: Find potential contributor comment
|
||||
uses: peter-evans/find-comment@d362b58d73ad53d089dd54460397ec1b8b47dbfd
|
||||
id: comment
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
body-includes: /ok
|
||||
|
||||
- name: Check commenter is contributor
|
||||
uses: actions-cool/check-user-permission@a0668c9aec87f3875fc56170b6452a453e9dd819
|
||||
id: comment-contrib
|
||||
if: ${{ !steps.comment.outputs.comment-id }}
|
||||
with:
|
||||
username: ${{ steps.comment.outputs.comment-author }}
|
||||
check-contributor: true
|
||||
|
||||
- name: Check author is contributor
|
||||
uses: actions-cool/check-user-permission@a0668c9aec87f3875fc56170b6452a453e9dd819
|
||||
id: author-contrib
|
||||
if: ${{ !steps.comment-contrib.check-result }}
|
||||
with:
|
||||
# no username means it checks the person who triggered the workflow run i.e. the issue creator
|
||||
check-contributor: true
|
||||
|
||||
- name: Tag and close issue
|
||||
if: ${{ !steps.comment-contrib.check-result && !steps.author-contrib.check-result }}
|
||||
run: |
|
||||
gh issue close $ISSUE -c "Your issue does not comply with our contributor requirement. Please do not ignore the issue template." -r "not planned"
|
||||
gh issue edit $ISSUE --add-label "ignored contributor requirement"
|
||||
gh issue lock $ISSUE
|
||||
env:
|
||||
ISSUE: ${{ github.event.issue.html_url }}
|
10
.github/workflows/publish.yml
vendored
10
.github/workflows/publish.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: check that tag matches package.json version
|
||||
run: |
|
||||
|
@ -20,19 +20,19 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 19
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 19
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build web
|
||||
run: pnpm buildWebStandalone
|
||||
run: pnpm buildWeb --standalone
|
||||
|
||||
- name: Publish extension
|
||||
run: |
|
||||
|
|
35
.github/workflows/reportBrokenPlugins.yml
vendored
35
.github/workflows/reportBrokenPlugins.yml
vendored
|
@ -11,40 +11,31 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ github.event_name == 'schedule' }}
|
||||
- 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:
|
||||
ref: dev
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 19
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm add puppeteer
|
||||
|
||||
- name: Install Google Chrome
|
||||
id: setup-chrome
|
||||
uses: browser-actions/setup-chrome@82b9ce628cc5595478a9ebadc480958a36457dc2
|
||||
with:
|
||||
chrome-version: stable
|
||||
sudo apt-get install -y chromium-browser
|
||||
|
||||
- name: Build Vencord Reporter Version
|
||||
run: pnpm buildReporter
|
||||
- name: Build web
|
||||
run: pnpm buildWeb --standalone
|
||||
|
||||
- name: Create Report
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||
export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
|
||||
export CHROMIUM_BIN=$(which chromium-browser)
|
||||
|
||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||
|
@ -57,7 +48,7 @@ jobs:
|
|||
if: success() || failure() # even run if previous one failed
|
||||
run: |
|
||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||
export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
|
||||
export CHROMIUM_BIN=$(which chromium-browser)
|
||||
export USE_CANARY=true
|
||||
|
||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
||||
|
|
13
.github/workflows/test.yml
vendored
13
.github/workflows/test.yml
vendored
|
@ -1,22 +1,23 @@
|
|||
name: test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -18,10 +18,7 @@ lerna-debug.log*
|
|||
.pnpm-debug.log*
|
||||
*.tsbuildinfo
|
||||
|
||||
src/userplugins
|
||||
|
||||
ExtensionCache/
|
||||
settings/
|
||||
|
||||
# devenv / direnv
|
||||
.devenv*
|
||||
devenv.local.nix
|
||||
.direnv
|
||||
|
|
15
.gitmodules
vendored
15
.gitmodules
vendored
|
@ -1,15 +0,0 @@
|
|||
[submodule "src/userplugins/ToastNotificationsMerge"]
|
||||
path = src/userplugins/ToastNotificationsMerge
|
||||
url = https://github.com/ethan-davies/ToastNotificationsMerge.git
|
||||
[submodule "src/userplugins/vc-message-logger-enhanced"]
|
||||
path = src/userplugins/vc-message-logger-enhanced
|
||||
url = https://github.com/Syncxv/vc-message-logger-enhanced.git
|
||||
[submodule "src/userplugins/DontLeak"]
|
||||
path = src/userplugins/DontLeak
|
||||
url = https://github.com/pernydev/DontLeak.git
|
||||
[submodule "src/userplugins/VencordPlugins"]
|
||||
path = src/userplugins/VencordPlugins
|
||||
url = https://github.com/Kyuuhachi/VencordPlugins.git
|
||||
[submodule "src/userplugins/vc-timezones"]
|
||||
path = src/userplugins/vc-timezones
|
||||
url = https://github.com/Syncxv/vc-timezones.git
|
1
.npmrc
1
.npmrc
|
@ -1,2 +1 @@
|
|||
strict-peer-dependencies=false
|
||||
package-manager-strict=false
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"selector-class-pattern": [
|
||||
"^[a-z][a-zA-Z0-9]*(-[a-z0-9][a-zA-Z0-9]*)*$",
|
||||
{
|
||||
"message": "Expected class selector to be kebab-case with camelCase segments"
|
||||
}
|
||||
]
|
||||
"indentation": 4
|
||||
}
|
||||
}
|
||||
|
|
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
|
@ -1,9 +1,11 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eamodio.gitlens",
|
||||
"EditorConfig.EditorConfig",
|
||||
"ExodiusStudios.comment-anchors",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"GregorBiswanger.json2ts",
|
||||
"stylelint.vscode-stylelint",
|
||||
"Vendicated.vencord-companion"
|
||||
"stylelint.vscode-stylelint"
|
||||
]
|
||||
}
|
||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
|
|
|
@ -16,6 +16,5 @@ DON'T
|
|||
|
||||
Repetitive violations of these guidelines might get your access to the repository restricted.
|
||||
|
||||
If you feel like a user is violating these guidelines or feel treated unfairly, please refrain from vigilantism
|
||||
and instead report the issue to a moderator! The best way is joining our [official Discord community](https://vencord.dev/discord)
|
||||
and opening a modmail ticket.
|
||||
|
||||
If you feel like a user is violating these guidelines or feel treated unfairly, please refrain from publicly challenging them and instead contact a Moderator on our Discord server or send an email to vendicated+conduct@riseup.net!
|
||||
|
|
100
CONTRIBUTING.md
100
CONTRIBUTING.md
|
@ -1,56 +1,82 @@
|
|||
# Contributing to Vencord
|
||||
# Contribution Guide
|
||||
|
||||
Vencord is a community project and welcomes any kind of contribution from anyone!
|
||||
First of all, thank you for contributing! :3
|
||||
|
||||
We have development documentation for new contributors, which can be found at <https://docs.vencord.dev>.
|
||||
To ensure your contribution is robust, please follow the below guide!
|
||||
|
||||
All contributions should be made in accordance with our [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||
For a friendly introduction to plugins, see [Megu's Plugin Guide!](docs/2_PLUGINS.md)
|
||||
|
||||
## How to contribute
|
||||
## Style Guide
|
||||
|
||||
Contributions can be sent via pull requests. If you're new to Git, check [this guide](https://opensource.com/article/19/7/create-pull-request-github).
|
||||
- This project has a very minimal .editorconfig. Make sure your editor supports this!
|
||||
If you are using VSCode, it should automatically recommend you the extension; If not,
|
||||
please install the Editorconfig extension
|
||||
- Try to follow the formatting in the rest of the project and stay consistent
|
||||
- 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
|
||||
|
||||
Pull requests can be made either to the `main` or the `dev` branch. However, unless you're an advanced user, I recommend sticking to `main`. This is because the dev branch might contain unstable changes and be force pushed frequently, which could cause conflicts in your pull request.
|
||||
## Contributing a Plugin
|
||||
|
||||
## Write a plugin
|
||||
Because plugins modify code directly, incompatibilities are a problem.
|
||||
|
||||
Writing a plugin is the primary way to contribute.
|
||||
Thus, 3rd party plugins are not supported, instead all plugins are part of Vencord itself.
|
||||
This way we can ensure compatibility and high quality patches.
|
||||
|
||||
Before starting your plugin:
|
||||
- Check existing pull requests to see if someone is already working on a similar plugin
|
||||
- Check our [plugin requests tracker](https://github.com/Vencord/plugin-requests/issues) to see if there is an existing request, or if the same idea has been rejected
|
||||
- If there isn't an existing request, [open one](https://github.com/Vencord/plugin-requests/issues/new?assignees=&labels=&projects=&template=request.yml) yourself
|
||||
and include that you'd like to work on this yourself. Then wait for feedback to see if the idea even has any chance of being accepted. Or maybe others have some ideas to improve it!
|
||||
- Familarise yourself with our plugin rules below to ensure your plugin is not banned
|
||||
Follow the below guide to make your first plugin!
|
||||
|
||||
### Plugin Rules
|
||||
### Finding the right module to patch
|
||||
|
||||
- No simple slash command plugins like `/cat`. Instead, make a [user installable Discord bot](https://discord.com/developers/docs/change-log#userinstallable-apps-preview)
|
||||
- No simple text replace plugins like Let me Google that for you. The TextReplace plugin can do this
|
||||
- No raw DOM manipulation. Use proper patches and React
|
||||
- No FakeDeafen or FakeMute
|
||||
- No StereoMic
|
||||
- No plugins that simply hide or redesign ui elements. This can be done with CSS
|
||||
- No plugins that interact with specific Discord bots (official Discord apps like Youtube WatchTogether are okay)
|
||||
- No selfbots or API spam (animated status, message pruner, auto reply, nitro snipers, etc)
|
||||
- No untrusted third party APIs. Popular services like Google or GitHub are fine, but absolutely no self hosted ones
|
||||
- No plugins that require the user to enter their own API key
|
||||
- Do not introduce new dependencies unless absolutely necessary and warranted
|
||||
If the thing you want to patch is an action performed when interacting with a part of the UI, use React DevTools.
|
||||
They come preinstalled and can be found as the "Components" tab in DevTools.
|
||||
Use the Selector (top left) to select the UI Element. Now you can see all callbacks, props or jump to the source
|
||||
directly.
|
||||
|
||||
## Improve Vencord itself
|
||||
If it is anything else, or you're too lazy to use React DevTools, hit `CTRL + Shift + F` while in DevTools and
|
||||
enter a search term, for example "getUser" to search all source files.
|
||||
Look at the results until you find something promising. Set a breakpoint and trigger the execution of that part of Code to inspect arguments, locals, etc...
|
||||
|
||||
If you have any ideas on how to improve Vencord itself, or want to propose a new plugin API, feel free to open a feature request so we can discuss.
|
||||
### Writing a robust patch
|
||||
|
||||
Or if you notice any bugs or typos, feel free to fix them!
|
||||
##### "find"
|
||||
|
||||
## Contribute to our Documentation
|
||||
First you need to find a good `find` value. This should be a string that is unique to your module.
|
||||
If you want to patch the `getUser` function, usually a good first try is `getUser:` or `function getUser()`,
|
||||
depending on how the module is structured. Again, make sure this string is unique to your module and is not
|
||||
found in any other module. To verify this, search for it in all bundles (CTRL + Shift + F)
|
||||
|
||||
The source code of our documentation is available at <https://github.com/Vencord/Docs>
|
||||
##### "match"
|
||||
|
||||
If you see anything outdated, incorrect or lacking, please fix it!
|
||||
If you think a new page should be added, feel free to suggest it via an issue and we can discuss.
|
||||
This is the regex that will operate on the module found with "find". Just like in find, you should make sure
|
||||
this only matches exactly the part you want to patch and no other parts in the file.
|
||||
|
||||
## Help out users in our Discord community
|
||||
The easiest way to write and test your regex is the following:
|
||||
|
||||
We have an open support channel in our [Discord community](https://vencord.dev/discord).
|
||||
Helping out users there is always appreciated! The more, the merrier.
|
||||
- Get the ID of the module you want to patch. To do this, go to it in the sources tab and scroll up until you
|
||||
see something like `447887: (e,t,n)=>{` (Obviously the number will differ).
|
||||
- Now paste the following into the console: `Vencord.Webpack.wreq.m[447887].toString()` (Changing the number to your ID)
|
||||
- Now either test regexes on this string in the console or use a tool like https://regex101.com
|
||||
|
||||
Also pay attention to the following:
|
||||
|
||||
- Never hardcode variable or parameter names or any other minified names. They will change in the future. The only Exception to this rule
|
||||
are the react props parameter which seems to always be `e`, but even then only rely on this if it is necessary.
|
||||
Instead, use one of the following approaches where applicable:
|
||||
- Match 1 or 2 of any character: `.{1,2}`, for example to match the variable name in `var a=b`, `var (.{1,2})=`
|
||||
- Match any but a guaranteed terminating character: `[^;]+`, for example to match the entire assigned value in `var a=b||c||func();`,
|
||||
`var .{1,2}=([^;]+);`
|
||||
- If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters
|
||||
- Additionally, as you might have noticed, all of the above approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
|
||||
|
||||
#### "replace"
|
||||
|
||||
This is the replacement for the match. This is the second argument to [String.replace](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace), so refer to those docs for info.
|
||||
|
||||
Never hardcode minified variable or parameter names here. Instead, use capture groups in your regex to capture the variable names
|
||||
and use those in your replacement
|
||||
|
||||
Make sure your replacement does not introduce any whitespace. While this might seem weird, random whitespace may mess up other patches.
|
||||
This includes spaces, tabs and especially newlines
|
||||
|
||||
---
|
||||
|
||||
And that's it! Now open a Pull Request with your Plugin
|
||||
|
|
14
README.md
14
README.md
|
@ -1,12 +1,10 @@
|
|||
# Vencord
|
||||
|
||||
[![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Vee/cord&color=2185D0&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAKbUlEQVR4nNVae3AV5RX/nW/3Pva+b24e5HHzIICQKGoiYiW8NFBFgohaa6ctglpbFSujSGurzUinohWsOij/gGX6R2fqOK0d1FYTEZXaTrWCBbEikJCEyCvkeXNvkrunf+zdkJDkPnex/c3cmd29+53v/M6e73znnF2Cydj4Tntldzi6qrN/qKqzf2jy6b7BnL4B1dI7oMp9AyoRAIdVsNMqhlxWMZjtspzyK/Jhr036OMsm//bh2vzPzNSPzBD6xFutd7R0Dq758ky4orkjYuc05RCAkixbeEq2/UCJ1/LczxcX/c5IPfU5DMHmxpbCpu7o1k/b+xc1n43YjJI7EqV+W2RmvuPt0oDjB2vn5bQbITNjAzzdeKK8qTO0bU9T77zucNQUjzofHrvENWWu3aUBZfW6+ZOOZiIrbYXrmUXo9daX3v6i667O/iGRiRLpwqtIvKDc+0efJ3hb/UIaSkdGWgZ4sqGt9r2m3lc/P9HvSWe80ZiRp3TPL/UsX1+bvyvVsSkb4NE3WjbuPNj5SM8Fcvdk4bAKrqvwv7DxhuCPUxmXNIn6XSy3nWr6R8OhrqrU1btwqJ3m/bgwu/SqZJdEUgbYsuuka09b9/4Pm3tLMlPvwuAbpe6m+RcplfdcURBKdG9CA2zZddLV2Nx1+JO2vlxj1LswqCpynlxc6SxLZIS40bueWfy9vXvv/xt5APhXa1/u7v+EPqvfxXK8++IaoO2Vpn9+cLS33FjVLhw+bOotOX7q6N/i3TOhAX7y+rHN/+sBLxm8fah71k93tjw/0f/jGuDJxtZrdh7setA8tS4sdn7eef+v3mmfP95/Ywxw6x9Yev9I35/6Iubv83WVfl5a6Uu3VkoavZEo7TnS/Vo98xi+Yy6UKC3bDp7sd5ut1OWFDjyzNMib6oq5Oug0ezp8dqLfG3r92Nbzr48ywNONJ8obDnV/z2xlAk4ZW1aUqhaJIAvCb5YVqwFn3GBtCBoO9dz5TOPxUbnMKAM0dYa2d5lc2AgCNi8r5klui3aBgWynjE11QZbI3FV3NjQkjnYNbB+lj36wubGlcE9T71xTNQDw0Px8nlvmHl73GmfCrKCL19Tkmh4P9jT1LHz2vVP5+vmwAZq71a1m1/PXTPXwD68eS5KIEVUZd1yZwwumeEw1Qld/lJrPhF7Sz4cNsO+rUK2ZExd6rfj10iCPZ2GJCCoAZuCJxQUc9FvNVAX72kPX6ccC0Hp4zR0Ru1kT2mTCSzeXqn5l/EAniMAqoDLDYZWwqa5EVSzmhaKmsxHbLxvbbgdiBmjpHFxj2mwANlxXxBdPUib8nwgQgqAyEFUZxT4L1i/MN3UpHDsTWQvEDHDoTLjCrIluuyzAt8zMSkhGFhp5hrYUFk3z8IqZftOMcKRj4GIAEM80tFccM8n9Z+Qq+MXigqRIWCQCMzQvYIbKwH1X53FFnjkr88iZsLKpoXWa6BiIrjbDzF67hK23lKp2Obm1LAstPEZVjTwDkAio/2ZQ9dolw/VjAB0DfKfoCg9WGy2cADy1NMhBX2rR3CIRGICq8rAhAg4Jj9UWsDBhg+4MR6vF2VC0zGjB99fk8eJp3pQdyyrRMHF9KURVxswCB6+alWO4o3b2RyeLU32D2UYKnVPm5gfm5qWlrF0Wo4hzbCmoDNw0089XlboNNcLpvsFc0RtRDXuNle+x4Lkbi9PO6WWJIBFGEY+qjGjswtq5eVzosRilLnoiUavoH1INiTCyIDy/vETNcmRW1dl0L4gRVxmx3YFhlwnrry1QrZIxASE0yJIIDaiGSHt8UQFXF2Ve1zusYgzxkXGhyGvFvePUE+mgfyAqhGqAqKWVPv5udbYhSjmtkpYWq6OJqzFjqCpjTpmbl1Rk3klSGRBWmTISNC3Hjo1LgoYFJ0GA1aIVR+cTVxlQoS2Pb18a4PLszMKXzSJYuCySmq4Al03CiytKVYfBhYvLKk1IXE+XLRLhwZp81WlNf26HTFHhd0jhdAYTgKduCPLkgPHfQjitYkLiAIEZBDBlu2R6aF7euCV2Mgg45bDw2qWOdAavnp3D109PPdlJBvpTnYg4kVY3MDMuylVw62WJi63x4LHLZ0TAIR9OdWBVodPUclUQwWmT4hLXfgCIUDfDi6oiR8rzBJzyl8LnkD9KZVCOU8aLN5eoshnJ+Qh4bFJC4gztmEjgrtk5anaKnWWfXfpIuBTLjmSpSILw/E0laq7LuGxsIngVCYmIa96hLRG3TaZ1C/KTfjAEQLFIO8TPFk7aH/RZI8kMWrdgEs8udqXLKSUoMkEW4ETEQTRsoHyPlVZfmVw+Uuy3hR9bVHBQAMD0XPu/Ew24dqqH777K/La1DiKCxyYlRRzQymgG4+oyDxZOTdxZnp5r3wvEWmJ5btuL8W4uzbJh87LitLebdOFVpKSJx4IlwIzbL81CcYLO8iSX/IImGQCYae6Wg/2tXQNjNnW7LPDKyilqZd7ETU2zEBlifNTSS4i9PNFIx44x4jh2nZlBsUr0dN8QP/6XVhEaHJvnlfhtkXd/NF0BUextKRFXFznfGk+JDdcX8tdBHtDa6YpFsB4I9ac88omf8wbEgqa2XAIOme6bM35foqrQ+QZIKwGG80ifVbrXZZNGDfhOVYBvviS9JMMoaP3AEcQpPnHdOxiMGXkKbrx4dGfZY5c4T8H9+vmwAeqXFLXOKXW9r59fWuDA44sKv1byAOBzyCkTH+kdS2f4MLPgXJI0p9T17vrFxcf181GVxEUB+0qfIqt+RcKWFSWGNR4ygd4RTpW4HiCJgFWzstmnSPA7ZLU827pypPwxDB/687GXl1X6Vs6bbGz/LRN80hZCT+yLFZ0cgHED4egACeiXm89GsP9EePuzy4rvGil7jAGYmQDsBjDHUBYZ4GhHBMfORigd4rpnyIS9u6d4rqgnGrUtjCmmSYuOqwB0GcwjbWh9xviurpNnxnDA1IspMPe6bOL755MHJvhKjIgOA7jbJD4pw22Thj+kSIW47h2KRaydVezeP57sCdspRPQqgGeNJJIuBAE+ReJUiOv32mXaXjPZs21C2QnmXgdghyEsMoRfkVMiDgCywF/by9z3xJMb1wCxeHAPgDczZpAh/Iq+HSYmDjCsstgThmf5t4ii8eQm7CgS0SCA5QBezoRApnBaBSyCEhIHCLJEb4ZUd+2SqZSwzE+qpUpEQ9CC4qb01M8cRIQsh8zxiKsMtsn08nvlnrpkyAPj5AGJwMw3AtgGwJ/q2ExxvHsQB74KxfKBMblAyGmTHq4pc4/5GjQeUm6qE9FrAK4E8H6ie41GlkN/jTk6F5Ak2ueUpNmpkgfSMAAAENERAAsB3AHgZDoy0oFdFnBYpXPEBfU4beLRD6Z4qmumug+kIzPjaoeZfQDWAHgAQFam8hLh4MkwWjsHemyS2OF08IYrCjynzZ4zKTCzi5nXMvOnzBw16bevIxR95JOj7DNKb1PqXWa+HMDtAGoBXII0lxq0N2OfAmgA8Hsi2muMhudgesHPzNkA5gKoADADwFRoS8UHQO+x9wLoBNAB4AsAnwM4AOADIjLVxf8L9kdXUOE0IskAAAAASUVORK5CYII=)](https://codeberg.org/Vee/cord)
|
||||
[![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Ven/cord&color=2185D0&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAKbUlEQVR4nNVae3AV5RX/nW/3Pva+b24e5HHzIICQKGoiYiW8NFBFgohaa6ctglpbFSujSGurzUinohWsOij/gGX6R2fqOK0d1FYTEZXaTrWCBbEikJCEyCvkeXNvkrunf+zdkJDkPnex/c3cmd29+53v/M6e73znnF2Cydj4Tntldzi6qrN/qKqzf2jy6b7BnL4B1dI7oMp9AyoRAIdVsNMqhlxWMZjtspzyK/Jhr036OMsm//bh2vzPzNSPzBD6xFutd7R0Dq758ky4orkjYuc05RCAkixbeEq2/UCJ1/LczxcX/c5IPfU5DMHmxpbCpu7o1k/b+xc1n43YjJI7EqV+W2RmvuPt0oDjB2vn5bQbITNjAzzdeKK8qTO0bU9T77zucNQUjzofHrvENWWu3aUBZfW6+ZOOZiIrbYXrmUXo9daX3v6i667O/iGRiRLpwqtIvKDc+0efJ3hb/UIaSkdGWgZ4sqGt9r2m3lc/P9HvSWe80ZiRp3TPL/UsX1+bvyvVsSkb4NE3WjbuPNj5SM8Fcvdk4bAKrqvwv7DxhuCPUxmXNIn6XSy3nWr6R8OhrqrU1btwqJ3m/bgwu/SqZJdEUgbYsuuka09b9/4Pm3tLMlPvwuAbpe6m+RcplfdcURBKdG9CA2zZddLV2Nx1+JO2vlxj1LswqCpynlxc6SxLZIS40bueWfy9vXvv/xt5APhXa1/u7v+EPqvfxXK8++IaoO2Vpn9+cLS33FjVLhw+bOotOX7q6N/i3TOhAX7y+rHN/+sBLxm8fah71k93tjw/0f/jGuDJxtZrdh7setA8tS4sdn7eef+v3mmfP95/Ywxw6x9Yev9I35/6Iubv83WVfl5a6Uu3VkoavZEo7TnS/Vo98xi+Yy6UKC3bDp7sd5ut1OWFDjyzNMib6oq5Oug0ezp8dqLfG3r92Nbzr48ywNONJ8obDnV/z2xlAk4ZW1aUqhaJIAvCb5YVqwFn3GBtCBoO9dz5TOPxUbnMKAM0dYa2d5lc2AgCNi8r5klui3aBgWynjE11QZbI3FV3NjQkjnYNbB+lj36wubGlcE9T71xTNQDw0Px8nlvmHl73GmfCrKCL19Tkmh4P9jT1LHz2vVP5+vmwAZq71a1m1/PXTPXwD68eS5KIEVUZd1yZwwumeEw1Qld/lJrPhF7Sz4cNsO+rUK2ZExd6rfj10iCPZ2GJCCoAZuCJxQUc9FvNVAX72kPX6ccC0Hp4zR0Ru1kT2mTCSzeXqn5l/EAniMAqoDLDYZWwqa5EVSzmhaKmsxHbLxvbbgdiBmjpHFxj2mwANlxXxBdPUib8nwgQgqAyEFUZxT4L1i/MN3UpHDsTWQvEDHDoTLjCrIluuyzAt8zMSkhGFhp5hrYUFk3z8IqZftOMcKRj4GIAEM80tFccM8n9Z+Qq+MXigqRIWCQCMzQvYIbKwH1X53FFnjkr88iZsLKpoXWa6BiIrjbDzF67hK23lKp2Obm1LAstPEZVjTwDkAio/2ZQ9dolw/VjAB0DfKfoCg9WGy2cADy1NMhBX2rR3CIRGICq8rAhAg4Jj9UWsDBhg+4MR6vF2VC0zGjB99fk8eJp3pQdyyrRMHF9KURVxswCB6+alWO4o3b2RyeLU32D2UYKnVPm5gfm5qWlrF0Wo4hzbCmoDNw0089XlboNNcLpvsFc0RtRDXuNle+x4Lkbi9PO6WWJIBFGEY+qjGjswtq5eVzosRilLnoiUavoH1INiTCyIDy/vETNcmRW1dl0L4gRVxmx3YFhlwnrry1QrZIxASE0yJIIDaiGSHt8UQFXF2Ve1zusYgzxkXGhyGvFvePUE+mgfyAqhGqAqKWVPv5udbYhSjmtkpYWq6OJqzFjqCpjTpmbl1Rk3klSGRBWmTISNC3Hjo1LgoYFJ0GA1aIVR+cTVxlQoS2Pb18a4PLszMKXzSJYuCySmq4Al03CiytKVYfBhYvLKk1IXE+XLRLhwZp81WlNf26HTFHhd0jhdAYTgKduCPLkgPHfQjitYkLiAIEZBDBlu2R6aF7euCV2Mgg45bDw2qWOdAavnp3D109PPdlJBvpTnYg4kVY3MDMuylVw62WJi63x4LHLZ0TAIR9OdWBVodPUclUQwWmT4hLXfgCIUDfDi6oiR8rzBJzyl8LnkD9KZVCOU8aLN5eoshnJ+Qh4bFJC4gztmEjgrtk5anaKnWWfXfpIuBTLjmSpSILw/E0laq7LuGxsIngVCYmIa96hLRG3TaZ1C/KTfjAEQLFIO8TPFk7aH/RZI8kMWrdgEs8udqXLKSUoMkEW4ETEQTRsoHyPlVZfmVw+Uuy3hR9bVHBQAMD0XPu/Ew24dqqH777K/La1DiKCxyYlRRzQymgG4+oyDxZOTdxZnp5r3wvEWmJ5btuL8W4uzbJh87LitLebdOFVpKSJx4IlwIzbL81CcYLO8iSX/IImGQCYae6Wg/2tXQNjNnW7LPDKyilqZd7ETU2zEBlifNTSS4i9PNFIx44x4jh2nZlBsUr0dN8QP/6XVhEaHJvnlfhtkXd/NF0BUextKRFXFznfGk+JDdcX8tdBHtDa6YpFsB4I9ac88omf8wbEgqa2XAIOme6bM35foqrQ+QZIKwGG80ifVbrXZZNGDfhOVYBvviS9JMMoaP3AEcQpPnHdOxiMGXkKbrx4dGfZY5c4T8H9+vmwAeqXFLXOKXW9r59fWuDA44sKv1byAOBzyCkTH+kdS2f4MLPgXJI0p9T17vrFxcf181GVxEUB+0qfIqt+RcKWFSWGNR4ygd4RTpW4HiCJgFWzstmnSPA7ZLU827pypPwxDB/687GXl1X6Vs6bbGz/LRN80hZCT+yLFZ0cgHED4egACeiXm89GsP9EePuzy4rvGil7jAGYmQDsBjDHUBYZ4GhHBMfORigd4rpnyIS9u6d4rqgnGrUtjCmmSYuOqwB0GcwjbWh9xviurpNnxnDA1IspMPe6bOL755MHJvhKjIgOA7jbJD4pw22Thj+kSIW47h2KRaydVezeP57sCdspRPQqgGeNJJIuBAE+ReJUiOv32mXaXjPZs21C2QnmXgdghyEsMoRfkVMiDgCywF/by9z3xJMb1wCxeHAPgDczZpAh/Iq+HSYmDjCsstgThmf5t4ii8eQm7CgS0SCA5QBezoRApnBaBSyCEhIHCLJEb4ZUd+2SqZSwzE+qpUpEQ9CC4qb01M8cRIQsh8zxiKsMtsn08nvlnrpkyAPj5AGJwMw3AtgGwJ/q2ExxvHsQB74KxfKBMblAyGmTHq4pc4/5GjQeUm6qE9FrAK4E8H6ie41GlkN/jTk6F5Ak2ueUpNmpkgfSMAAAENERAAsB3AHgZDoy0oFdFnBYpXPEBfU4beLRD6Z4qmumug+kIzPjaoeZfQDWAHgAQFam8hLh4MkwWjsHemyS2OF08IYrCjynzZ4zKTCzi5nXMvOnzBw16bevIxR95JOj7DNKb1PqXWa+HMDtAGoBXII0lxq0N2OfAmgA8Hsi2muMhudgesHPzNkA5gKoADADwFRoS8UHQO+x9wLoBNAB4AsAnwM4AOADIjLVxf8L9kdXUOE0IskAAAAASUVORK5CYII=)](https://codeberg.org/Ven/cord)
|
||||
|
||||
The cutest Discord client mod
|
||||
|
||||
| ![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334) |
|
||||
| :--------------------------------------------------------------------------------------------------: |
|
||||
| A screenshot of vencord showcasing the [vencord-theme](https://github.com/synqat/vencord-theme) |
|
||||
![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334)
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -30,14 +28,6 @@ Visit https://vencord.dev/download
|
|||
|
||||
https://discord.gg/D9uwnFnqmd
|
||||
|
||||
## Sponsors
|
||||
|
||||
| **Thanks a lot to all Vencord [sponsors](https://github.com/sponsors/Vendicated)!!** |
|
||||
| :------------------------------------------------------------------------------------------: |
|
||||
| [![](https://meow.vendicated.dev/sponsors.png)](https://github.com/sponsors/Vendicated) |
|
||||
| *generated using [github-sponsor-graph](https://github.com/Vendicated/github-sponsor-graph)* |
|
||||
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#Vendicated/Vencord&Timeline">
|
||||
|
|
|
@ -62,7 +62,7 @@ function GM_fetch(url, opt) {
|
|||
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
|
||||
resp.text = () => blobTo("text", blob);
|
||||
resp.json = async () => JSON.parse(await blobTo("text", blob));
|
||||
resp.headers = parseHeaders(resp.responseHeaders);
|
||||
resp.headers = new Headers(parseHeaders(resp.responseHeaders));
|
||||
resp.ok = resp.status >= 200 && resp.status < 300;
|
||||
resolve(resp);
|
||||
};
|
||||
|
|
|
@ -19,14 +19,13 @@
|
|||
/// <reference path="../src/modules.d.ts" />
|
||||
/// <reference path="../src/globals.d.ts" />
|
||||
|
||||
import monacoHtmlLocal from "file://monacoWin.html?minify";
|
||||
import monacoHtmlCdn from "file://../src/main/monacoWin.html?minify";
|
||||
import monacoHtmlLocal from "~fileContent/monacoWin.html";
|
||||
import monacoHtmlCdn from "~fileContent/../src/main/monacoWin.html";
|
||||
import * as DataStore from "../src/api/DataStore";
|
||||
import { debounce } from "../src/utils";
|
||||
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
|
||||
import { getTheme, Theme } from "../src/utils/discord";
|
||||
import { getThemeInfo } from "../src/main/themes";
|
||||
import { Settings } from "../src/Vencord";
|
||||
|
||||
// Discord deletes this so need to store in variable
|
||||
const { localStorage } = window;
|
||||
|
@ -97,15 +96,8 @@ window.VencordNative = {
|
|||
},
|
||||
|
||||
settings: {
|
||||
get: () => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem("VencordSettings") || "{}");
|
||||
} catch (e) {
|
||||
console.error("Failed to parse settings from localStorage: ", e);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
set: async (s: Settings) => localStorage.setItem("VencordSettings", JSON.stringify(s)),
|
||||
get: () => localStorage.getItem("VencordSettings") || "{}",
|
||||
set: async (s: string) => localStorage.setItem("VencordSettings", s),
|
||||
getSettingsDir: async () => "LocalStorage"
|
||||
},
|
||||
|
||||
|
|
|
@ -2,22 +2,23 @@ if (typeof browser === "undefined") {
|
|||
var browser = chrome;
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = browser.runtime.getURL("dist/Vencord.js");
|
||||
script.id = "vencord-script";
|
||||
Object.assign(script.dataset, {
|
||||
extensionBaseUrl: browser.runtime.getURL(""),
|
||||
version: browser.runtime.getManifest().version
|
||||
});
|
||||
|
||||
const style = document.createElement("link");
|
||||
style.type = "text/css";
|
||||
style.rel = "stylesheet";
|
||||
style.href = browser.runtime.getURL("dist/Vencord.css");
|
||||
|
||||
document.documentElement.append(script);
|
||||
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
() => {
|
||||
document.documentElement.append(style);
|
||||
window.postMessage({
|
||||
type: "vencord:meta",
|
||||
meta: {
|
||||
EXTENSION_VERSION: browser.runtime.getManifest().version,
|
||||
EXTENSION_BASE_URL: browser.runtime.getURL(""),
|
||||
}
|
||||
});
|
||||
},
|
||||
() => document.documentElement.append(style),
|
||||
{ once: true }
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"minimum_chrome_version": "111",
|
||||
"minimum_chrome_version": "91",
|
||||
|
||||
"name": "Vencord Web",
|
||||
"description": "The cutest Discord mod now in your browser",
|
||||
|
@ -22,15 +22,7 @@
|
|||
"run_at": "document_start",
|
||||
"matches": ["*://*.discord.com/*"],
|
||||
"js": ["content.js"],
|
||||
"all_frames": true,
|
||||
"world": "ISOLATED"
|
||||
},
|
||||
{
|
||||
"run_at": "document_start",
|
||||
"matches": ["*://*.discord.com/*"],
|
||||
"js": ["dist/Vencord.js"],
|
||||
"all_frames": true,
|
||||
"world": "MAIN"
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
|
||||
|
|
|
@ -22,15 +22,7 @@
|
|||
"run_at": "document_start",
|
||||
"matches": ["*://*.discord.com/*"],
|
||||
"js": ["content.js"],
|
||||
"all_frames": true,
|
||||
"world": "ISOLATED"
|
||||
},
|
||||
{
|
||||
"run_at": "document_start",
|
||||
"matches": ["*://*.discord.com/*"],
|
||||
"js": ["dist/Vencord.js"],
|
||||
"all_frames": true,
|
||||
"world": "MAIN"
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -43,7 +35,7 @@
|
|||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "vencord-firefox@vendicated.dev",
|
||||
"strict_min_version": "128.0"
|
||||
"strict_min_version": "91.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
// @author Vendicated (https://github.com/Vendicated)
|
||||
// @namespace https://github.com/Vendicated/Vencord
|
||||
// @supportURL https://github.com/Vendicated/Vencord
|
||||
// @icon https://raw.githubusercontent.com/Vendicated/Vencord/refs/heads/main/browser/icon.png
|
||||
// @license GPL-3.0
|
||||
// @match *://*.discord.com/*
|
||||
// @grant GM_xmlhttpRequest
|
||||
|
|
99
docs/1_INSTALLING.md
Normal file
99
docs/1_INSTALLING.md
Normal file
|
@ -0,0 +1,99 @@
|
|||
> [!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.
|
||||
> No support will be provided for installing in this fashion. If you cannot figure it out, you should just stick to a regular install.
|
||||
|
||||
# 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)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Install Git from https://git-scm.com/download
|
||||
- Install Node.JS LTS from here: https://nodejs.dev/en/
|
||||
|
||||
## Installing Vencord
|
||||
|
||||
Install `pnpm`:
|
||||
|
||||
> :exclamation: This next command may need to be run as admin/root depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
|
||||
|
||||
```shell
|
||||
npm i -g pnpm
|
||||
```
|
||||
|
||||
> :exclamation: **IMPORTANT** Make sure you aren't using an admin/root terminal from here onwards. It **will** mess up your Discord/Vencord instance and you **will** most likely have to reinstall.
|
||||
|
||||
Clone Vencord:
|
||||
|
||||
```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 necessary 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
|
||||
```
|
||||
|
||||
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
111
docs/2_PLUGINS.md
Normal file
111
docs/2_PLUGINS.md
Normal file
|
@ -0,0 +1,111 @@
|
|||
# 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).
|
|
@ -1,126 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
import stylistic from "@stylistic/eslint-plugin";
|
||||
import pathAlias from "eslint-plugin-path-alias";
|
||||
import header from "eslint-plugin-simple-header";
|
||||
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist", "browser", "packages/vencord-types"] },
|
||||
{
|
||||
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}", "eslint.config.mjs"],
|
||||
plugins: {
|
||||
"simple-header": header,
|
||||
"@stylistic": stylistic,
|
||||
"@typescript-eslint": tseslint.plugin,
|
||||
"simple-import-sort": simpleImportSort,
|
||||
"unused-imports": unusedImports,
|
||||
"path-alias": pathAlias,
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
map: [
|
||||
["@webpack", "./src/webpack"],
|
||||
["@webpack/common", "./src/webpack/common"],
|
||||
["@utils", "./src/utils"],
|
||||
["@api", "./src/api"],
|
||||
["@components", "./src/components"]
|
||||
]
|
||||
}
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
tsconfigRootDir: import.meta.dirname
|
||||
}
|
||||
},
|
||||
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
|
||||
*/
|
||||
"simple-header/header": [
|
||||
"error",
|
||||
{
|
||||
"files": ["scripts/header-new.txt", "scripts/header-old.txt"],
|
||||
"templates": { "author": [".*", "Vendicated and contributors"] }
|
||||
}
|
||||
],
|
||||
|
||||
// Style Rules
|
||||
"@stylistic/jsx-quotes": ["error", "prefer-double"],
|
||||
"@stylistic/quotes": ["error", "double", { "avoidEscape": true }],
|
||||
"@stylistic/no-mixed-spaces-and-tabs": "error",
|
||||
"@stylistic/arrow-parens": ["error", "as-needed"],
|
||||
"@stylistic/eol-last": ["error", "always"],
|
||||
"@stylistic/no-multi-spaces": "error",
|
||||
"@stylistic/no-trailing-spaces": "error",
|
||||
"@stylistic/no-whitespace-before-property": "error",
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
"@stylistic/semi-style": ["error", "last"],
|
||||
"@stylistic/space-in-parens": ["error", "never"],
|
||||
"@stylistic/block-spacing": ["error", "always"],
|
||||
"@stylistic/object-curly-spacing": ["error", "always"],
|
||||
"@stylistic/spaced-comment": ["error", "always", { "markers": ["!"] }],
|
||||
"@stylistic/no-extra-semi": "error",
|
||||
|
||||
// TS Rules
|
||||
"@stylistic/func-call-spacing": ["error", "never"],
|
||||
|
||||
// ESLint Rules
|
||||
"yoda": "error",
|
||||
"eqeqeq": ["error", "always", { "null": "ignore" }],
|
||||
"prefer-destructuring": ["error", {
|
||||
"VariableDeclarator": { "array": false, "object": true },
|
||||
"AssignmentExpression": { "array": false, "object": 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",
|
||||
"dot-notation": "error",
|
||||
"no-useless-escape": [
|
||||
"error",
|
||||
{
|
||||
"extra": "i"
|
||||
}
|
||||
],
|
||||
"no-fallthrough": "error",
|
||||
"for-direction": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-dupe-else-if": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-loss-of-precision": "error",
|
||||
"no-misleading-character-class": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-unexpected-multiline": "error",
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-useless-backreference": "error",
|
||||
"use-isnan": "error",
|
||||
"prefer-const": "error",
|
||||
"prefer-spread": "error",
|
||||
|
||||
// Plugin Rules
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"path-alias/no-relative": "error"
|
||||
}
|
||||
}
|
||||
);
|
265
flake.lock
265
flake.lock
|
@ -1,265 +0,0 @@
|
|||
{
|
||||
"nodes": {
|
||||
"cachix": {
|
||||
"inputs": {
|
||||
"devenv": [
|
||||
"devenv"
|
||||
],
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"git-hooks": [
|
||||
"devenv"
|
||||
],
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1728672398,
|
||||
"narHash": "sha256-KxuGSoVUFnQLB2ZcYODW7AVPAh9JqRlD5BrfsC/Q4qs=",
|
||||
"owner": "cachix",
|
||||
"repo": "cachix",
|
||||
"rev": "aac51f698309fd0f381149214b7eee213c66ef0a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "latest",
|
||||
"repo": "cachix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devenv": {
|
||||
"inputs": {
|
||||
"cachix": "cachix",
|
||||
"flake-compat": "flake-compat",
|
||||
"git-hooks": "git-hooks",
|
||||
"nix": "nix",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1732025403,
|
||||
"narHash": "sha256-wHUN9dQA9wz/LYq8z91IjeuD47PIixtqaoa30k5LxUA=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "6473534b5f3a7ae956ee751084bc4bf2391ccc28",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"devenv",
|
||||
"nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712014858,
|
||||
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": [
|
||||
"devenv"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1730302582,
|
||||
"narHash": "sha256-W1MIJpADXQCgosJZT8qBYLRuZls2KSiKdpnTVdKBuvU=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "af8a16fe5c264f5e9e18bcee2859b40a656876cf",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"git-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"libgit2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1697646580,
|
||||
"narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
|
||||
"owner": "libgit2",
|
||||
"repo": "libgit2",
|
||||
"rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "libgit2",
|
||||
"repo": "libgit2",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"flake-parts": "flake-parts",
|
||||
"libgit2": "libgit2",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixpkgs-23-11": [
|
||||
"devenv"
|
||||
],
|
||||
"nixpkgs-regression": [
|
||||
"devenv"
|
||||
],
|
||||
"pre-commit-hooks": [
|
||||
"devenv"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1727438425,
|
||||
"narHash": "sha256-X8ES7I1cfNhR9oKp06F6ir4Np70WGZU5sfCOuNBEwMg=",
|
||||
"owner": "domenkozar",
|
||||
"repo": "nix",
|
||||
"rev": "f6c5ae4c1b2e411e6b1e6a8181cc84363d6a7546",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "domenkozar",
|
||||
"ref": "devenv-2.24",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1730531603,
|
||||
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7ffd9ae656aec493492b44d0ddfb28e79a1ea25d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1717432640,
|
||||
"narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "release-24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1731676054,
|
||||
"narHash": "sha256-OZiZ3m8SCMfh3B6bfGC/Bm4x3qc1m2SVEAlkV6iY7Yg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5e4fbfb6b3de1aa2872b76d49fafc942626e2add",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"nixpkgs": "nixpkgs_3",
|
||||
"systems": "systems"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
43
flake.nix
43
flake.nix
|
@ -1,43 +0,0 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
systems.url = "github:nix-systems/default";
|
||||
devenv.url = "github:cachix/devenv";
|
||||
devenv.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
nixConfig = {
|
||||
extra-trusted-public-keys =
|
||||
"devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
|
||||
extra-substituters = "https://devenv.cachix.org";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, devenv, systems, ... }@inputs:
|
||||
let forEachSystem = nixpkgs.lib.genAttrs (import systems);
|
||||
in {
|
||||
packages = forEachSystem (system: {
|
||||
devenv-up = self.devShells.${system}.default.config.procfileScript;
|
||||
devenv-test = self.devShells.${system}.default.config.test;
|
||||
});
|
||||
|
||||
devShells = forEachSystem (system:
|
||||
let pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
default = devenv.lib.mkShell {
|
||||
inherit inputs pkgs;
|
||||
modules = [{
|
||||
languages = {
|
||||
javascript = {
|
||||
enable = true;
|
||||
pnpm = {
|
||||
enable = true;
|
||||
install.enable = true;
|
||||
};
|
||||
};
|
||||
typescript.enable = true;
|
||||
};
|
||||
}];
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
91
package.json
91
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.10.9",
|
||||
"version": "1.6.3",
|
||||
"description": "The cutest Discord client mod",
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
|
@ -13,77 +13,68 @@
|
|||
},
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": "Vendicated",
|
||||
"directories": {
|
||||
"doc": "docs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
|
||||
"buildStandalone": "pnpm build --standalone",
|
||||
"build": "node scripts/build/build.mjs",
|
||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||
"buildWebStandalone": "pnpm buildWeb --standalone",
|
||||
"buildReporter": "pnpm buildWebStandalone --reporter --skip-extension",
|
||||
"buildReporterDesktop": "pnpm build --reporter",
|
||||
"watch": "pnpm build --watch",
|
||||
"dev": "pnpm watch",
|
||||
"watchWeb": "pnpm buildWeb --watch",
|
||||
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
||||
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
|
||||
"inject": "node scripts/runInstaller.mjs",
|
||||
"uninject": "node scripts/runInstaller.mjs",
|
||||
"lint": "eslint",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
|
||||
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"test": "pnpm buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
|
||||
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
|
||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
||||
"testTsc": "tsc --noEmit"
|
||||
"testTsc": "tsc --noEmit",
|
||||
"uninject": "node scripts/runInstaller.mjs",
|
||||
"watch": "node scripts/build/build.mjs --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@intrnl/xxhash64": "^0.1.2",
|
||||
"@sapphi-red/web-noise-suppressor": "0.3.5",
|
||||
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
||||
"@vap/core": "0.0.12",
|
||||
"@vap/shiki": "0.10.5",
|
||||
"fflate": "^0.8.2",
|
||||
"eslint-plugin-simple-header": "^1.0.2",
|
||||
"fflate": "^0.7.4",
|
||||
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"monaco-editor": "^0.43.0",
|
||||
"nanoid": "^4.0.2",
|
||||
"virtual-merge": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stylistic/eslint-plugin": "^2.6.1",
|
||||
"@types/chrome": "^0.0.269",
|
||||
"@types/diff": "^5.2.1",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/node": "^22.0.3",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/yazl": "^2.4.5",
|
||||
"diff": "^5.2.0",
|
||||
"@types/chrome": "^0.0.246",
|
||||
"@types/diff": "^5.0.3",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^18.16.3",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@types/yazl": "^2.4.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||
"@typescript-eslint/parser": "^5.59.1",
|
||||
"diff": "^5.1.0",
|
||||
"discord-types": "^1.3.26",
|
||||
"esbuild": "^0.15.18",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-path-alias": "2.1.0",
|
||||
"eslint-plugin-simple-header": "^1.1.1",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.0.1",
|
||||
"highlight.js": "10.7.3",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"moment": "^2.30.1",
|
||||
"puppeteer-core": "^22.15.0",
|
||||
"eslint-plugin-path-alias": "^1.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"highlight.js": "10.6.0",
|
||||
"moment": "^2.29.4",
|
||||
"puppeteer-core": "^19.11.1",
|
||||
"standalone-electron-types": "^1.0.0",
|
||||
"stylelint": "^16.8.1",
|
||||
"stylelint-config-standard": "^36.0.1",
|
||||
"ts-patch": "^3.2.1",
|
||||
"ts-pattern": "^5.3.1",
|
||||
"tsx": "^4.16.5",
|
||||
"type-fest": "^4.23.0",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"typescript-transform-paths": "^3.4.7",
|
||||
"stylelint": "^15.6.0",
|
||||
"stylelint-config-standard": "^33.0.0",
|
||||
"tsx": "^3.12.7",
|
||||
"type-fest": "^3.9.0",
|
||||
"typescript": "^5.0.4",
|
||||
"zip-local": "^0.3.5"
|
||||
},
|
||||
"packageManager": "pnpm@9.1.0",
|
||||
"packageManager": "pnpm@8.10.2",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"eslint@9.8.0": "patches/eslint@9.8.0.patch",
|
||||
"eslint-plugin-path-alias@2.1.0": "patches/eslint-plugin-path-alias@2.1.0.patch"
|
||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
||||
"eslint@8.46.0": "patches/eslint@8.46.0.patch"
|
||||
},
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
|
@ -107,6 +98,6 @@
|
|||
},
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"pnpm": ">=9"
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
}
|
||||
|
|
7
packages/vencord-types/.gitignore
vendored
7
packages/vencord-types/.gitignore
vendored
|
@ -1,7 +0,0 @@
|
|||
*
|
||||
!.*ignore
|
||||
!package.json
|
||||
!*.md
|
||||
!prepare.ts
|
||||
!index.d.ts
|
||||
!globals.d.ts
|
|
@ -1,4 +0,0 @@
|
|||
node_modules
|
||||
prepare.ts
|
||||
.gitignore
|
||||
HOW2PUB.md
|
|
@ -1,5 +0,0 @@
|
|||
# How to publish
|
||||
|
||||
1. run `pnpm generateTypes` in the project root
|
||||
2. bump package.json version
|
||||
3. npm publish
|
|
@ -1,11 +0,0 @@
|
|||
# Vencord Types
|
||||
|
||||
Typings for Vencord's api, published to npm
|
||||
|
||||
```sh
|
||||
npm i @vencord/types
|
||||
|
||||
yarn add @vencord/types
|
||||
|
||||
pnpm add @vencord/types
|
||||
```
|
24
packages/vencord-types/globals.d.ts
vendored
24
packages/vencord-types/globals.d.ts
vendored
|
@ -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/>.
|
||||
*/
|
||||
|
||||
declare global {
|
||||
export var VencordNative: typeof import("./VencordNative").default;
|
||||
export var Vencord: typeof import("./Vencord");
|
||||
}
|
||||
|
||||
export { };
|
5
packages/vencord-types/index.d.ts
vendored
5
packages/vencord-types/index.d.ts
vendored
|
@ -1,5 +0,0 @@
|
|||
/* eslint-disable */
|
||||
|
||||
/// <reference path="Vencord.d.ts" />
|
||||
/// <reference path="globals.d.ts" />
|
||||
/// <reference path="modules.d.ts" />
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"name": "@vencord/types",
|
||||
"private": false,
|
||||
"version": "0.1.3",
|
||||
"description": "",
|
||||
"types": "index.d.ts",
|
||||
"scripts": {
|
||||
"prepublishOnly": "tsx ./prepare.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Vencord",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"tsx": "^3.12.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"discord-types": "^1.3.26",
|
||||
"standalone-electron-types": "^1.0.0",
|
||||
"type-fest": "^3.5.3"
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { cpSync, moveSync, readdirSync, rmSync } from "fs-extra";
|
||||
import { join } from "path";
|
||||
|
||||
readdirSync(join(__dirname, "src"))
|
||||
.forEach(child => moveSync(join(__dirname, "src", child), join(__dirname, child), { overwrite: true }));
|
||||
|
||||
const VencordSrc = join(__dirname, "..", "..", "src");
|
||||
|
||||
for (const file of ["preload.d.ts", "userplugins", "main", "debug", "src", "browser", "scripts"]) {
|
||||
rmSync(join(__dirname, file), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function copyDtsFiles(from: string, to: string) {
|
||||
for (const file of readdirSync(from, { withFileTypes: true })) {
|
||||
// bad
|
||||
if (from === VencordSrc && file.name === "globals.d.ts") continue;
|
||||
|
||||
const fullFrom = join(from, file.name);
|
||||
const fullTo = join(to, file.name);
|
||||
|
||||
if (file.isDirectory()) {
|
||||
copyDtsFiles(fullFrom, fullTo);
|
||||
} else if (file.name.endsWith(".d.ts")) {
|
||||
cpSync(fullFrom, fullTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
copyDtsFiles(VencordSrc, __dirname);
|
13
patches/eslint-plugin-path-alias@1.0.0.patch
Normal file
13
patches/eslint-plugin-path-alias@1.0.0.patch
Normal file
|
@ -0,0 +1,13 @@
|
|||
diff --git a/lib/rules/no-relative.js b/lib/rules/no-relative.js
|
||||
index 71594c83f1f4f733ffcc6047d7f7084348335dbe..d8623d87c89499c442171db3272cba07c9efabbe 100644
|
||||
--- a/lib/rules/no-relative.js
|
||||
+++ b/lib/rules/no-relative.js
|
||||
@@ -41,7 +41,7 @@ module.exports = {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
|
||||
- if (!/^(\.?\.\/)/.test(importPath)) {
|
||||
+ if (!/^(\.\.\/)/.test(importPath)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
diff --git a/dist/index.js b/dist/index.js
|
||||
index 67de6fb139070fd0e49beca65e3b63c531202e16..aa2883c8126e4952a42872ee920f59547a066430 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -1 +1 @@
|
||||
-var C=Object.create;var f=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var S=Object.getPrototypeOf,F=Object.prototype.hasOwnProperty;var $=(e,t)=>{for(var r in t)f(e,r,{get:t[r],enumerable:!0})},y=(e,t,r,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of U(t))!F.call(e,s)&&s!==r&&f(e,s,{get:()=>t[s],enumerable:!(i=I(t,s))||i.enumerable});return e};var b=(e,t,r)=>(r=e!=null?C(S(e)):{},y(t||!e||!e.__esModule?f(r,"default",{value:e,enumerable:!0}):r,e)),D=e=>y(f({},"__esModule",{value:!0}),e);var N={};$(N,{default:()=>J});module.exports=D(N);var h="eslint-plugin-path-alias",v="2.0.0";var l=require("path"),M=b(require("nanomatch"));function j(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}var R=require("get-tsconfig"),a=require("path"),w=b(require("find-pkg")),O=require("fs");function P(e){if(e.options[0]?.paths)return z(e);let t=e.getFilename?.()??e.filename,r=(0,R.getTsconfig)(t);if(r?.config?.compilerOptions?.paths)return q(r);let i=w.default.sync((0,a.dirname)(t));if(!i)return;let s=JSON.parse((0,O.readFileSync)(i).toString());if(s?.imports)return L(s,i)}function L(e,t){let r=new Map,i=e.imports??{},s=(0,a.dirname)(t);return Object.entries(i).forEach(([o,n])=>{if(!n||typeof n!="string")return;let p=(0,a.resolve)(s,n);r.set(o,[p])}),r}function q(e){let t=new Map,r=e?.config?.compilerOptions?.paths??{},i=(0,a.dirname)(e.path);return e.config.compilerOptions?.baseUrl&&(i=(0,a.resolve)((0,a.dirname)(e.path),e.config.compilerOptions.baseUrl)),Object.entries(r).forEach(([s,o])=>{s=s.replace(/\/\*$/,""),o=o.map(n=>(0,a.resolve)(i,n.replace(/\/\*$/,""))),t.set(s,o)}),t}function z(e){let t=new Map,r=e.options[0]?.paths??{};return Object.entries(r).forEach(([i,s])=>{if(!s||typeof s!="string")return;if(s.startsWith("/")){t.set(i,[s]);return}let o=e.getCwd?.()??e.cwd,n=(0,a.resolve)(o,s);t.set(i,[n])}),t}var T={meta:{type:"suggestion",docs:{description:"Ensure imports use path aliases whenever possible vs. relative paths",url:j("no-relative")},fixable:"code",schema:[{type:"object",properties:{exceptions:{type:"array",items:{type:"string"}},paths:{type:"object"}},additionalProperties:!1}],messages:{shouldUseAlias:"Import should use path alias instead of relative path"}},create(e){let t=e.options[0]?.exceptions,r=e.getFilename?.()??e.filename,i=P(e);return i?.size?{ImportExpression(s){if(s.source.type!=="Literal"||typeof s.source.value!="string")return;let o=s.source.raw,n=s.source.value;if(!/^(\.?\.\/)/.test(n))return;let p=(0,l.resolve)((0,l.dirname)(r),n);if(A(p,t))return;let c=k(p,i);c&&e.report({node:s,messageId:"shouldUseAlias",data:{alias:c},fix(m){let g=E(p,c,i.get(c)),d=o.replace(n,g);return m.replaceText(s.source,d)}})},ImportDeclaration(s){if(typeof s.source.value!="string")return;let o=s.source.value;if(!/^(\.?\.\/)/.test(o))return;let n=(0,l.resolve)((0,l.dirname)(r),o),p=A(n,t),u=k(n,i);p||u&&e.report({node:s,messageId:"shouldUseAlias",data:{alias:u},fix(c){let m=s.source.raw,g=E(n,u,i.get(u)),d=m.replace(o,g);return c.replaceText(s.source,d)}})}}:{}}};function k(e,t){return Array.from(t.keys()).find(r=>t.get(r).some(s=>e.indexOf(s)===0))}function A(e,t){if(!t)return!1;let r=(0,l.basename)(e);return(0,M.default)(r,t).includes(r)}function E(e,t,r){for(let i of r)if(e.indexOf(i)===0)return e.replace(i,t)}var J={name:h,version:v,meta:{name:h,version:v},rules:{"no-relative":T}};
|
||||
+var C=Object.create;var f=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var S=Object.getPrototypeOf,F=Object.prototype.hasOwnProperty;var $=(e,t)=>{for(var r in t)f(e,r,{get:t[r],enumerable:!0})},y=(e,t,r,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of U(t))!F.call(e,s)&&s!==r&&f(e,s,{get:()=>t[s],enumerable:!(i=I(t,s))||i.enumerable});return e};var b=(e,t,r)=>(r=e!=null?C(S(e)):{},y(t||!e||!e.__esModule?f(r,"default",{value:e,enumerable:!0}):r,e)),D=e=>y(f({},"__esModule",{value:!0}),e);var N={};$(N,{default:()=>J});module.exports=D(N);var h="eslint-plugin-path-alias",v="2.0.0";var l=require("path"),M=b(require("nanomatch"));function j(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}var R=require("get-tsconfig"),a=require("path"),w=b(require("find-pkg")),O=require("fs");function P(e){if(e.options[0]?.paths)return z(e);let t=e.getFilename?.()??e.filename,r=(0,R.getTsconfig)(t);if(r?.config?.compilerOptions?.paths)return q(r);let i=w.default.sync((0,a.dirname)(t));if(!i)return;let s=JSON.parse((0,O.readFileSync)(i).toString());if(s?.imports)return L(s,i)}function L(e,t){let r=new Map,i=e.imports??{},s=(0,a.dirname)(t);return Object.entries(i).forEach(([o,n])=>{if(!n||typeof n!="string")return;let p=(0,a.resolve)(s,n);r.set(o,[p])}),r}function q(e){let t=new Map,r=e?.config?.compilerOptions?.paths??{},i=(0,a.dirname)(e.path);return e.config.compilerOptions?.baseUrl&&(i=(0,a.resolve)((0,a.dirname)(e.path),e.config.compilerOptions.baseUrl)),Object.entries(r).forEach(([s,o])=>{s=s.replace(/\/\*$/,""),o=o.map(n=>(0,a.resolve)(i,n.replace(/\/\*$/,""))),t.set(s,o)}),t}function z(e){let t=new Map,r=e.options[0]?.paths??{};return Object.entries(r).forEach(([i,s])=>{if(!s||typeof s!="string")return;if(s.startsWith("/")){t.set(i,[s]);return}let o=e.getCwd?.()??e.cwd,n=(0,a.resolve)(o,s);t.set(i,[n])}),t}var T={meta:{type:"suggestion",docs:{description:"Ensure imports use path aliases whenever possible vs. relative paths",url:j("no-relative")},fixable:"code",schema:[{type:"object",properties:{exceptions:{type:"array",items:{type:"string"}},paths:{type:"object"}},additionalProperties:!1}],messages:{shouldUseAlias:"Import should use path alias instead of relative path"}},create(e){let t=e.options[0]?.exceptions,r=e.getFilename?.()??e.filename,i=P(e);return i?.size?{ImportExpression(s){if(s.source.type!=="Literal"||typeof s.source.value!="string")return;let o=s.source.raw,n=s.source.value;if(!/^(\.\.\/)/.test(n))return;let p=(0,l.resolve)((0,l.dirname)(r),n);if(A(p,t))return;let c=k(p,i);c&&e.report({node:s,messageId:"shouldUseAlias",data:{alias:c},fix(m){let g=E(p,c,i.get(c)),d=o.replace(n,g);return m.replaceText(s.source,d)}})},ImportDeclaration(s){if(typeof s.source.value!="string")return;let o=s.source.value;if(!/^(\.\.\/)/.test(o))return;let n=(0,l.resolve)((0,l.dirname)(r),o),p=A(n,t),u=k(n,i);p||u&&e.report({node:s,messageId:"shouldUseAlias",data:{alias:u},fix(c){let m=s.source.raw,g=E(n,u,i.get(u)),d=m.replace(o,g);return c.replaceText(s.source,d)}})}}:{}}};function k(e,t){return Array.from(t.keys()).find(r=>t.get(r).some(s=>e.indexOf(s)===0))}function A(e,t){if(!t)return!1;let r=(0,l.basename)(e);return(0,M.default)(r,t).includes(r)}function E(e,t,r){for(let i of r)if(e.indexOf(i)===0)return e.replace(i,t)}var J={name:h,version:v,meta:{name:h,version:v},rules:{"no-relative":T}};
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index 96de18e06d4cc413e11af038cd760e4804c32e59..27e8c4e3e2c942400cc3982e52159904ca6eedfa 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -1 +1 @@
|
||||
-var d="eslint-plugin-path-alias",h="2.0.0";import{dirname as x,resolve as j,basename as I}from"path";import U from"nanomatch";function y(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}import{getTsconfig as k}from"get-tsconfig";import{resolve as c,dirname as u}from"path";import A from"find-pkg";import{readFileSync as E}from"fs";function b(e){if(e.options[0]?.paths)return C(e);let s=e.getFilename?.()??e.filename,i=k(s);if(i?.config?.compilerOptions?.paths)return T(i);let r=A.sync(u(s));if(!r)return;let t=JSON.parse(E(r).toString());if(t?.imports)return M(t,r)}function M(e,s){let i=new Map,r=e.imports??{},t=u(s);return Object.entries(r).forEach(([o,n])=>{if(!n||typeof n!="string")return;let a=c(t,n);i.set(o,[a])}),i}function T(e){let s=new Map,i=e?.config?.compilerOptions?.paths??{},r=u(e.path);return e.config.compilerOptions?.baseUrl&&(r=c(u(e.path),e.config.compilerOptions.baseUrl)),Object.entries(i).forEach(([t,o])=>{t=t.replace(/\/\*$/,""),o=o.map(n=>c(r,n.replace(/\/\*$/,""))),s.set(t,o)}),s}function C(e){let s=new Map,i=e.options[0]?.paths??{};return Object.entries(i).forEach(([r,t])=>{if(!t||typeof t!="string")return;if(t.startsWith("/")){s.set(r,[t]);return}let o=e.getCwd?.()??e.cwd,n=c(o,t);s.set(r,[n])}),s}var P={meta:{type:"suggestion",docs:{description:"Ensure imports use path aliases whenever possible vs. relative paths",url:y("no-relative")},fixable:"code",schema:[{type:"object",properties:{exceptions:{type:"array",items:{type:"string"}},paths:{type:"object"}},additionalProperties:!1}],messages:{shouldUseAlias:"Import should use path alias instead of relative path"}},create(e){let s=e.options[0]?.exceptions,i=e.getFilename?.()??e.filename,r=b(e);return r?.size?{ImportExpression(t){if(t.source.type!=="Literal"||typeof t.source.value!="string")return;let o=t.source.raw,n=t.source.value;if(!/^(\.?\.\/)/.test(n))return;let a=j(x(i),n);if(w(a,s))return;let l=R(a,r);l&&e.report({node:t,messageId:"shouldUseAlias",data:{alias:l},fix(f){let m=O(a,l,r.get(l)),g=o.replace(n,m);return f.replaceText(t.source,g)}})},ImportDeclaration(t){if(typeof t.source.value!="string")return;let o=t.source.value;if(!/^(\.?\.\/)/.test(o))return;let n=j(x(i),o),a=w(n,s),p=R(n,r);a||p&&e.report({node:t,messageId:"shouldUseAlias",data:{alias:p},fix(l){let f=t.source.raw,m=O(n,p,r.get(p)),g=f.replace(o,m);return l.replaceText(t.source,g)}})}}:{}}};function R(e,s){return Array.from(s.keys()).find(i=>s.get(i).some(t=>e.indexOf(t)===0))}function w(e,s){if(!s)return!1;let i=I(e);return U(i,s).includes(i)}function O(e,s,i){for(let r of i)if(e.indexOf(r)===0)return e.replace(r,s)}var Q={name:d,version:h,meta:{name:d,version:h},rules:{"no-relative":P}};export{Q as default};
|
||||
+var d="eslint-plugin-path-alias",h="2.0.0";import{dirname as x,resolve as j,basename as I}from"path";import U from"nanomatch";function y(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}import{getTsconfig as k}from"get-tsconfig";import{resolve as c,dirname as u}from"path";import A from"find-pkg";import{readFileSync as E}from"fs";function b(e){if(e.options[0]?.paths)return C(e);let s=e.getFilename?.()??e.filename,i=k(s);if(i?.config?.compilerOptions?.paths)return T(i);let r=A.sync(u(s));if(!r)return;let t=JSON.parse(E(r).toString());if(t?.imports)return M(t,r)}function M(e,s){let i=new Map,r=e.imports??{},t=u(s);return Object.entries(r).forEach(([o,n])=>{if(!n||typeof n!="string")return;let a=c(t,n);i.set(o,[a])}),i}function T(e){let s=new Map,i=e?.config?.compilerOptions?.paths??{},r=u(e.path);return e.config.compilerOptions?.baseUrl&&(r=c(u(e.path),e.config.compilerOptions.baseUrl)),Object.entries(i).forEach(([t,o])=>{t=t.replace(/\/\*$/,""),o=o.map(n=>c(r,n.replace(/\/\*$/,""))),s.set(t,o)}),s}function C(e){let s=new Map,i=e.options[0]?.paths??{};return Object.entries(i).forEach(([r,t])=>{if(!t||typeof t!="string")return;if(t.startsWith("/")){s.set(r,[t]);return}let o=e.getCwd?.()??e.cwd,n=c(o,t);s.set(r,[n])}),s}var P={meta:{type:"suggestion",docs:{description:"Ensure imports use path aliases whenever possible vs. relative paths",url:y("no-relative")},fixable:"code",schema:[{type:"object",properties:{exceptions:{type:"array",items:{type:"string"}},paths:{type:"object"}},additionalProperties:!1}],messages:{shouldUseAlias:"Import should use path alias instead of relative path"}},create(e){let s=e.options[0]?.exceptions,i=e.getFilename?.()??e.filename,r=b(e);return r?.size?{ImportExpression(t){if(t.source.type!=="Literal"||typeof t.source.value!="string")return;let o=t.source.raw,n=t.source.value;if(!/^(\.\.\/)/.test(n))return;let a=j(x(i),n);if(w(a,s))return;let l=R(a,r);l&&e.report({node:t,messageId:"shouldUseAlias",data:{alias:l},fix(f){let m=O(a,l,r.get(l)),g=o.replace(n,m);return f.replaceText(t.source,g)}})},ImportDeclaration(t){if(typeof t.source.value!="string")return;let o=t.source.value;if(!/^(\.\.\/)/.test(o))return;let n=j(x(i),o),a=w(n,s),p=R(n,r);a||p&&e.report({node:t,messageId:"shouldUseAlias",data:{alias:p},fix(l){let f=t.source.raw,m=O(n,p,r.get(p)),g=f.replace(o,m);return l.replaceText(t.source,g)}})}}:{}}};function R(e,s){return Array.from(s.keys()).find(i=>s.get(i).some(t=>e.indexOf(t)===0))}function w(e,s){if(!s)return!1;let i=I(e);return U(i,s).includes(i)}function O(e,s,i){for(let r of i)if(e.indexOf(r)===0)return e.replace(r,s)}var Q={name:d,version:h,meta:{name:d,version:h},rules:{"no-relative":P}};export{Q as default};
|
6579
pnpm-lock.yaml
6579
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,2 +0,0 @@
|
|||
packages:
|
||||
- packages/*
|
|
@ -21,21 +21,19 @@ import esbuild from "esbuild";
|
|||
import { readdir } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch } from "./common.mjs";
|
||||
import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
|
||||
|
||||
const defines = {
|
||||
IS_STANDALONE,
|
||||
IS_DEV,
|
||||
IS_REPORTER,
|
||||
IS_UPDATER_DISABLED,
|
||||
IS_STANDALONE: isStandalone,
|
||||
IS_DEV: JSON.stringify(watch),
|
||||
IS_UPDATER_DISABLED: updaterDisabled,
|
||||
IS_WEB: false,
|
||||
IS_EXTENSION: false,
|
||||
VERSION: JSON.stringify(VERSION),
|
||||
BUILD_TIMESTAMP
|
||||
BUILD_TIMESTAMP,
|
||||
};
|
||||
|
||||
if (defines.IS_STANDALONE === false)
|
||||
// If this is a local build (not standalone), optimize
|
||||
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);
|
||||
|
||||
|
@ -48,7 +46,7 @@ const nodeCommonOpts = {
|
|||
platform: "node",
|
||||
target: ["esnext"],
|
||||
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
|
||||
define: defines
|
||||
define: defines,
|
||||
};
|
||||
|
||||
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
|
||||
|
@ -75,21 +73,19 @@ const globNativesPlugin = {
|
|||
let i = 0;
|
||||
for (const dir of pluginDirs) {
|
||||
const dirPath = join("src", dir);
|
||||
if (!await exists(dirPath)) continue;
|
||||
const plugins = await readdir(dirPath, { withFileTypes: true });
|
||||
for (const file of plugins) {
|
||||
const fileName = file.name;
|
||||
const nativePath = join(dirPath, fileName, "native.ts");
|
||||
const indexNativePath = join(dirPath, fileName, "native/index.ts");
|
||||
if (!await existsAsync(dirPath)) continue;
|
||||
const plugins = await readdir(dirPath);
|
||||
for (const p of plugins) {
|
||||
if (!await existsAsync(join(dirPath, p, "native.ts"))) continue;
|
||||
|
||||
if (!(await exists(nativePath)) && !(await exists(indexNativePath)))
|
||||
continue;
|
||||
|
||||
const pluginName = await resolvePluginName(dirPath, file);
|
||||
const nameParts = p.split(".");
|
||||
const namePartsWithoutTarget = nameParts.length === 1 ? nameParts : nameParts.slice(0, -1);
|
||||
// pluginName.thing.desktop -> PluginName.thing
|
||||
const cleanPluginName = p[0].toUpperCase() + namePartsWithoutTarget.join(".").slice(1);
|
||||
|
||||
const mod = `p${i}`;
|
||||
code += `import * as ${mod} from "./${dir}/${fileName}/native";\n`;
|
||||
natives += `${JSON.stringify(pluginName)}:${mod},\n`;
|
||||
code += `import * as ${mod} from "./${dir}/${p}/native";\n`;
|
||||
natives += `${JSON.stringify(cleanPluginName)}:${mod},\n`;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
@ -131,7 +127,7 @@ await Promise.all([
|
|||
sourcemap,
|
||||
plugins: [
|
||||
globPlugins("discordDesktop"),
|
||||
...commonRendererPlugins
|
||||
...commonOpts.plugins
|
||||
],
|
||||
define: {
|
||||
...defines,
|
||||
|
@ -180,7 +176,7 @@ await Promise.all([
|
|||
sourcemap,
|
||||
plugins: [
|
||||
globPlugins("vencordDesktop"),
|
||||
...commonRendererPlugins
|
||||
...commonOpts.plugins
|
||||
],
|
||||
define: {
|
||||
...defines,
|
||||
|
|
|
@ -23,7 +23,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises
|
|||
import { join } from "path";
|
||||
import Zip from "zip-local";
|
||||
|
||||
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION, commonRendererPlugins } from "./common.mjs";
|
||||
import { BUILD_TIMESTAMP, commonOpts, globPlugins, VERSION, watch } from "./common.mjs";
|
||||
|
||||
/**
|
||||
* @type {esbuild.BuildOptions}
|
||||
|
@ -33,23 +33,22 @@ const commonOptions = {
|
|||
entryPoints: ["browser/Vencord.ts"],
|
||||
globalName: "Vencord",
|
||||
format: "iife",
|
||||
external: ["~plugins", "~git-hash", "/assets/*"],
|
||||
external: ["plugins", "git-hash", "/assets/*"],
|
||||
plugins: [
|
||||
globPlugins("web"),
|
||||
...commonRendererPlugins
|
||||
...commonOpts.plugins,
|
||||
],
|
||||
target: ["esnext"],
|
||||
define: {
|
||||
IS_WEB: true,
|
||||
IS_EXTENSION: false,
|
||||
IS_STANDALONE: true,
|
||||
IS_DEV,
|
||||
IS_REPORTER,
|
||||
IS_DISCORD_DESKTOP: false,
|
||||
IS_VESKTOP: false,
|
||||
IS_UPDATER_DISABLED: true,
|
||||
IS_WEB: "true",
|
||||
IS_EXTENSION: "false",
|
||||
IS_STANDALONE: "true",
|
||||
IS_DEV: JSON.stringify(watch),
|
||||
IS_DISCORD_DESKTOP: "false",
|
||||
IS_VESKTOP: "false",
|
||||
IS_UPDATER_DISABLED: "true",
|
||||
VERSION: JSON.stringify(VERSION),
|
||||
BUILD_TIMESTAMP
|
||||
BUILD_TIMESTAMP,
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -88,16 +87,16 @@ await Promise.all(
|
|||
esbuild.build({
|
||||
...commonOptions,
|
||||
outfile: "dist/browser.js",
|
||||
footer: { js: "//# sourceURL=VencordWeb" }
|
||||
footer: { js: "//# sourceURL=VencordWeb" },
|
||||
}),
|
||||
esbuild.build({
|
||||
...commonOptions,
|
||||
outfile: "dist/extension.js",
|
||||
define: {
|
||||
...commonOptions?.define,
|
||||
IS_EXTENSION: true,
|
||||
IS_EXTENSION: "true",
|
||||
},
|
||||
footer: { js: "//# sourceURL=VencordWeb" }
|
||||
footer: { js: "//# sourceURL=VencordWeb" },
|
||||
}),
|
||||
esbuild.build({
|
||||
...commonOptions,
|
||||
|
@ -113,15 +112,10 @@ await Promise.all(
|
|||
footer: {
|
||||
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
|
||||
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
|
||||
}
|
||||
},
|
||||
})
|
||||
]
|
||||
).catch(err => {
|
||||
console.error("Build failed");
|
||||
console.error(err.message);
|
||||
if (!commonOpts.watch)
|
||||
process.exit(1);
|
||||
});;
|
||||
);
|
||||
|
||||
/**
|
||||
* @type {(dir: string) => Promise<string[]>}
|
||||
|
@ -171,7 +165,7 @@ async function buildExtension(target, files) {
|
|||
f.startsWith("manifest") ? "manifest.json" : f,
|
||||
content
|
||||
];
|
||||
})))
|
||||
}))),
|
||||
};
|
||||
|
||||
await rm(target, { recursive: true, force: true });
|
||||
|
@ -198,19 +192,14 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
|
|||
return appendFile("dist/Vencord.user.js", cssRuntime);
|
||||
});
|
||||
|
||||
if (!process.argv.includes("--skip-extension")) {
|
||||
await Promise.all([
|
||||
appendCssRuntime,
|
||||
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
|
||||
buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]),
|
||||
]);
|
||||
await Promise.all([
|
||||
appendCssRuntime,
|
||||
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
|
||||
buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]),
|
||||
]);
|
||||
|
||||
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip");
|
||||
console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
|
||||
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip");
|
||||
console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
|
||||
|
||||
Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
|
||||
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
|
||||
|
||||
} else {
|
||||
await appendCssRuntime;
|
||||
}
|
||||
Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
|
||||
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
|
||||
|
|
|
@ -20,68 +20,35 @@ import "../suppressExperimentalWarnings.js";
|
|||
import "../checkNodeVersion.js";
|
||||
|
||||
import { exec, execSync } from "child_process";
|
||||
import esbuild from "esbuild";
|
||||
import { constants as FsConstants, readFileSync } from "fs";
|
||||
import { access, readdir, readFile } from "fs/promises";
|
||||
import { minify as minifyHtml } from "html-minifier-terser";
|
||||
import { join, relative } from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
// wtf is this assert syntax
|
||||
import PackageJSON from "../../package.json" assert { type: "json" };
|
||||
import { getPluginTarget } from "../utils.mjs";
|
||||
import { builtinModules } from "module";
|
||||
|
||||
/** @type {import("../../package.json")} */
|
||||
const PackageJSON = JSON.parse(readFileSync("package.json"));
|
||||
|
||||
export const VERSION = PackageJSON.version;
|
||||
// https://reproducible-builds.org/docs/source-date-epoch/
|
||||
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
|
||||
|
||||
export const watch = process.argv.includes("--watch");
|
||||
export const IS_DEV = watch || process.argv.includes("--dev");
|
||||
export const IS_REPORTER = process.argv.includes("--reporter");
|
||||
export const IS_STANDALONE = process.argv.includes("--standalone");
|
||||
|
||||
export const IS_UPDATER_DISABLED = process.argv.includes("--disable-updater");
|
||||
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
||||
export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater"));
|
||||
export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
||||
|
||||
export const banner = {
|
||||
js: `
|
||||
// Vencord ${gitHash}
|
||||
// Standalone: ${IS_STANDALONE}
|
||||
// Platform: ${IS_STANDALONE === false ? process.platform : "Universal"}
|
||||
// Updater Disabled: ${IS_UPDATER_DISABLED}
|
||||
// Standalone: ${isStandalone}
|
||||
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
|
||||
// Updater disabled: ${updaterDisabled}
|
||||
`.trim()
|
||||
};
|
||||
|
||||
const PluginDefinitionNameMatcher = /definePlugin\(\{\s*(["'])?name\1:\s*(["'`])(.+?)\2/;
|
||||
/**
|
||||
* @param {string} base
|
||||
* @param {import("fs").Dirent} dirent
|
||||
*/
|
||||
export async function resolvePluginName(base, dirent) {
|
||||
const fullPath = join(base, dirent.name);
|
||||
const content = dirent.isFile()
|
||||
? await readFile(fullPath, "utf-8")
|
||||
: await (async () => {
|
||||
for (const file of ["index.ts", "index.tsx"]) {
|
||||
try {
|
||||
return await readFile(join(fullPath, file), "utf-8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw new Error(`Invalid plugin ${fullPath}: could not resolve entry point`);
|
||||
})();
|
||||
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
|
||||
|
||||
return PluginDefinitionNameMatcher.exec(content)?.[3]
|
||||
?? (() => {
|
||||
throw new Error(`Invalid plugin ${fullPath}: must contain definePlugin call with simple string name property as first property`);
|
||||
})();
|
||||
}
|
||||
|
||||
export async function exists(path) {
|
||||
return await access(path, FsConstants.F_OK)
|
||||
export function existsAsync(path) {
|
||||
return access(path, FsConstants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
@ -95,7 +62,7 @@ export const makeAllPackagesExternalPlugin = {
|
|||
setup(build) {
|
||||
const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
|
||||
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -115,48 +82,31 @@ export const globPlugins = kind => ({
|
|||
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
|
||||
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins"];
|
||||
let code = "";
|
||||
let pluginsCode = "\n";
|
||||
let metaCode = "\n";
|
||||
let excludedCode = "\n";
|
||||
let plugins = "\n";
|
||||
let i = 0;
|
||||
for (const dir of pluginDirs) {
|
||||
const userPlugin = dir === "userplugins";
|
||||
|
||||
const fullDir = `./src/${dir}`;
|
||||
if (!await exists(fullDir)) continue;
|
||||
const files = await readdir(fullDir, { withFileTypes: true });
|
||||
if (!await existsAsync(`./src/${dir}`)) continue;
|
||||
const files = await readdir(`./src/${dir}`);
|
||||
for (const file of files) {
|
||||
const fileName = file.name;
|
||||
if (fileName.startsWith("_") || fileName.startsWith(".")) continue;
|
||||
if (fileName === "index.ts") continue;
|
||||
if (file.startsWith("_") || file.startsWith(".")) continue;
|
||||
if (file === "index.ts") continue;
|
||||
|
||||
const target = getPluginTarget(fileName);
|
||||
|
||||
if (target && !IS_REPORTER) {
|
||||
const excluded =
|
||||
(target === "dev" && !IS_DEV) ||
|
||||
(target === "web" && kind === "discordDesktop") ||
|
||||
(target === "desktop" && kind === "web") ||
|
||||
(target === "discordDesktop" && kind !== "discordDesktop") ||
|
||||
(target === "vencordDesktop" && kind !== "vencordDesktop");
|
||||
|
||||
if (excluded) {
|
||||
const name = await resolvePluginName(fullDir, file);
|
||||
excludedCode += `${JSON.stringify(name)}:${JSON.stringify(target)},\n`;
|
||||
continue;
|
||||
}
|
||||
const target = getPluginTarget(file);
|
||||
if (target) {
|
||||
if (target === "dev" && !watch) continue;
|
||||
if (target === "web" && kind === "discordDesktop") continue;
|
||||
if (target === "desktop" && kind === "web") continue;
|
||||
if (target === "discordDesktop" && kind !== "discordDesktop") continue;
|
||||
if (target === "vencordDesktop" && kind !== "vencordDesktop") continue;
|
||||
}
|
||||
|
||||
const folderName = `src/${dir}/${fileName}`.replace(/^src\/plugins\//, "");
|
||||
|
||||
const mod = `p${i}`;
|
||||
code += `import ${mod} from "./${dir}/${fileName.replace(/\.tsx?$/, "")}";\n`;
|
||||
pluginsCode += `[${mod}.name]:${mod},\n`;
|
||||
metaCode += `[${mod}.name]:${JSON.stringify({ folderName, userPlugin })},\n`; // TODO: add excluded plugins to display in the UI?
|
||||
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
|
||||
plugins += `[${mod}.name]:${mod},\n`;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
code += `export default {${pluginsCode}};export const PluginMeta={${metaCode}};export const ExcludedPlugins={${excludedCode}};`;
|
||||
code += `export default {${plugins}};`;
|
||||
return {
|
||||
contents: code,
|
||||
resolveDir: "./src"
|
||||
|
@ -209,60 +159,21 @@ export const gitRemotePlugin = {
|
|||
/**
|
||||
* @type {import("esbuild").Plugin}
|
||||
*/
|
||||
export const fileUrlPlugin = {
|
||||
name: "file-uri-plugin",
|
||||
export const fileIncludePlugin = {
|
||||
name: "file-include-plugin",
|
||||
setup: build => {
|
||||
const filter = /^file:\/\/.+$/;
|
||||
const filter = /^~fileContent\/.+$/;
|
||||
build.onResolve({ filter }, args => ({
|
||||
namespace: "file-uri",
|
||||
namespace: "include-file",
|
||||
path: args.path,
|
||||
pluginData: {
|
||||
uri: args.path,
|
||||
path: join(args.resolveDir, args.path.slice("file://".length).split("?")[0])
|
||||
path: join(args.resolveDir, args.path.slice("include-file/".length))
|
||||
}
|
||||
}));
|
||||
build.onLoad({ filter, namespace: "file-uri" }, async ({ pluginData: { path, uri } }) => {
|
||||
const { searchParams } = new URL(uri);
|
||||
const base64 = searchParams.has("base64");
|
||||
const minify = IS_STANDALONE === true && searchParams.has("minify");
|
||||
const noTrim = searchParams.get("trim") === "false";
|
||||
|
||||
const encoding = base64 ? "base64" : "utf-8";
|
||||
|
||||
let content;
|
||||
if (!minify) {
|
||||
content = await readFile(path, encoding);
|
||||
if (!noTrim) content = content.trimEnd();
|
||||
} else {
|
||||
if (path.endsWith(".html")) {
|
||||
content = await minifyHtml(await readFile(path, "utf-8"), {
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeRedundantAttributes: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
useShortDoctype: true
|
||||
});
|
||||
} else if (/[mc]?[jt]sx?$/.test(path)) {
|
||||
const res = await esbuild.build({
|
||||
entryPoints: [path],
|
||||
write: false,
|
||||
minify: true
|
||||
});
|
||||
content = res.outputFiles[0].text;
|
||||
} else {
|
||||
throw new Error(`Don't know how to minify file type: ${path}`);
|
||||
}
|
||||
|
||||
if (base64)
|
||||
content = Buffer.from(content).toString("base64");
|
||||
}
|
||||
|
||||
build.onLoad({ filter, namespace: "include-file" }, async ({ pluginData: { path } }) => {
|
||||
const [name, format] = path.split(";");
|
||||
return {
|
||||
contents: `export default ${JSON.stringify(content)}`
|
||||
contents: `export default ${JSON.stringify(await readFile(name, format ?? "utf-8"))}`
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -293,18 +204,6 @@ export const stylePlugin = {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {(filter: RegExp, message: string) => import("esbuild").Plugin}
|
||||
*/
|
||||
export const banImportPlugin = (filter, message) => ({
|
||||
name: "ban-imports",
|
||||
setup: build => {
|
||||
build.onResolve({ filter }, () => {
|
||||
return { errors: [{ text: message }] };
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @type {import("esbuild").BuildOptions}
|
||||
*/
|
||||
|
@ -316,7 +215,7 @@ export const commonOpts = {
|
|||
sourcemap: watch ? "inline" : "",
|
||||
legalComments: "linked",
|
||||
banner,
|
||||
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
||||
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
||||
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
||||
inject: ["./scripts/build/inject/react.mjs"],
|
||||
jsxFactory: "VencordCreateElement",
|
||||
|
@ -324,16 +223,3 @@ export const commonOpts = {
|
|||
// Work around https://github.com/evanw/esbuild/issues/2460
|
||||
tsconfig: "./scripts/build/tsconfig.esbuild.json"
|
||||
};
|
||||
|
||||
const escapedBuiltinModules = builtinModules
|
||||
.map(m => m.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
|
||||
.join("|");
|
||||
const builtinModuleRegex = new RegExp(`^(node:)?(${escapedBuiltinModules})$`);
|
||||
|
||||
export const commonRendererPlugins = [
|
||||
banImportPlugin(builtinModuleRegex, "Cannot import node inbuilt modules in browser code. You need to use a native.ts file"),
|
||||
banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"),
|
||||
banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"),
|
||||
banImportPlugin(/^ts-pattern$/, "Cannot import from ts-pattern. match and P should be imported from @webpack/common"),
|
||||
...commonOpts.plugins
|
||||
];
|
||||
|
|
|
@ -39,7 +39,7 @@ interface PluginData {
|
|||
hasCommands: boolean;
|
||||
required: boolean;
|
||||
enabledByDefault: boolean;
|
||||
target: "discordDesktop" | "vencordDesktop" | "desktop" | "web" | "dev";
|
||||
target: "discordDesktop" | "vencordDesktop" | "web" | "dev";
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-fallthrough */
|
||||
|
||||
// eslint-disable-next-line spaced-comment
|
||||
/// <reference types="../src/globals" />
|
||||
// eslint-disable-next-line spaced-comment
|
||||
|
@ -42,12 +40,10 @@ const browser = await pup.launch({
|
|||
|
||||
const page = await browser.newPage();
|
||||
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
|
||||
await page.setBypassCSP(true);
|
||||
|
||||
async function maybeGetError(handle: JSHandle): Promise<string | undefined> {
|
||||
return await (handle as JSHandle<Error>)?.getProperty("message")
|
||||
.then(m => m?.jsonValue())
|
||||
.catch(() => undefined);
|
||||
function maybeGetError(handle: JSHandle) {
|
||||
return (handle as JSHandle<Error>)?.getProperty("message")
|
||||
.then(m => m.jsonValue());
|
||||
}
|
||||
|
||||
const report = {
|
||||
|
@ -62,72 +58,50 @@ const report = {
|
|||
plugin: string;
|
||||
error: string;
|
||||
}[],
|
||||
otherErrors: [] as string[],
|
||||
ignoredErrors: [] as string[],
|
||||
badWebpackFinds: [] as string[]
|
||||
otherErrors: [] as string[]
|
||||
};
|
||||
|
||||
const IGNORED_DISCORD_ERRORS = [
|
||||
"KeybindStore: Looking for callback action",
|
||||
"Unable to process domain list delta: Client revision number is null",
|
||||
"Downloading the full bad domains file",
|
||||
/\[GatewaySocket\].{0,110}Cannot access '/,
|
||||
"search for 'name' in undefined",
|
||||
"Attempting to set fast connect zstd when unsupported"
|
||||
/\[GatewaySocket\].{0,110}Cannot access '/
|
||||
] as Array<string | RegExp>;
|
||||
|
||||
function toCodeBlock(s: string, indentation = 0, isDiscord = false) {
|
||||
function toCodeBlock(s: string) {
|
||||
s = s.replace(/```/g, "`\u200B`\u200B`");
|
||||
|
||||
const indentationStr = Array(!isDiscord ? indentation : 0).fill(" ").join("");
|
||||
return `\`\`\`\n${s.split("\n").map(s => indentationStr + s).join("\n")}\n${indentationStr}\`\`\``;
|
||||
return "```" + s + " ```";
|
||||
}
|
||||
|
||||
async function printReport() {
|
||||
console.log();
|
||||
|
||||
console.log("# Vencord Report" + (CANARY ? " (Canary)" : ""));
|
||||
|
||||
console.log();
|
||||
|
||||
console.log("## Bad Patches");
|
||||
report.badPatches.forEach(p => {
|
||||
console.log(`- ${p.plugin} (${p.type})`);
|
||||
console.log(` - ID: \`${p.id}\``);
|
||||
console.log(` - Match: ${toCodeBlock(p.match, " - Match: ".length)}`);
|
||||
if (p.error) console.log(` - Error: ${toCodeBlock(p.error, " - Error: ".length)}`);
|
||||
console.log(` - Match: ${toCodeBlock(p.match)}`);
|
||||
if (p.error) console.log(` - Error: ${toCodeBlock(p.error)}`);
|
||||
});
|
||||
|
||||
console.log();
|
||||
|
||||
console.log("## Bad Webpack Finds");
|
||||
report.badWebpackFinds.forEach(p => console.log("- " + toCodeBlock(p, "- ".length)));
|
||||
|
||||
console.log();
|
||||
|
||||
console.log("## Bad Starts");
|
||||
report.badStarts.forEach(p => {
|
||||
console.log(`- ${p.plugin}`);
|
||||
console.log(` - Error: ${toCodeBlock(p.error, " - Error: ".length)}`);
|
||||
console.log(` - Error: ${toCodeBlock(p.error)}`);
|
||||
});
|
||||
|
||||
console.log();
|
||||
report.otherErrors = report.otherErrors.filter(e => !IGNORED_DISCORD_ERRORS.some(regex => e.match(regex)));
|
||||
|
||||
console.log("## Discord Errors");
|
||||
report.otherErrors.forEach(e => {
|
||||
console.log(`- ${toCodeBlock(e, "- ".length)}`);
|
||||
console.log(`- ${toCodeBlock(e)}`);
|
||||
});
|
||||
|
||||
console.log();
|
||||
|
||||
console.log("## Ignored Discord Errors");
|
||||
report.ignoredErrors.forEach(e => {
|
||||
console.log(`- ${toCodeBlock(e, "- ".length)}`);
|
||||
});
|
||||
|
||||
console.log();
|
||||
|
||||
if (process.env.DISCORD_WEBHOOK) {
|
||||
// this code was written almost entirely by Copilot xD
|
||||
await fetch(process.env.DISCORD_WEBHOOK, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
@ -136,6 +110,7 @@ async function printReport() {
|
|||
body: JSON.stringify({
|
||||
description: "Here's the latest Vencord Report!",
|
||||
username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""),
|
||||
avatar_url: "https://cdn.discordapp.com/icons/1015060230222131221/f0204a918c6c9c9a43195997e97d8adf.webp",
|
||||
embeds: [
|
||||
{
|
||||
title: "Bad Patches",
|
||||
|
@ -143,24 +118,19 @@ async function printReport() {
|
|||
const lines = [
|
||||
`**__${p.plugin} (${p.type}):__**`,
|
||||
`ID: \`${p.id}\``,
|
||||
`Match: ${toCodeBlock(p.match, "Match: ".length, true)}`
|
||||
`Match: ${toCodeBlock(p.match)}`
|
||||
];
|
||||
if (p.error) lines.push(`Error: ${toCodeBlock(p.error, "Error: ".length, true)}`);
|
||||
if (p.error) lines.push(`Error: ${toCodeBlock(p.error)}`);
|
||||
return lines.join("\n");
|
||||
}).join("\n\n") || "None",
|
||||
color: report.badPatches.length ? 0xff0000 : 0x00ff00
|
||||
},
|
||||
{
|
||||
title: "Bad Webpack Finds",
|
||||
description: report.badWebpackFinds.map(f => toCodeBlock(f, 0, true)).join("\n") || "None",
|
||||
color: report.badWebpackFinds.length ? 0xff0000 : 0x00ff00
|
||||
},
|
||||
{
|
||||
title: "Bad Starts",
|
||||
description: report.badStarts.map(p => {
|
||||
const lines = [
|
||||
`**__${p.plugin}:__**`,
|
||||
toCodeBlock(p.error, 0, true)
|
||||
toCodeBlock(p.error)
|
||||
];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
@ -169,7 +139,7 @@ async function printReport() {
|
|||
},
|
||||
{
|
||||
title: "Discord Errors",
|
||||
description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n"), 0, true) : "None",
|
||||
description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n")) : "None",
|
||||
color: report.otherErrors.length ? 0xff0000 : 0x00ff00
|
||||
}
|
||||
]
|
||||
|
@ -183,137 +153,152 @@ async function printReport() {
|
|||
|
||||
page.on("console", async e => {
|
||||
const level = e.type();
|
||||
const rawArgs = e.args();
|
||||
const args = e.args();
|
||||
|
||||
async function getText() {
|
||||
try {
|
||||
return await Promise.all(
|
||||
e.args().map(async a => {
|
||||
return await maybeGetError(a) || await a.jsonValue();
|
||||
})
|
||||
).then(a => a.join(" ").trim());
|
||||
} catch {
|
||||
return e.text();
|
||||
}
|
||||
const firstArg = (await args[0]?.jsonValue());
|
||||
if (firstArg === "PUPPETEER_TEST_DONE_SIGNAL") {
|
||||
await browser.close();
|
||||
await printReport();
|
||||
process.exit();
|
||||
}
|
||||
|
||||
const firstArg = await rawArgs[0]?.jsonValue();
|
||||
const isVencord = (await args[0]?.jsonValue()) === "[Vencord]";
|
||||
const isDebug = (await args[0]?.jsonValue()) === "[PUP_DEBUG]";
|
||||
|
||||
const isVencord = firstArg === "[Vencord]";
|
||||
const isDebug = firstArg === "[PUP_DEBUG]";
|
||||
|
||||
outer:
|
||||
if (isVencord) {
|
||||
try {
|
||||
var args = await Promise.all(e.args().map(a => a.jsonValue()));
|
||||
} catch {
|
||||
break outer;
|
||||
}
|
||||
// make ci fail
|
||||
process.exitCode = 1;
|
||||
|
||||
const [, tag, message, otherMessage] = args as Array<string>;
|
||||
const jsonArgs = await Promise.all(args.map(a => a.jsonValue()));
|
||||
const [, tag, message] = jsonArgs;
|
||||
const cause = await maybeGetError(args[3]);
|
||||
|
||||
switch (tag) {
|
||||
case "WebpackInterceptor:":
|
||||
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
|
||||
if (!patchFailMatch) break;
|
||||
|
||||
console.error(await getText());
|
||||
process.exitCode = 1;
|
||||
|
||||
const [, plugin, type, id, regex] = patchFailMatch;
|
||||
const [, plugin, type, id, regex] = (message as string).match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
|
||||
report.badPatches.push({
|
||||
plugin,
|
||||
type,
|
||||
id,
|
||||
match: regex.replace(/\(\?:\[A-Za-z_\$\]\[\\w\$\]\*\)/g, "\\i"),
|
||||
error: await maybeGetError(e.args()[3])
|
||||
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
|
||||
error: cause
|
||||
});
|
||||
|
||||
break;
|
||||
case "PluginManager:":
|
||||
const failedToStartMatch = message.match(/Failed to start (.+)/);
|
||||
if (!failedToStartMatch) break;
|
||||
|
||||
console.error(await getText());
|
||||
process.exitCode = 1;
|
||||
|
||||
const [, name] = failedToStartMatch;
|
||||
const [, name] = (message as string).match(/Failed to start (.+)/)!;
|
||||
report.badStarts.push({
|
||||
plugin: name,
|
||||
error: await maybeGetError(e.args()[3]) ?? "Unknown error"
|
||||
error: cause
|
||||
});
|
||||
|
||||
break;
|
||||
case "LazyChunkLoader:":
|
||||
console.error(await getText());
|
||||
|
||||
switch (message) {
|
||||
case "A fatal error occurred:":
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
break;
|
||||
case "Reporter:":
|
||||
console.error(await getText());
|
||||
|
||||
switch (message) {
|
||||
case "A fatal error occurred:":
|
||||
process.exit(1);
|
||||
case "Webpack Find Fail:":
|
||||
process.exitCode = 1;
|
||||
report.badWebpackFinds.push(otherMessage);
|
||||
break;
|
||||
case "Finished test":
|
||||
await browser.close();
|
||||
await printReport();
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
console.error(await getText());
|
||||
} else if (isDebug) {
|
||||
console.error(e.text());
|
||||
} else if (level === "error") {
|
||||
const text = await getText();
|
||||
const text = await Promise.all(
|
||||
e.args().map(async a => {
|
||||
try {
|
||||
return await maybeGetError(a) || await a.jsonValue();
|
||||
} catch (e) {
|
||||
return a.toString();
|
||||
}
|
||||
})
|
||||
).then(a => a.join(" ").trim());
|
||||
|
||||
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
||||
if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) {
|
||||
report.ignoredErrors.push(text);
|
||||
} else {
|
||||
console.error("[Unexpected Error]", text);
|
||||
report.otherErrors.push(text);
|
||||
|
||||
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of")) {
|
||||
console.error("Got unexpected error", text);
|
||||
report.otherErrors.push(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
page.on("error", e => console.error("[Error]", e));
|
||||
page.on("pageerror", e => console.error("[Page Error]", e));
|
||||
|
||||
await page.setBypassCSP(true);
|
||||
|
||||
function runTime(token: string) {
|
||||
console.error("[PUP_DEBUG]", "Starting test...");
|
||||
|
||||
try {
|
||||
// spoof languages to not be suspicious
|
||||
Object.defineProperty(navigator, "languages", {
|
||||
get: function () {
|
||||
return ["en-US", "en"];
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// Monkey patch Logger to not log with custom css
|
||||
// @ts-ignore
|
||||
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
|
||||
if (level === "warn" || level === "error")
|
||||
console[level]("[Vencord]", this.name + ":", ...args);
|
||||
};
|
||||
|
||||
// force enable all plugins and patches
|
||||
Vencord.Plugins.patches.length = 0;
|
||||
Object.values(Vencord.Plugins.plugins).forEach(p => {
|
||||
// Needs native server to run
|
||||
if (p.name === "WebRichPresence (arRPC)") return;
|
||||
|
||||
Vencord.Settings.plugins[p.name].enabled = true;
|
||||
p.patches?.forEach(patch => {
|
||||
patch.plugin = p.name;
|
||||
delete patch.predicate;
|
||||
if (!Array.isArray(patch.replacement))
|
||||
patch.replacement = [patch.replacement];
|
||||
Vencord.Plugins.patches.push(patch);
|
||||
});
|
||||
});
|
||||
|
||||
Vencord.Webpack.waitFor(
|
||||
"loginToken",
|
||||
m => {
|
||||
console.error("[PUP_DEBUG]", "Logging in with token...");
|
||||
m.loginToken(token);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// force load all chunks
|
||||
Vencord.Webpack.onceReady.then(() => setTimeout(async () => {
|
||||
console.error("[PUP_DEBUG]", "Webpack is ready!");
|
||||
|
||||
const { wreq } = Vencord.Webpack;
|
||||
|
||||
console.error("[PUP_DEBUG]", "Loading all chunks...");
|
||||
const ids = Function("return" + wreq.u.toString().match(/(?<=\()\{.+?\}/s)![0])();
|
||||
for (const id in ids) {
|
||||
const isWasm = await fetch(wreq.p + wreq.u(id))
|
||||
.then(r => r.text())
|
||||
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
||||
|
||||
if (!isWasm)
|
||||
await wreq.e(id as any);
|
||||
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
}
|
||||
console.error("[PUP_DEBUG]", "Finished loading chunks!");
|
||||
|
||||
for (const patch of Vencord.Plugins.patches) {
|
||||
if (!patch.all) {
|
||||
new Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
||||
}
|
||||
}
|
||||
setTimeout(() => console.log("PUPPETEER_TEST_DONE_SIGNAL"), 1000);
|
||||
}, 1000));
|
||||
} catch (e) {
|
||||
console.error("[PUP_DEBUG]", "A fatal error occurred");
|
||||
console.error("[PUP_DEBUG]", e);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
page.on("error", e => console.error("[Error]", e.message));
|
||||
page.on("pageerror", e => {
|
||||
if (e.message.includes("Sentry successfully disabled")) return;
|
||||
|
||||
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) {
|
||||
console.error("[Page Error]", e.message);
|
||||
report.otherErrors.push(e.message);
|
||||
} else {
|
||||
report.ignoredErrors.push(e.message);
|
||||
}
|
||||
});
|
||||
|
||||
async function reporterRuntime(token: string) {
|
||||
Vencord.Webpack.waitFor(
|
||||
"loginToken",
|
||||
m => {
|
||||
console.log("[PUP_DEBUG]", "Logging in with token...");
|
||||
m.loginToken(token);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await page.evaluateOnNewDocument(`
|
||||
if (location.host.endsWith("discord.com")) {
|
||||
${readFileSync("./dist/browser.js", "utf-8")};
|
||||
(${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
||||
}
|
||||
${readFileSync("./dist/browser.js", "utf-8")}
|
||||
|
||||
;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
||||
`);
|
||||
|
||||
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
|
||||
|
|
|
@ -35,11 +35,11 @@ const ETAG_FILE = join(FILE_DIR, "etag.txt");
|
|||
function getFilename() {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
return "VencordInstallerCli.exe";
|
||||
return "VencordInstaller.exe";
|
||||
case "darwin":
|
||||
return "VencordInstaller.MacOS.zip";
|
||||
case "linux":
|
||||
return "VencordInstallerCli-linux";
|
||||
return "VencordInstaller-" + (process.env.WAYLAND_DISPLAY ? "wayland" : "x11");
|
||||
default:
|
||||
throw new Error("Unsupported platform: " + process.platform);
|
||||
}
|
||||
|
@ -118,15 +118,11 @@ const installerBin = await ensureBinary();
|
|||
|
||||
console.log("Now running Installer...");
|
||||
|
||||
try {
|
||||
execFileSync(installerBin, {
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
VENCORD_USER_DATA_DIR: BASE_DIR,
|
||||
VENCORD_DEV_INSTALL: "1"
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
console.error("Something went wrong. Please check the logs above.");
|
||||
}
|
||||
execFileSync(installerBin, {
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
VENCORD_USER_DATA_DIR: BASE_DIR,
|
||||
VENCORD_DEV_INSTALL: "1"
|
||||
}
|
||||
});
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
*/
|
||||
|
||||
export * as Api from "./api";
|
||||
export * as Components from "./components";
|
||||
export * as Plugins from "./plugins";
|
||||
export * as Util from "./utils";
|
||||
export * as QuickCss from "./utils/quickCss";
|
||||
|
@ -28,9 +27,6 @@ export { PlainSettings, Settings };
|
|||
import "./utils/quickCss";
|
||||
import "./webpack/patchWebpack";
|
||||
|
||||
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
||||
import { StartAt } from "@utils/types";
|
||||
|
||||
import { get as dsGet } from "./api/DataStore";
|
||||
import { showNotification } from "./api/Notifications";
|
||||
import { PlainSettings, Settings } from "./api/Settings";
|
||||
|
@ -42,15 +38,11 @@ import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
|
|||
import { onceReady } from "./webpack";
|
||||
import { SettingsRouter } from "./webpack/common";
|
||||
|
||||
if (IS_REPORTER) {
|
||||
require("./debug/runReporter");
|
||||
}
|
||||
|
||||
async function syncSettings() {
|
||||
// pre-check for local shared settings
|
||||
if (
|
||||
Settings.cloud.authenticated &&
|
||||
!await dsGet("Vencord_cloudSecret") // this has been enabled due to local settings share or some other bug
|
||||
await dsGet("Vencord_cloudSecret") === null // this has been enabled due to local settings share or some other bug
|
||||
) {
|
||||
// show a notification letting them know and tell them how to fix it
|
||||
showNotification({
|
||||
|
@ -87,11 +79,11 @@ async function syncSettings() {
|
|||
|
||||
async function init() {
|
||||
await onceReady;
|
||||
startAllPlugins(StartAt.WebpackReady);
|
||||
startAllPlugins();
|
||||
|
||||
syncSettings();
|
||||
|
||||
if (!IS_WEB && !IS_UPDATER_DISABLED) {
|
||||
if (!IS_WEB) {
|
||||
try {
|
||||
const isOutdated = await checkForUpdates();
|
||||
if (!isOutdated) return;
|
||||
|
@ -109,13 +101,16 @@ async function init() {
|
|||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => showNotification({
|
||||
title: "A Vencord update is available!",
|
||||
body: "Click here to view the update",
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick: openUpdaterModal!
|
||||
}), 10_000);
|
||||
if (Settings.notifyAboutUpdates)
|
||||
setTimeout(() => showNotification({
|
||||
title: "A Vencord update is available!",
|
||||
body: "Click here to view the update",
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick() {
|
||||
SettingsRouter.open("VencordUpdater");
|
||||
}
|
||||
}), 10_000);
|
||||
} catch (err) {
|
||||
UpdateLogger.error("Failed to check for updates", err);
|
||||
}
|
||||
|
@ -135,16 +130,13 @@ async function init() {
|
|||
}
|
||||
}
|
||||
|
||||
startAllPlugins(StartAt.Init);
|
||||
init();
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
startAllPlugins(StartAt.DOMContentLoaded);
|
||||
|
||||
if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
|
||||
if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.head.append(Object.assign(document.createElement("style"), {
|
||||
id: "vencord-native-titlebar-style",
|
||||
textContent: "[class*=titleBar]{display: none!important}"
|
||||
}));
|
||||
}
|
||||
}, { once: true });
|
||||
}, { once: true });
|
||||
}
|
||||
|
|
|
@ -4,12 +4,11 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { PluginIpcMappings } from "@main/ipcPlugins";
|
||||
import type { UserThemeHeader } from "@main/themes";
|
||||
import { IpcEvents } from "@shared/IpcEvents";
|
||||
import { IpcEvents } from "@utils/IpcEvents";
|
||||
import { IpcRes } from "@utils/types";
|
||||
import type { Settings } from "api/Settings";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { PluginIpcMappings } from "main/ipcPlugins";
|
||||
import type { UserThemeHeader } from "main/themes";
|
||||
|
||||
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
|
||||
return ipcRenderer.invoke(event, ...args) as Promise<T>;
|
||||
|
@ -47,8 +46,8 @@ export default {
|
|||
},
|
||||
|
||||
settings: {
|
||||
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
|
||||
set: (settings: Settings, pathToNotify?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, pathToNotify),
|
||||
get: () => sendSync<string>(IpcEvents.GET_SETTINGS),
|
||||
set: (settings: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings),
|
||||
getSettingsDir: () => invoke<string>(IpcEvents.GET_SETTINGS_DIR),
|
||||
},
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { User } from "discord-types/general";
|
||||
import { ComponentType, HTMLProps } from "react";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
|
@ -35,7 +36,7 @@ export interface ProfileBadge {
|
|||
image?: string;
|
||||
link?: string;
|
||||
/** Action to perform when you click the badge */
|
||||
onClick?(event: React.MouseEvent<HTMLButtonElement, MouseEvent>, props: BadgeUserArgs): void;
|
||||
onClick?(): void;
|
||||
/** Should the user display this badge? */
|
||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
||||
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
||||
|
@ -44,11 +45,6 @@ export interface ProfileBadge {
|
|||
position?: BadgePosition;
|
||||
/** The badge name to display, Discord uses this. Required for component badges */
|
||||
key?: string;
|
||||
|
||||
/**
|
||||
* Allows dynamically returning multiple badges
|
||||
*/
|
||||
getBadges?(userInfo: BadgeUserArgs): ProfileBadge[];
|
||||
}
|
||||
|
||||
const Badges = new Set<ProfileBadge>();
|
||||
|
@ -78,27 +74,22 @@ export function _getBadges(args: BadgeUserArgs) {
|
|||
const badges = [] as ProfileBadge[];
|
||||
for (const badge of Badges) {
|
||||
if (!badge.shouldShow || badge.shouldShow(args)) {
|
||||
const b = badge.getBadges
|
||||
? badge.getBadges(args).map(b => {
|
||||
b.component &&= ErrorBoundary.wrap(b.component, { noop: true });
|
||||
return b;
|
||||
})
|
||||
: [{ ...badge, ...args }];
|
||||
|
||||
badge.position === BadgePosition.START
|
||||
? badges.unshift(...b)
|
||||
: badges.push(...b);
|
||||
? badges.unshift({ ...badge, ...args })
|
||||
: badges.push({ ...badge, ...args });
|
||||
}
|
||||
}
|
||||
const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/_api/badges").default).getDonorBadges(args.userId);
|
||||
const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/_api/badges").default).getDonorBadges(args.user.id);
|
||||
if (donorBadges) badges.unshift(...donorBadges);
|
||||
|
||||
return badges;
|
||||
}
|
||||
|
||||
export interface BadgeUserArgs {
|
||||
userId: string;
|
||||
guildId: string;
|
||||
user: User;
|
||||
profile: Profile;
|
||||
premiumSince: Date;
|
||||
premiumGuildSince?: Date;
|
||||
}
|
||||
|
||||
interface ConnectedAccount {
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
.vc-chatbar-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./ChatButton.css";
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { waitFor } from "@webpack";
|
||||
import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
import { HTMLProps, MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>;
|
||||
waitFor(["buttonContainer", "channelTextArea"], m => ChannelTextAreaClasses = m);
|
||||
|
||||
export interface ChatBarProps {
|
||||
channel: Channel;
|
||||
disabled: boolean;
|
||||
isEmpty: boolean;
|
||||
type: {
|
||||
analyticsName: string;
|
||||
attachments: boolean;
|
||||
autocomplete: {
|
||||
addReactionShortcut: boolean,
|
||||
forceChatLayer: boolean,
|
||||
reactions: boolean;
|
||||
},
|
||||
commands: {
|
||||
enabled: boolean;
|
||||
},
|
||||
drafts: {
|
||||
type: number,
|
||||
commandType: number,
|
||||
autoSave: boolean;
|
||||
},
|
||||
emojis: {
|
||||
button: boolean;
|
||||
},
|
||||
gifs: {
|
||||
button: boolean,
|
||||
allowSending: boolean;
|
||||
},
|
||||
gifts: {
|
||||
button: boolean;
|
||||
},
|
||||
permissions: {
|
||||
requireSendMessages: boolean;
|
||||
},
|
||||
showThreadPromptOnReply: boolean,
|
||||
stickers: {
|
||||
button: boolean,
|
||||
allowSending: boolean,
|
||||
autoSuggest: boolean;
|
||||
},
|
||||
users: {
|
||||
allowMentioning: boolean;
|
||||
},
|
||||
submit: {
|
||||
button: boolean,
|
||||
ignorePreference: boolean,
|
||||
disableEnterToSubmit: boolean,
|
||||
clearOnSubmit: boolean,
|
||||
useDisabledStylesOnSubmit: boolean;
|
||||
},
|
||||
uploadLongMessages: boolean,
|
||||
upsellLongMessages: {
|
||||
iconOnly: boolean;
|
||||
},
|
||||
showCharacterCount: boolean,
|
||||
sedReplace: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type ChatBarButton = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null;
|
||||
|
||||
const buttonFactories = new Map<string, ChatBarButton>();
|
||||
const logger = new Logger("ChatButtons");
|
||||
|
||||
export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) {
|
||||
if (props.disabled) return;
|
||||
|
||||
for (const [key, Button] of buttonFactories) {
|
||||
buttons.push(
|
||||
<ErrorBoundary noop key={key} onError={e => logger.error(`Failed to render ${key}`, e.error)}>
|
||||
<Button {...props} isMainChat={props.type.analyticsName === "normal"} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const addChatBarButton = (id: string, button: ChatBarButton) => buttonFactories.set(id, button);
|
||||
export const removeChatBarButton = (id: string) => buttonFactories.delete(id);
|
||||
|
||||
export interface ChatBarButtonProps {
|
||||
children: ReactNode;
|
||||
tooltip: string;
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
|
||||
onAuxClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
buttonProps?: Omit<HTMLProps<HTMLButtonElement>, "size" | "onClick" | "onContextMenu" | "onAuxClick">;
|
||||
}
|
||||
export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
|
||||
return (
|
||||
<Tooltip text={props.tooltip}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<div className={`expression-picker-chat-input-button ${ChannelTextAreaClasses?.buttonContainer ?? ""} vc-chatbar-button`}>
|
||||
<Button
|
||||
aria-label={props.tooltip}
|
||||
size=""
|
||||
look={ButtonLooks.BLANK}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
innerClassName={`${ButtonWrapperClasses.button} ${ChannelTextAreaClasses?.button}`}
|
||||
onClick={props.onClick}
|
||||
onContextMenu={props.onContextMenu}
|
||||
onAuxClick={props.onAuxClick}
|
||||
{...props.buttonProps}
|
||||
>
|
||||
<div className={ButtonWrapperClasses.buttonWrapper}>
|
||||
{props.children}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}, { noop: true });
|
|
@ -16,15 +16,16 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { MessageActions, SnowflakeUtils } from "@webpack/common";
|
||||
import { mergeDefaults } from "@utils/misc";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { SnowflakeUtils } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
import type { PartialDeep } from "type-fest";
|
||||
|
||||
import { Argument } from "./types";
|
||||
|
||||
const createBotMessage = findByCodeLazy('username:"Clyde"');
|
||||
const MessageCreator = findByPropsLazy("createBotMessage");
|
||||
const MessageSender = findByPropsLazy("receiveMessage");
|
||||
|
||||
export function generateId() {
|
||||
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
|
||||
|
@ -37,9 +38,9 @@ export function generateId() {
|
|||
* @returns {Message}
|
||||
*/
|
||||
export function sendBotMessage(channelId: string, message: PartialDeep<Message>): Message {
|
||||
const botMessage = createBotMessage({ channelId, content: "", embeds: [] });
|
||||
const botMessage = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] });
|
||||
|
||||
MessageActions.receiveMessage(channelId, mergeDefaults(message, botMessage));
|
||||
MessageSender.receiveMessage(channelId, mergeDefaults(message, botMessage));
|
||||
|
||||
return message as Message;
|
||||
}
|
||||
|
@ -54,5 +55,5 @@ export function sendBotMessage(channelId: string, message: PartialDeep<Message>)
|
|||
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;
|
||||
return (args.find(a => a.name === name)?.value || fallbackValue) as any;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { makeCodeblock } from "@utils/text";
|
||||
|
||||
import { sendBotMessage } from "./commandHelpers";
|
||||
|
@ -47,10 +46,10 @@ export let RequiredMessageOption: Option = ReqPlaceholder;
|
|||
export const _init = function (cmds: Command[]) {
|
||||
try {
|
||||
BUILT_IN = cmds;
|
||||
OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
|
||||
RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "me")!.options![0];
|
||||
OptionalMessageOption = cmds.find(c => c.name === "shrug")!.options![0];
|
||||
RequiredMessageOption = cmds.find(c => c.name === "me")!.options![0];
|
||||
} catch (e) {
|
||||
new Logger("CommandsAPI").error("Failed to load CommandsApi", e, " - cmds is", cmds);
|
||||
console.error("Failed to load CommandsApi");
|
||||
}
|
||||
return cmds;
|
||||
} as never;
|
||||
|
@ -110,7 +109,6 @@ function registerSubCommands(cmd: Command, plugin: string) {
|
|||
const subCmd = {
|
||||
...cmd,
|
||||
...o,
|
||||
options: o.options !== undefined ? o.options : undefined,
|
||||
type: ApplicationCommandType.CHAT_INPUT,
|
||||
name: `${cmd.name} ${o.name}`,
|
||||
id: `${o.name}-${cmd.id}`,
|
||||
|
@ -140,8 +138,6 @@ export function registerCommand<C extends Command>(command: C, plugin: string) {
|
|||
throw new Error(`Command '${command.name}' already exists.`);
|
||||
|
||||
command.isVencordCommand = true;
|
||||
command.untranslatedName ??= command.name;
|
||||
command.untranslatedDescription ??= command.description;
|
||||
command.id ??= `-${BUILT_IN.length + 1}`;
|
||||
command.applicationId ??= "-1"; // BUILT_IN;
|
||||
command.type ??= ApplicationCommandType.CHAT_INPUT;
|
||||
|
|
|
@ -93,10 +93,8 @@ export interface Command {
|
|||
isVencordCommand?: boolean;
|
||||
|
||||
name: string;
|
||||
untranslatedName?: string;
|
||||
displayName?: string;
|
||||
description: string;
|
||||
untranslatedDescription?: string;
|
||||
displayDescription?: string;
|
||||
|
||||
options?: Option[];
|
||||
|
|
|
@ -17,20 +17,22 @@
|
|||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { Menu, React } from "@webpack/common";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
type ContextMenuPatchCallbackReturn = (() => void) | void;
|
||||
/**
|
||||
* @param children The rendered context menu elements
|
||||
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
||||
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
|
||||
*/
|
||||
export type NavContextMenuPatchCallback = (children: Array<ReactElement | null>, ...args: Array<any>) => void;
|
||||
export type NavContextMenuPatchCallback = (children: Array<ReactElement | null>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
|
||||
/**
|
||||
* @param navId The navId of the context menu being patched
|
||||
* @param children The rendered context menu elements
|
||||
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
||||
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
|
||||
*/
|
||||
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement | null>, ...args: Array<any>) => void;
|
||||
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement | null>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
|
||||
|
||||
const ContextMenuLogger = new Logger("ContextMenu");
|
||||
|
||||
|
@ -90,21 +92,15 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba
|
|||
* A helper function for finding the children array of a group nested inside a context menu based on the id(s) of its children
|
||||
* @param id The id of the child. If an array is specified, all ids will be tried
|
||||
* @param children The context menu children
|
||||
* @param matchSubstring Whether to check if the id is a substring of the child id
|
||||
*/
|
||||
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null | undefined>, matchSubstring = false): Array<ReactElement | null | undefined> | null {
|
||||
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null>, _itemsArray?: Array<ReactElement | null>): Array<ReactElement | null> | null {
|
||||
for (const child of children) {
|
||||
if (child == null) continue;
|
||||
|
||||
if (Array.isArray(child)) {
|
||||
const found = findGroupChildrenByChildId(id, child, matchSubstring);
|
||||
if (found !== null) return found;
|
||||
}
|
||||
|
||||
if (
|
||||
(Array.isArray(id) && id.some(id => matchSubstring ? child.props?.id?.includes(id) : child.props?.id === id))
|
||||
|| (matchSubstring ? child.props?.id?.includes(id) : child.props?.id === id)
|
||||
) return children;
|
||||
(Array.isArray(id) && id.some(id => child.props?.id === id))
|
||||
|| child.props?.id === id
|
||||
) return _itemsArray ?? null;
|
||||
|
||||
let nextChildren = child.props?.children;
|
||||
if (nextChildren) {
|
||||
|
@ -113,7 +109,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
|
|||
child.props.children = nextChildren;
|
||||
}
|
||||
|
||||
const found = findGroupChildrenByChildId(id, nextChildren, matchSubstring);
|
||||
const found = findGroupChildrenByChildId(id, nextChildren, nextChildren);
|
||||
if (found !== null) return found;
|
||||
}
|
||||
}
|
||||
|
@ -130,12 +126,9 @@ interface ContextMenuProps {
|
|||
onClose: (callback: (...args: Array<any>) => any) => void;
|
||||
}
|
||||
|
||||
export function _usePatchContextMenu(props: ContextMenuProps) {
|
||||
props = {
|
||||
...props,
|
||||
children: cloneMenuChildren(props.children),
|
||||
};
|
||||
const patchedMenus = new WeakSet();
|
||||
|
||||
export function _patchContextMenu(props: ContextMenuProps) {
|
||||
props.contextMenuApiArguments ??= [];
|
||||
const contextMenuPatches = navPatches.get(props.navId);
|
||||
|
||||
|
@ -144,7 +137,8 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
|
|||
if (contextMenuPatches) {
|
||||
for (const patch of contextMenuPatches) {
|
||||
try {
|
||||
patch(props.children, ...props.contextMenuApiArguments);
|
||||
const callback = patch(props.children, ...props.contextMenuApiArguments);
|
||||
if (!patchedMenus.has(props)) callback?.();
|
||||
} catch (err) {
|
||||
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
|
||||
}
|
||||
|
@ -153,30 +147,12 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
|
|||
|
||||
for (const patch of globalPatches) {
|
||||
try {
|
||||
patch(props.navId, props.children, ...props.contextMenuApiArguments);
|
||||
const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments);
|
||||
if (!patchedMenus.has(props)) callback?.();
|
||||
} catch (err) {
|
||||
ContextMenuLogger.error("Global patch errored,", err);
|
||||
}
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
function cloneMenuChildren(obj: ReactElement | Array<ReactElement | null> | null) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(cloneMenuChildren);
|
||||
}
|
||||
|
||||
if (React.isValidElement(obj)) {
|
||||
obj = React.cloneElement(obj);
|
||||
|
||||
if (
|
||||
obj?.props?.children &&
|
||||
(obj.type !== Menu.MenuControlItem || obj.type === Menu.MenuControlItem && obj.props.control != null)
|
||||
) {
|
||||
obj.props.children = cloneMenuChildren(obj.props.children);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
patchedMenus.add(props);
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ let defaultGetStoreFunc: UseStore | undefined;
|
|||
|
||||
function defaultGetStore() {
|
||||
if (!defaultGetStoreFunc) {
|
||||
defaultGetStoreFunc = createStore(!IS_REPORTER ? "VencordData" : "VencordDataReporter", "VencordStore");
|
||||
defaultGetStoreFunc = createStore("VencordData", "VencordStore");
|
||||
}
|
||||
return defaultGetStoreFunc;
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ export interface MessageExtra {
|
|||
}
|
||||
|
||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
|
||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
|
||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void>;
|
||||
|
||||
const sendListeners = new Set<SendListener>();
|
||||
const editListeners = new Set<EditListener>();
|
||||
|
@ -84,7 +84,7 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec
|
|||
for (const listener of sendListeners) {
|
||||
try {
|
||||
const result = await listener(channelId, messageObj, extra);
|
||||
if (result?.cancel) {
|
||||
if (result && result.cancel === true) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -97,15 +97,11 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec
|
|||
export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
|
||||
for (const listener of editListeners) {
|
||||
try {
|
||||
const result = await listener(channelId, messageId, messageObj);
|
||||
if (result?.cancel) {
|
||||
return true;
|
||||
}
|
||||
await listener(channelId, messageId, messageObj);
|
||||
} catch (e) {
|
||||
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,17 +16,16 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { Channel, Message } from "discord-types/general";
|
||||
import type { ComponentType, MouseEventHandler } from "react";
|
||||
import type { MouseEventHandler } from "react";
|
||||
|
||||
const logger = new Logger("MessagePopover");
|
||||
|
||||
export interface ButtonItem {
|
||||
key?: string,
|
||||
label: string,
|
||||
icon: ComponentType<any>,
|
||||
icon: React.ComponentType<any>,
|
||||
message: Message,
|
||||
channel: Channel,
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>,
|
||||
|
@ -49,26 +48,22 @@ export function removeButton(identifier: string) {
|
|||
}
|
||||
|
||||
export function _buildPopoverElements(
|
||||
Component: React.ComponentType<ButtonItem>,
|
||||
message: Message
|
||||
msg: Message,
|
||||
makeButton: (item: ButtonItem) => React.ComponentType
|
||||
) {
|
||||
const items: React.ReactNode[] = [];
|
||||
const items = [] as React.ComponentType[];
|
||||
|
||||
for (const [identifier, getItem] of buttons.entries()) {
|
||||
try {
|
||||
const item = getItem(message);
|
||||
const item = getItem(msg);
|
||||
if (item) {
|
||||
item.key ??= identifier;
|
||||
items.push(
|
||||
<ErrorBoundary noop>
|
||||
<Component {...item} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
items.push(makeButton(item));
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`[${identifier}]`, err);
|
||||
}
|
||||
}
|
||||
|
||||
return <>{items}</>;
|
||||
return items;
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { MessageCache, MessageStore } from "@webpack/common";
|
||||
import { FluxStore } from "@webpack/types";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
/**
|
||||
* Update and re-render a message
|
||||
* @param channelId The channel id of the message
|
||||
* @param messageId The message id
|
||||
* @param fields The fields of the message to change. Leave empty if you just want to re-render
|
||||
*/
|
||||
export function updateMessage(channelId: string, messageId: string, fields?: Partial<Message & Record<string, any>>) {
|
||||
const channelMessageCache = MessageCache.getOrCreate(channelId);
|
||||
if (!channelMessageCache.has(messageId)) return;
|
||||
|
||||
// To cause a message to re-render, we basically need to create a new instance of the message and obtain a new reference
|
||||
// If we have fields to modify we can use the merge method of the class, otherwise we just create a new instance with the old fields
|
||||
const newChannelMessageCache = channelMessageCache.update(messageId, (oldMessage: any) => {
|
||||
return fields ? oldMessage.merge(fields) : new oldMessage.constructor(oldMessage);
|
||||
});
|
||||
|
||||
MessageCache.commit(newChannelMessageCache);
|
||||
(MessageStore as unknown as FluxStore).emitChange();
|
||||
}
|
|
@ -113,7 +113,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
|||
{timeout !== 0 && !permanent && (
|
||||
<div
|
||||
className="vc-notification-progressbar"
|
||||
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-500)" }}
|
||||
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
|
|
@ -100,7 +100,6 @@ export async function showNotification(data: NotificationData) {
|
|||
const n = new Notification(title, {
|
||||
body,
|
||||
icon,
|
||||
// @ts-expect-error ts is drunk
|
||||
image
|
||||
});
|
||||
n.onclick = onClick;
|
||||
|
|
|
@ -19,11 +19,9 @@
|
|||
import * as DataStore from "@api/DataStore";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { openNotificationSettingsModal } from "@components/VencordSettings/NotificationSettings";
|
||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
||||
import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { DispatchWithoutAction } from "react";
|
||||
|
||||
|
@ -131,7 +129,7 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
|||
richBody={
|
||||
<div className={cl("body")}>
|
||||
{data.body}
|
||||
<Timestamp timestamp={new Date(data.timestamp)} className={cl("timestamp")} />
|
||||
<Timestamp timestamp={moment(data.timestamp)} className={cl("timestamp")} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
@ -172,31 +170,24 @@ function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void
|
|||
</ModalContent>
|
||||
|
||||
<ModalFooter>
|
||||
<Flex>
|
||||
<Button onClick={openNotificationSettingsModal}>
|
||||
Notification Settings
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={log.length === 0}
|
||||
color={Button.Colors.RED}
|
||||
onClick={() => {
|
||||
Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
|
||||
async onConfirm() {
|
||||
await DataStore.set(KEY, []);
|
||||
signals.forEach(x => x());
|
||||
},
|
||||
confirmText: "Do it!",
|
||||
confirmColor: "vc-notification-log-danger-btn",
|
||||
cancelText: "Nevermind"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear Notification Log
|
||||
</Button>
|
||||
</Flex>
|
||||
<Button
|
||||
disabled={log.length === 0}
|
||||
onClick={() => {
|
||||
Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
|
||||
async onConfirm() {
|
||||
await DataStore.set(KEY, []);
|
||||
signals.forEach(x => x());
|
||||
},
|
||||
confirmText: "Do it!",
|
||||
confirmColor: "vc-notification-log-danger-btn",
|
||||
cancelText: "Nevermind"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear Notification Log
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
|
|
|
@ -16,11 +16,10 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { localStorage } from "@utils/localStorage";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||
import { mergeDefaults } from "@utils/misc";
|
||||
import { putCloudSettings } from "@utils/settingsSync";
|
||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||
import { React } from "@webpack/common";
|
||||
|
@ -29,6 +28,7 @@ import plugins from "~plugins";
|
|||
|
||||
const logger = new Logger("Settings");
|
||||
export interface Settings {
|
||||
notifyAboutUpdates: boolean;
|
||||
autoUpdate: boolean;
|
||||
autoUpdateNotification: boolean,
|
||||
useQuickCss: boolean;
|
||||
|
@ -38,20 +38,7 @@ export interface Settings {
|
|||
frameless: boolean;
|
||||
transparent: boolean;
|
||||
winCtrlQ: boolean;
|
||||
macosVibrancyStyle:
|
||||
| "content"
|
||||
| "fullscreen-ui"
|
||||
| "header"
|
||||
| "hud"
|
||||
| "menu"
|
||||
| "popover"
|
||||
| "selection"
|
||||
| "sidebar"
|
||||
| "titlebar"
|
||||
| "tooltip"
|
||||
| "under-page"
|
||||
| "window"
|
||||
| undefined;
|
||||
macosTranslucency: boolean;
|
||||
disableMinSize: boolean;
|
||||
winNativeTitleBar: boolean;
|
||||
plugins: {
|
||||
|
@ -77,7 +64,8 @@ export interface Settings {
|
|||
}
|
||||
|
||||
const DefaultSettings: Settings = {
|
||||
autoUpdate: true,
|
||||
notifyAboutUpdates: true,
|
||||
autoUpdate: false,
|
||||
autoUpdateNotification: true,
|
||||
useQuickCss: true,
|
||||
themeLinks: [],
|
||||
|
@ -86,7 +74,7 @@ const DefaultSettings: Settings = {
|
|||
frameless: false,
|
||||
transparent: false,
|
||||
winCtrlQ: false,
|
||||
macosVibrancyStyle: undefined,
|
||||
macosTranslucency: false,
|
||||
disableMinSize: false,
|
||||
winNativeTitleBar: false,
|
||||
plugins: {},
|
||||
|
@ -106,8 +94,13 @@ const DefaultSettings: Settings = {
|
|||
}
|
||||
};
|
||||
|
||||
const settings = !IS_REPORTER ? VencordNative.settings.get() : {} as Settings;
|
||||
mergeDefaults(settings, DefaultSettings);
|
||||
try {
|
||||
var settings = JSON.parse(VencordNative.settings.get()) as Settings;
|
||||
mergeDefaults(settings, DefaultSettings);
|
||||
} catch (err) {
|
||||
var settings = mergeDefaults({} as Settings, DefaultSettings);
|
||||
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
|
||||
}
|
||||
|
||||
const saveSettingsOnFrequentAction = debounce(async () => {
|
||||
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
|
||||
|
@ -116,52 +109,74 @@ const saveSettingsOnFrequentAction = debounce(async () => {
|
|||
}
|
||||
}, 60_000);
|
||||
|
||||
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _paths?: Array<string>; };
|
||||
const subscriptions = new Set<SubscriptionCallback>();
|
||||
|
||||
export const SettingsStore = new SettingsStoreClass(settings, {
|
||||
readOnly: true,
|
||||
getDefaultValue({
|
||||
target,
|
||||
key,
|
||||
path
|
||||
}) {
|
||||
const v = target[key];
|
||||
if (!plugins) return v; // plugins not initialised yet. this means this path was reached by being called on the top level
|
||||
const proxyCache = {} as Record<string, any>;
|
||||
|
||||
if (path === "plugins" && key in plugins)
|
||||
return target[key] = {
|
||||
enabled: IS_REPORTER || plugins[key].required || plugins[key].enabledByDefault || false
|
||||
};
|
||||
// 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) {
|
||||
const v = target[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?.[key];
|
||||
if (!setting) return 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 ?? plugins[p].enabledByDefault ?? false
|
||||
}, root, `plugins.${p}`);
|
||||
|
||||
if ("default" in setting)
|
||||
// normal setting with a default value
|
||||
return (target[key] = setting.default);
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (setting.type === OptionType.SELECT) {
|
||||
const def = setting.options.find(o => o.default);
|
||||
if (def)
|
||||
target[key] = def.value;
|
||||
return def?.value;
|
||||
// Recursively proxy Objects with the updated property path
|
||||
if (typeof v === "object" && !Array.isArray(v) && v !== null)
|
||||
return makeProxy(v, root, `${path}${path && "."}${p}`);
|
||||
|
||||
// primitive or similar, no need to proxy further
|
||||
return v;
|
||||
},
|
||||
|
||||
set(target, p: string, v) {
|
||||
// avoid unnecessary updates to React Components and other listeners
|
||||
if (target[p] === v) return true;
|
||||
|
||||
target[p] = v;
|
||||
// Call any listeners that are listening to a setting of this path
|
||||
const setPath = `${path}${path && "."}${p}`;
|
||||
delete proxyCache[setPath];
|
||||
for (const subscription of subscriptions) {
|
||||
if (!subscription._paths || subscription._paths.includes(setPath)) {
|
||||
subscription(v, setPath);
|
||||
}
|
||||
}
|
||||
// And don't forget to persist the settings!
|
||||
PlainSettings.cloud.settingsSyncVersion = Date.now();
|
||||
localStorage.Vencord_settingsDirty = true;
|
||||
saveSettingsOnFrequentAction();
|
||||
VencordNative.settings.set(JSON.stringify(root, null, 4));
|
||||
return true;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
});
|
||||
|
||||
if (!IS_REPORTER) {
|
||||
SettingsStore.addGlobalChangeListener((_, path) => {
|
||||
SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
|
||||
localStorage.Vencord_settingsDirty = true;
|
||||
saveSettingsOnFrequentAction();
|
||||
VencordNative.settings.set(SettingsStore.plain, path);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -179,7 +194,7 @@ export const PlainSettings = settings;
|
|||
* the updated settings to disk.
|
||||
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}
|
||||
*/
|
||||
export const Settings = SettingsStore.store;
|
||||
export const Settings = makeProxy(settings);
|
||||
|
||||
/**
|
||||
* Settings hook for React components. Returns a smart settings
|
||||
|
@ -192,21 +207,43 @@ export const Settings = SettingsStore.store;
|
|||
export function useSettings(paths?: UseSettings<Settings>[]) {
|
||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||
|
||||
const onUpdate: SubscriptionCallback = paths
|
||||
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
|
||||
: forceUpdate;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (paths) {
|
||||
paths.forEach(p => SettingsStore.addChangeListener(p, forceUpdate));
|
||||
return () => paths.forEach(p => SettingsStore.removeChangeListener(p, forceUpdate));
|
||||
} else {
|
||||
SettingsStore.addGlobalChangeListener(forceUpdate);
|
||||
return () => SettingsStore.removeGlobalChangeListener(forceUpdate);
|
||||
}
|
||||
subscriptions.add(onUpdate);
|
||||
return () => void subscriptions.delete(onUpdate);
|
||||
}, []);
|
||||
|
||||
return SettingsStore.store;
|
||||
return Settings;
|
||||
}
|
||||
|
||||
// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
|
||||
type ResolvePropDeep<T, P> = P extends "" ? T :
|
||||
P extends `${infer Pre}.${infer Suf}` ?
|
||||
Pre extends keyof T ? ResolvePropDeep<T[Pre], Suf> : never : P extends keyof T ? T[P] : never;
|
||||
|
||||
/**
|
||||
* Add a settings listener that will be invoked whenever the desired setting is updated
|
||||
* @param path Path to the setting that you want to watch, for example "plugins.Unindent.enabled" will fire your callback
|
||||
* whenever Unindent is toggled. Pass an empty string to get notified for all changes
|
||||
* @param onUpdate Callback function whenever a setting matching path is updated. It gets passed the new value and the path
|
||||
* to the updated setting. This path will be the same as your path argument, unless it was an empty string.
|
||||
*
|
||||
* @example addSettingsListener("", (newValue, path) => console.log(`${path} is now ${newValue}`))
|
||||
* addSettingsListener("plugins.Unindent.enabled", v => console.log("Unindent is now", v ? "enabled" : "disabled"))
|
||||
*/
|
||||
export function addSettingsListener<Path extends keyof Settings>(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void;
|
||||
export function addSettingsListener<Path extends string>(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep<Settings, Path>, path: Path extends "" ? string : Path) => void): void;
|
||||
export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) {
|
||||
if (path)
|
||||
((onUpdate as SubscriptionCallback)._paths ??= []).push(path);
|
||||
subscriptions.add(onUpdate);
|
||||
}
|
||||
|
||||
export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
||||
const { plugins } = SettingsStore.plain;
|
||||
const { plugins } = settings;
|
||||
if (name in plugins) return;
|
||||
|
||||
for (const oldName of oldNames) {
|
||||
|
@ -214,7 +251,7 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
|||
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
|
||||
plugins[name] = plugins[oldName];
|
||||
delete plugins[oldName];
|
||||
SettingsStore.markAsChanged();
|
||||
VencordNative.settings.set(JSON.stringify(settings, null, 4));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -230,10 +267,6 @@ export function definePluginSettings<
|
|||
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
|
||||
return Settings.plugins[definedSettings.pluginName] as any;
|
||||
},
|
||||
get plain() {
|
||||
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
|
||||
return PlainSettings.plugins[definedSettings.pluginName] as any;
|
||||
},
|
||||
use: settings => useSettings(
|
||||
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
|
||||
).plugins[definedSettings.pluginName] as any,
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { findModuleId, proxyLazyWebpack, wreq } from "@webpack";
|
||||
|
||||
interface UserSettingDefinition<T> {
|
||||
/**
|
||||
* Get the setting value
|
||||
*/
|
||||
getSetting(): T;
|
||||
/**
|
||||
* Update the setting value
|
||||
* @param value The new value
|
||||
*/
|
||||
updateSetting(value: T): Promise<void>;
|
||||
/**
|
||||
* Update the setting value
|
||||
* @param value A callback that accepts the old value as the first argument, and returns the new value
|
||||
*/
|
||||
updateSetting(value: (old: T) => T): Promise<void>;
|
||||
/**
|
||||
* Stateful React hook for this setting value
|
||||
*/
|
||||
useSetting(): T;
|
||||
userSettingsAPIGroup: string;
|
||||
userSettingsAPIName: string;
|
||||
}
|
||||
|
||||
export const UserSettings: Record<PropertyKey, UserSettingDefinition<any>> | undefined = proxyLazyWebpack(() => {
|
||||
const modId = findModuleId('"textAndImages","renderSpoilers"');
|
||||
if (modId == null) return new Logger("UserSettingsAPI ").error("Didn't find settings module.");
|
||||
|
||||
return wreq(modId as any);
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the setting with the given setting group and name.
|
||||
*
|
||||
* @param group The setting group
|
||||
* @param name The name of the setting
|
||||
*/
|
||||
export function getUserSetting<T = any>(group: string, name: string): UserSettingDefinition<T> | undefined {
|
||||
if (!Vencord.Plugins.isPluginEnabled("UserSettingsAPI")) throw new Error("Cannot use UserSettingsAPI without setting as dependency.");
|
||||
|
||||
for (const key in UserSettings) {
|
||||
const userSetting = UserSettings[key];
|
||||
|
||||
if (userSetting.userSettingsAPIGroup === group && userSetting.userSettingsAPIName === name) {
|
||||
return userSetting;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link getUserSettingDefinition}, lazy.
|
||||
*
|
||||
* Get the setting with the given setting group and name.
|
||||
*
|
||||
* @param group The setting group
|
||||
* @param name The name of the setting
|
||||
*/
|
||||
export function getUserSettingLazy<T = any>(group: string, name: string) {
|
||||
return proxyLazy(() => getUserSetting<T>(group, name));
|
||||
}
|
|
@ -17,7 +17,6 @@
|
|||
*/
|
||||
|
||||
import * as $Badges from "./Badges";
|
||||
import * as $ChatButtons from "./ChatButtons";
|
||||
import * as $Commands from "./Commands";
|
||||
import * as $ContextMenu from "./ContextMenu";
|
||||
import * as $DataStore from "./DataStore";
|
||||
|
@ -26,13 +25,11 @@ import * as $MessageAccessories from "./MessageAccessories";
|
|||
import * as $MessageDecorations from "./MessageDecorations";
|
||||
import * as $MessageEventsAPI from "./MessageEvents";
|
||||
import * as $MessagePopover from "./MessagePopover";
|
||||
import * as $MessageUpdater from "./MessageUpdater";
|
||||
import * as $Notices from "./Notices";
|
||||
import * as $Notifications from "./Notifications";
|
||||
import * as $ServerList from "./ServerList";
|
||||
import * as $Settings from "./Settings";
|
||||
import * as $Styles from "./Styles";
|
||||
import * as $UserSettings from "./UserSettings";
|
||||
|
||||
/**
|
||||
* An API allowing you to listen to Message Clicks or run your own logic
|
||||
|
@ -107,18 +104,3 @@ export const Notifications = $Notifications;
|
|||
* An api allowing you to patch and add/remove items to/from context menus
|
||||
*/
|
||||
export const ContextMenu = $ContextMenu;
|
||||
|
||||
/**
|
||||
* An API allowing you to add buttons to the chat input
|
||||
*/
|
||||
export const ChatButtons = $ChatButtons;
|
||||
|
||||
/**
|
||||
* An API allowing you to update and re-render messages
|
||||
*/
|
||||
export const MessageUpdater = $MessageUpdater;
|
||||
|
||||
/**
|
||||
* An API allowing you to get an user setting
|
||||
*/
|
||||
export const UserSettings = $UserSettings;
|
||||
|
|
12
src/components/ExpandableHeader.css
Normal file
12
src/components/ExpandableHeader.css
Normal file
|
@ -0,0 +1,12 @@
|
|||
.vc-expandableheader-center-flex {
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vc-expandableheader-btn {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
108
src/components/ExpandableHeader.tsx
Normal file
108
src/components/ExpandableHeader.tsx
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Text, Tooltip, useState } from "@webpack/common";
|
||||
export const cl = classNameFactory("vc-expandableheader-");
|
||||
import "./ExpandableHeader.css";
|
||||
|
||||
export interface ExpandableHeaderProps {
|
||||
onMoreClick?: () => void;
|
||||
moreTooltipText?: string;
|
||||
onDropDownClick?: (state: boolean) => void;
|
||||
defaultState?: boolean;
|
||||
headerText: string;
|
||||
children: React.ReactNode;
|
||||
buttons?: React.ReactNode[];
|
||||
}
|
||||
|
||||
export default function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
|
||||
const [showContent, setShowContent] = useState(defaultState);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "8px"
|
||||
}}>
|
||||
<Text
|
||||
tag="h2"
|
||||
variant="eyebrow"
|
||||
style={{
|
||||
color: "var(--header-primary)",
|
||||
display: "inline"
|
||||
}}
|
||||
>
|
||||
{headerText}
|
||||
</Text>
|
||||
|
||||
<div className={cl("center-flex")}>
|
||||
{
|
||||
buttons ?? null
|
||||
}
|
||||
|
||||
{
|
||||
onMoreClick && // only show more button if callback is provided
|
||||
<Tooltip text={moreTooltipText}>
|
||||
{tooltipProps => (
|
||||
<button
|
||||
{...tooltipProps}
|
||||
className={cl("btn")}
|
||||
onClick={onMoreClick}>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill="var(--text-normal)" d="M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
|
||||
<Tooltip text={showContent ? "Hide " + headerText : "Show " + headerText}>
|
||||
{tooltipProps => (
|
||||
<button
|
||||
{...tooltipProps}
|
||||
className={cl("btn")}
|
||||
onClick={() => {
|
||||
setShowContent(v => !v);
|
||||
onDropDownClick?.(showContent);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
transform={showContent ? "scale(1 -1)" : "scale(1 1)"}
|
||||
>
|
||||
<path fill="var(--text-normal)" d="M16.59 8.59003L12 13.17L7.41 8.59003L6 10L12 16L18 10L16.59 8.59003Z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{showContent && children}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
interface Props {
|
||||
columns: number;
|
||||
gap?: string;
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
export function Grid(props: Props & JSX.IntrinsicElements["div"]) {
|
||||
const style: CSSProperties = {
|
||||
display: props.inline ? "inline-grid" : "grid",
|
||||
gridTemplateColumns: `repeat(${props.columns}, 1fr)`,
|
||||
gap: props.gap,
|
||||
...props.style
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...props} style={style}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -18,16 +18,19 @@
|
|||
|
||||
import "./iconStyles.css";
|
||||
|
||||
import { getIntlMessage } from "@utils/discord";
|
||||
import { classes } from "@utils/misc";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { i18n } from "@webpack/common";
|
||||
import type { PropsWithChildren, SVGProps } from "react";
|
||||
|
||||
interface BaseIconProps extends IconProps {
|
||||
viewBox: string;
|
||||
}
|
||||
|
||||
type IconProps = JSX.IntrinsicElements["svg"];
|
||||
type ImageProps = JSX.IntrinsicElements["img"];
|
||||
interface IconProps extends SVGProps<SVGSVGElement> {
|
||||
className?: string;
|
||||
height?: string | number;
|
||||
width?: string | number;
|
||||
}
|
||||
|
||||
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
|
||||
return (
|
||||
|
@ -64,7 +67,8 @@ export function LinkIcon({ height = 24, width = 24, className }: IconProps) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Discord's copy icon, as seen in the user panel popout on the right of the username and in large code blocks
|
||||
* Discord's copy icon, as seen in the user popout right of the username when clicking
|
||||
* your own username in the bottom left user panel
|
||||
*/
|
||||
export function CopyIcon(props: IconProps) {
|
||||
return (
|
||||
|
@ -74,9 +78,8 @@ export function CopyIcon(props: IconProps) {
|
|||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="currentColor">
|
||||
<path d="M3 16a1 1 0 0 1-1-1v-5a8 8 0 0 1 8-8h5a1 1 0 0 1 1 1v.5a.5.5 0 0 1-.5.5H10a6 6 0 0 0-6 6v5.5a.5.5 0 0 1-.5.5H3Z" />
|
||||
<path d="M6 18a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4v-4h-3a5 5 0 0 1-5-5V6h-4a4 4 0 0 0-4 4v8Z" />
|
||||
<path d="M21.73 12a3 3 0 0 0-.6-.88l-4.25-4.24a3 3 0 0 0-.88-.61V9a3 3 0 0 0 3 3h2.73Z" />
|
||||
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1z" />
|
||||
<path d="M15 5H8c-1.1 0-1.99.9-1.99 2L6 21c0 1.1.89 2 1.99 2H19c1.1 0 2-.9 2-2V11l-6-6zM8 21V7h6v5h5v9H8z" />
|
||||
</g>
|
||||
</Icon>
|
||||
);
|
||||
|
@ -122,8 +125,8 @@ export function InfoIcon(props: IconProps) {
|
|||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M23 12a11 11 0 1 1-22 0 11 11 0 0 1 22 0Zm-9.5-4.75a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm-.77 3.96a1 1 0 1 0-1.96-.42l-1.04 4.86a2.77 2.77 0 0 0 4.31 2.83l.24-.17a1 1 0 1 0-1.16-1.62l-.24.17a.77.77 0 0 1-1.2-.79l1.05-4.86Z" clip-rule="evenodd"
|
||||
transform="translate(2 2)"
|
||||
d="M9,7 L11,7 L11,5 L9,5 L9,7 Z M10,18 C5.59,18 2,14.41 2,10 C2,5.59 5.59,2 10,2 C14.41,2 18,5.59 18,10 C18,14.41 14.41,18 10,18 L10,18 Z M10,4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16,4.4771525 0,10 C-1.33226763e-15,12.6521649 1.0535684,15.195704 2.92893219,17.0710678 C4.80429597,18.9464316 7.3478351,20 10,20 C12.6521649,20 15.195704,18.9464316 17.0710678,17.0710678 C18.9464316,15.195704 20,12.6521649 20,10 C20,7.3478351 18.9464316,4.80429597 17.0710678,2.92893219 C15.195704,1.0535684 12.6521649,2.22044605e-16 10,0 L10,4.4408921e-16 Z M9,15 L11,15 L11,9 L9,9 L9,15 L9,15 Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
|
@ -132,7 +135,7 @@ export function InfoIcon(props: IconProps) {
|
|||
export function OwnerCrownIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
aria-label={getIntlMessage("GUILD_OWNER")}
|
||||
aria-label={i18n.Messages.GUILD_OWNER}
|
||||
{...props}
|
||||
className={classes(props.className, "vc-owner-crown-icon")}
|
||||
role="img"
|
||||
|
@ -211,10 +214,9 @@ export function CogWheel(props: IconProps) {
|
|||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M10.56 1.1c-.46.05-.7.53-.64.98.18 1.16-.19 2.2-.98 2.53-.8.33-1.79-.15-2.49-1.1-.27-.36-.78-.52-1.14-.24-.77.59-1.45 1.27-2.04 2.04-.28.36-.12.87.24 1.14.96.7 1.43 1.7 1.1 2.49-.33.8-1.37 1.16-2.53.98-.45-.07-.93.18-.99.64a11.1 11.1 0 0 0 0 2.88c.06.46.54.7.99.64 1.16-.18 2.2.19 2.53.98.33.8-.14 1.79-1.1 2.49-.36.27-.52.78-.24 1.14.59.77 1.27 1.45 2.04 2.04.36.28.87.12 1.14-.24.7-.95 1.7-1.43 2.49-1.1.8.33 1.16 1.37.98 2.53-.07.45.18.93.64.99a11.1 11.1 0 0 0 2.88 0c.46-.06.7-.54.64-.99-.18-1.16.19-2.2.98-2.53.8-.33 1.79.14 2.49 1.1.27.36.78.52 1.14.24.77-.59 1.45-1.27 2.04-2.04.28-.36.12-.87-.24-1.14-.96-.7-1.43-1.7-1.1-2.49.33-.8 1.37-1.16 2.53-.98.45.07.93-.18.99-.64a11.1 11.1 0 0 0 0-2.88c-.06-.46-.54-.7-.99-.64-1.16.18-2.2-.19-2.53-.98-.33-.8.14-1.79 1.1-2.49.36-.27.52-.78.24-1.14a11.07 11.07 0 0 0-2.04-2.04c-.36-.28-.87-.12-1.14.24-.7.96-1.7 1.43-2.49 1.1-.8-.33-1.16-1.37-.98-2.53.07-.45-.18-.93-.64-.99a11.1 11.1 0 0 0-2.88 0ZM16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"
|
||||
clip-rule="evenodd"
|
||||
d="M19.738 10H22V14H19.739C19.498 14.931 19.1 15.798 18.565 16.564L20 18L18 20L16.565 18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069 19.498 8.203 19.099 7.436 18.564L6 20L4 18L5.436 16.564C4.901 15.799 4.502 14.932 4.262 14H2V10H4.262C4.502 9.068 4.9 8.202 5.436 7.436L4 6L6 4L7.436 5.436C8.202 4.9 9.068 4.502 10 4.262V2H14V4.261C14.932 4.502 15.797 4.9 16.565 5.435L18 3.999L20 5.999L18.564 7.436C19.099 8.202 19.498 9.069 19.738 10ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
|
@ -253,184 +255,3 @@ export function DeleteIcon(props: IconProps) {
|
|||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function PlusIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-plus-icon")}
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<polygon
|
||||
fill-rule="nonzero"
|
||||
fill="currentColor"
|
||||
points="15 10 10 10 10 15 8 15 8 10 3 10 3 8 8 8 8 3 10 3 10 8 15 8"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoEntrySignIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-no-entry-sign-icon")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8 0-1.85.63-3.55 1.69-4.9L16.9 18.31C15.55 19.37 13.85 20 12 20zm6.31-3.1L7.1 5.69C8.45 4.63 10.15 4 12 4c4.42 0 8 3.58 8 8 0 1.85-.63 3.55-1.69 4.9z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function SafetyIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-safety-icon")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.27 5.22A2.66 2.66 0 0 0 3 7.5v2.3c0 5.6 3.3 10.68 8.42 12.95.37.17.79.17 1.16 0A14.18 14.18 0 0 0 21 9.78V7.5c0-.93-.48-1.78-1.27-2.27l-6.17-3.76a3 3 0 0 0-3.12 0L4.27 5.22ZM6 7.68l6-3.66V12H6.22C6.08 11.28 6 10.54 6 9.78v-2.1Zm6 12.01V12h5.78A11.19 11.19 0 0 1 12 19.7Z"
|
||||
/>
|
||||
</Icon>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export function NotesIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-notes-icon")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8 3C7.44771 3 7 3.44772 7 4V5C7 5.55228 7.44772 6 8 6H16C16.5523 6 17 5.55228 17 5V4C17 3.44772 16.5523 3 16 3H15.1245C14.7288 3 14.3535 2.82424 14.1002 2.52025L13.3668 1.64018C13.0288 1.23454 12.528 1 12 1C11.472 1 10.9712 1.23454 10.6332 1.64018L9.8998 2.52025C9.64647 2.82424 9.27121 3 8.8755 3H8Z"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
fill="currentColor"
|
||||
d="M19 4.49996V4.99996C19 6.65681 17.6569 7.99996 16 7.99996H8C6.34315 7.99996 5 6.65681 5 4.99996V4.49996C5 4.22382 4.77446 3.99559 4.50209 4.04109C3.08221 4.27826 2 5.51273 2 6.99996V19C2 20.6568 3.34315 22 5 22H19C20.6569 22 22 20.6568 22 19V6.99996C22 5.51273 20.9178 4.27826 19.4979 4.04109C19.2255 3.99559 19 4.22382 19 4.49996ZM8 12C7.44772 12 7 12.4477 7 13C7 13.5522 7.44772 14 8 14H16C16.5523 14 17 13.5522 17 13C17 12.4477 16.5523 12 16 12H8ZM7 17C7 16.4477 7.44772 16 8 16H13C13.5523 16 14 16.4477 14 17C14 17.5522 13.5523 18 13 18H8C7.44772 18 7 17.5522 7 17Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function FolderIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-folder-icon")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M2 5a3 3 0 0 1 3-3h3.93a2 2 0 0 1 1.66.9L12 5h7a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function LogIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-log-icon")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.11 8H6v10.82c0 .86.37 1.68 1 2.27.46.43 1.02.71 1.63.84A1 1 0 0 0 9 22h10a4 4 0 0 0 4-4v-1a2 2 0 0 0-2-2h-1V5a3 3 0 0 0-3-3H4.67c-.87 0-1.7.32-2.34.9-.63.6-1 1.42-1 2.28 0 .71.3 1.35.52 1.75a5.35 5.35 0 0 0 .48.7l.01.01h.01L3.11 7l-.76.65a1 1 0 0 0 .76.35Zm1.56-4c-.38 0-.72.14-.97.37-.24.23-.37.52-.37.81a1.69 1.69 0 0 0 .3.82H6v-.83c0-.29-.13-.58-.37-.8C5.4 4.14 5.04 4 4.67 4Zm5 13a3.58 3.58 0 0 1 0 3H19a2 2 0 0 0 2-2v-1H9.66ZM3.86 6.35ZM11 8a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5Zm-1 5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function RestartIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-restart-icon")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 0 1 14.93-4H15a1 1 0 1 0 0 2h6a1 1 0 0 0 1-1V3a1 1 0 1 0-2 0v3a9.98 9.98 0 0 0-18 6 10 10 0 0 0 16.29 7.78 1 1 0 0 0-1.26-1.56A8 8 0 0 1 4 12Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function PaintbrushIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-paintbrush-icon")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.35 7.24C15.9 6.67 16 5.8 16 5a3 3 0 1 1 3 3c-.8 0-1.67.09-2.24.65a1.5 1.5 0 0 0 0 2.11l1.12 1.12a3 3 0 0 1 0 4.24l-5 5a3 3 0 0 1-4.25 0l-5.76-5.75a3 3 0 0 1 0-4.24l4.04-4.04.97-.97a3 3 0 0 1 4.24 0l1.12 1.12c.58.58 1.52.58 2.1 0ZM6.9 9.9 4.3 12.54a1 1 0 0 0 0 1.42l2.17 2.17.83-.84a1 1 0 0 1 1.42 1.42l-.84.83.59.59 1.83-1.84a1 1 0 0 1 1.42 1.42l-1.84 1.83.17.17a1 1 0 0 0 1.42 0l2.63-2.62L6.9 9.9Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function PencilIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-pencil-icon")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function GithubIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="-3 -3 30 30"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.757-1.333-1.757-1.09-.745.083-.73.083-.73 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.807 1.305 3.492.998.108-.775.42-1.305.763-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.467-2.38 1.235-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.398 3-.403 1.02.005 2.043.137 3 .403 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.24 2.873.118 3.176.77.84 1.233 1.91 1.233 3.22 0 4.61-2.803 5.625-5.475 5.92.43.37.823 1.102.823 2.222v3.293c0 .32.218.694.825.577C20.565 21.797 24 17.298 24 12c0-6.63-5.37-12-12-12z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function WebsiteIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zM4 12c0-.899.156-1.762.431-2.569L6 11l2 2v2l2 2 1 1v1.931C7.061 19.436 4 16.072 4 12zm14.33 4.873C17.677 16.347 16.687 16 16 16v-1a2 2 0 0 0-2-2h-4v-3a2 2 0 0 0 2-2V7h1a2 2 0 0 0 2-2v-.411C17.928 5.778 20 8.65 20 12a7.947 7.947 0 0 1-1.67 4.873z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,18 +9,20 @@ import "./contributorModal.css";
|
|||
import { useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Link } from "@components/Link";
|
||||
import { DevsById } from "@utils/constants";
|
||||
import { fetchUserProfile } from "@utils/discord";
|
||||
import { classes, pluralise } from "@utils/misc";
|
||||
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
|
||||
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
||||
import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||
import { Forms, MaskedLink, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
|
||||
import { PluginCard } from ".";
|
||||
import { GithubButton, WebsiteButton } from "./LinkIconButton";
|
||||
|
||||
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
|
||||
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
|
||||
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
|
||||
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
|
||||
|
||||
const cl = classNameFactory("vc-author-modal-");
|
||||
|
||||
|
@ -36,6 +38,16 @@ export function openContributorModal(user: User) {
|
|||
);
|
||||
}
|
||||
|
||||
function GithubIcon() {
|
||||
const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark;
|
||||
return <img src={src} alt="GitHub" />;
|
||||
}
|
||||
|
||||
function WebsiteIcon() {
|
||||
const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark;
|
||||
return <img src={src} alt="Website" />;
|
||||
}
|
||||
|
||||
function ContributorModal({ user }: { user: User; }) {
|
||||
useSettings();
|
||||
|
||||
|
@ -60,8 +72,6 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
||||
}, [user.id, user.username]);
|
||||
|
||||
const ContributedHyperLink = <Link href="https://vencord.dev/source">contributed</Link>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cl("header")}>
|
||||
|
@ -72,44 +82,32 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
/>
|
||||
<Forms.FormTitle tag="h2" className={cl("name")}>{user.username}</Forms.FormTitle>
|
||||
|
||||
<div className={classes("vc-settings-modal-links", cl("links"))}>
|
||||
<div className={cl("links")}>
|
||||
{website && (
|
||||
<WebsiteButton
|
||||
text={website}
|
||||
href={`https://${website}`}
|
||||
/>
|
||||
<MaskedLink
|
||||
href={"https://" + website}
|
||||
>
|
||||
<WebsiteIcon />
|
||||
</MaskedLink>
|
||||
)}
|
||||
{githubName && (
|
||||
<GithubButton
|
||||
text={githubName}
|
||||
href={`https://github.com/${githubName}`}
|
||||
/>
|
||||
<MaskedLink href={`https://github.com/${githubName}`}>
|
||||
<GithubIcon />
|
||||
</MaskedLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{plugins.length ? (
|
||||
<Forms.FormText>
|
||||
This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
|
||||
</Forms.FormText>
|
||||
) : (
|
||||
<Forms.FormText>
|
||||
This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
|
||||
</Forms.FormText>
|
||||
)}
|
||||
|
||||
{!!plugins.length && (
|
||||
<div className={cl("plugins")}>
|
||||
{plugins.map(p =>
|
||||
<PluginCard
|
||||
key={p.name}
|
||||
plugin={p}
|
||||
disabled={p.required ?? false}
|
||||
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={cl("plugins")}>
|
||||
{plugins.map(p =>
|
||||
<PluginCard
|
||||
key={p.name}
|
||||
plugin={p}
|
||||
disabled={p.required ?? false}
|
||||
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
.vc-settings-modal-link-icon {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid var(--background-tertiary);
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
.vc-settings-modal-links {
|
||||
display: flex;
|
||||
gap: 0.2em;
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./LinkIconButton.css";
|
||||
|
||||
import { getTheme, Theme } from "@utils/discord";
|
||||
import { MaskedLink, Tooltip } from "@webpack/common";
|
||||
|
||||
import { GithubIcon, WebsiteIcon } from "..";
|
||||
|
||||
export function GithubLinkIcon() {
|
||||
const theme = getTheme() === Theme.Light ? "#000000" : "#FFFFFF";
|
||||
return <GithubIcon aria-hidden fill={theme} className={"vc-settings-modal-link-icon"} />;
|
||||
}
|
||||
|
||||
export function WebsiteLinkIcon() {
|
||||
const theme = getTheme() === Theme.Light ? "#000000" : "#FFFFFF";
|
||||
return <WebsiteIcon aria-hidden fill={theme} className={"vc-settings-modal-link-icon"} />;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
function LinkIcon({ text, href, Icon }: Props & { Icon: React.ComponentType; }) {
|
||||
return (
|
||||
<Tooltip text={text}>
|
||||
{props => (
|
||||
<MaskedLink {...props} href={href}>
|
||||
<Icon />
|
||||
</MaskedLink>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export const WebsiteButton = (props: Props) => <LinkIcon {...props} Icon={WebsiteLinkIcon} />;
|
||||
export const GithubButton = (props: Props) => <LinkIcon {...props} Icon={GithubLinkIcon} />;
|
|
@ -1,7 +0,0 @@
|
|||
.vc-plugin-modal-info {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vc-plugin-modal-description {
|
||||
flex-grow: 1;
|
||||
}
|
|
@ -16,26 +16,21 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./PluginModal.css";
|
||||
|
||||
import { generateId } from "@api/Commands";
|
||||
import { useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { gitRemote } from "@shared/vencordUserAgent";
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes, isObjectEmpty } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { OptionType, Plugin } from "@utils/types";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { findByCode, findByPropsLazy } from "@webpack";
|
||||
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
import { Constructor } from "type-fest";
|
||||
|
||||
import { PluginMeta } from "~plugins";
|
||||
|
||||
import {
|
||||
ISettingElementProps,
|
||||
SettingBooleanComponent,
|
||||
|
@ -46,11 +41,8 @@ import {
|
|||
SettingTextComponent
|
||||
} from "./components";
|
||||
import { openContributorModal } from "./ContributorModal";
|
||||
import { GithubButton, WebsiteButton } from "./LinkIconButton";
|
||||
|
||||
const cl = classNameFactory("vc-plugin-modal-");
|
||||
|
||||
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
|
||||
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
||||
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
||||
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
||||
|
||||
|
@ -189,54 +181,16 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
);
|
||||
}
|
||||
|
||||
/*
|
||||
function switchToPopout() {
|
||||
onClose();
|
||||
|
||||
const PopoutKey = `DISCORD_VENCORD_PLUGIN_SETTINGS_MODAL_${plugin.name}`;
|
||||
PopoutActions.open(
|
||||
PopoutKey,
|
||||
() => <PluginModal
|
||||
transitionState={transitionState}
|
||||
plugin={plugin}
|
||||
onRestartNeeded={onRestartNeeded}
|
||||
onClose={() => PopoutActions.close(PopoutKey)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
const pluginMeta = PluginMeta[plugin.name];
|
||||
|
||||
return (
|
||||
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM} className="vc-text-selectable">
|
||||
<ModalHeader separator={false}>
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
|
||||
|
||||
{/*
|
||||
<Button look={Button.Looks.BLANK} onClick={switchToPopout}>
|
||||
<OpenExternalIcon aria-label="Open in Popout" />
|
||||
</Button>
|
||||
*/}
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Forms.FormSection>
|
||||
<Flex className={cl("info")}>
|
||||
<Forms.FormText className={cl("description")}>{plugin.description}</Forms.FormText>
|
||||
{!pluginMeta.userPlugin && (
|
||||
<div className="vc-settings-modal-links">
|
||||
<WebsiteButton
|
||||
text="View more info"
|
||||
href={`https://vencord.dev/plugins/${plugin.name}`}
|
||||
/>
|
||||
<GithubButton
|
||||
text="View source code"
|
||||
href={`https://github.com/${gitRemote}/tree/main/src/plugins/${pluginMeta.folderName}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
|
||||
<Forms.FormText>{plugin.description}</Forms.FormText>
|
||||
<Forms.FormTitle tag="h3" style={{ marginTop: 8, marginBottom: 0 }}>Authors</Forms.FormTitle>
|
||||
<div style={{ width: "fit-content", marginBottom: 8 }}>
|
||||
<UserSummaryItem
|
||||
|
@ -310,13 +264,3 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
export function openPluginModal(plugin: Plugin, onRestartNeeded?: (pluginName: string) => void) {
|
||||
openModal(modalProps => (
|
||||
<PluginModal
|
||||
{...modalProps}
|
||||
plugin={plugin}
|
||||
onRestartNeeded={() => onRestartNeeded?.(plugin.name)}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Margins } from "@utils/margins";
|
||||
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
||||
import { OptionType, PluginOptionNumber } from "@utils/types";
|
||||
import { Forms, React, TextInput } from "@webpack/common";
|
||||
|
||||
|
@ -56,8 +54,7 @@ export function SettingNumericComponent({ option, pluginSettings, definedSetting
|
|||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<TextInput
|
||||
type="number"
|
||||
pattern="-?[0-9]+"
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Margins } from "@utils/margins";
|
||||
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
||||
import { PluginOptionSelect } from "@utils/types";
|
||||
import { Forms, React, Select } from "@webpack/common";
|
||||
|
||||
|
@ -46,8 +44,7 @@ export function SettingSelectComponent({ option, pluginSettings, definedSettings
|
|||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom16} type="description">{option.description}</Forms.FormText>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<Select
|
||||
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
||||
options={option.options}
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Margins } from "@utils/margins";
|
||||
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
||||
import { PluginOptionSlider } from "@utils/types";
|
||||
import { Forms, React, Slider } from "@webpack/common";
|
||||
|
||||
|
@ -52,8 +50,7 @@ export function SettingSliderComponent({ option, pluginSettings, definedSettings
|
|||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<Slider
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
markers={option.markers}
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Margins } from "@utils/margins";
|
||||
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
||||
import { PluginOptionString } from "@utils/types";
|
||||
import { Forms, React, TextInput } from "@webpack/common";
|
||||
|
||||
|
@ -43,8 +41,7 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
|
|||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<TextInput
|
||||
type="text"
|
||||
value={state}
|
||||
|
|
|
@ -25,13 +25,11 @@
|
|||
display: block;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 32px;
|
||||
width: 16px;
|
||||
background: var(--background-tertiary);
|
||||
z-index: -1;
|
||||
left: -32px;
|
||||
left: -16px;
|
||||
top: 0;
|
||||
border-top-left-radius: 9999px;
|
||||
border-bottom-left-radius: 9999px;
|
||||
}
|
||||
|
||||
.vc-author-modal-avatar {
|
||||
|
@ -42,10 +40,19 @@
|
|||
|
||||
.vc-author-modal-links {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 0.2em;
|
||||
}
|
||||
|
||||
.vc-author-modal-links img {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid var(--background-tertiary);
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
.vc-author-modal-plugins {
|
||||
display: grid;
|
||||
gap: 0.5em;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
|
|
@ -23,28 +23,28 @@ import { showNotice } from "@api/Notices";
|
|||
import { Settings, useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { CogWheel, InfoIcon } from "@components/Icons";
|
||||
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||
import PluginModal from "@components/PluginSettings/PluginModal";
|
||||
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
||||
import { SettingsTab } from "@components/VencordSettings/shared";
|
||||
import { ChangeList } from "@utils/ChangeList";
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes, isObjectEmpty } from "@utils/misc";
|
||||
import { openModalLazy } from "@utils/modal";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { Plugin } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip, useMemo } from "@webpack/common";
|
||||
import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
||||
|
||||
import Plugins, { ExcludedPlugins } from "~plugins";
|
||||
import Plugins from "~plugins";
|
||||
|
||||
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
||||
|
||||
// Avoid circular dependency
|
||||
const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() => require("../../plugins"));
|
||||
|
||||
const cl = classNameFactory("vc-plugins-");
|
||||
const logger = new Logger("PluginSettings", "#a6d189");
|
||||
|
||||
const InputStyles = findByPropsLazy("inputWrapper", "inputDefault", "error");
|
||||
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
|
||||
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
|
||||
|
||||
|
||||
|
@ -68,7 +68,7 @@ function ReloadRequiredCard({ required }: { required: boolean; }) {
|
|||
<Forms.FormText className={cl("dep-text")}>
|
||||
Restart now to apply new plugins and their settings
|
||||
</Forms.FormText>
|
||||
<Button onClick={() => location.reload()}>
|
||||
<Button color={Button.Colors.YELLOW} onClick={() => location.reload()}>
|
||||
Restart
|
||||
</Button>
|
||||
</>
|
||||
|
@ -93,7 +93,15 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
|||
export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
||||
const settings = Settings.plugins[plugin.name];
|
||||
|
||||
const isEnabled = () => Vencord.Plugins.isPluginEnabled(plugin.name);
|
||||
const isEnabled = () => settings.enabled ?? false;
|
||||
|
||||
function openModal() {
|
||||
openModalLazy(async () => {
|
||||
return modalProps => {
|
||||
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function toggleEnabled() {
|
||||
const wasEnabled = isEnabled();
|
||||
|
@ -151,11 +159,7 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
|
|||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
infoButton={
|
||||
<button
|
||||
role="switch"
|
||||
onClick={() => openPluginModal(plugin, onRestartNeeded)}
|
||||
className={classes(ButtonClasses.button, cl("info-button"))}
|
||||
>
|
||||
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
|
||||
{plugin.options && !isObjectEmpty(plugin.options)
|
||||
? <CogWheel />
|
||||
: <InfoIcon />}
|
||||
|
@ -172,37 +176,6 @@ const enum SearchStatus {
|
|||
NEW
|
||||
}
|
||||
|
||||
function ExcludedPluginsList({ search }: { search: string; }) {
|
||||
const matchingExcludedPlugins = Object.entries(ExcludedPlugins)
|
||||
.filter(([name]) => name.toLowerCase().includes(search));
|
||||
|
||||
const ExcludedReasons: Record<"web" | "discordDesktop" | "vencordDesktop" | "desktop" | "dev", string> = {
|
||||
desktop: "Discord Desktop app or Vesktop",
|
||||
discordDesktop: "Discord Desktop app",
|
||||
vencordDesktop: "Vesktop app",
|
||||
web: "Vesktop app and the Web version of Discord",
|
||||
dev: "Developer version of Vencord"
|
||||
};
|
||||
|
||||
return (
|
||||
<Text variant="text-md/normal" className={Margins.top16}>
|
||||
{matchingExcludedPlugins.length
|
||||
? <>
|
||||
<Forms.FormText>Are you looking for:</Forms.FormText>
|
||||
<ul>
|
||||
{matchingExcludedPlugins.map(([name, reason]) => (
|
||||
<li key={name}>
|
||||
<b>{name}</b>: Only available on the {ExcludedReasons[reason]}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
: "No plugins meet the search criteria."
|
||||
}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PluginSettings() {
|
||||
const settings = useSettings();
|
||||
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
||||
|
@ -241,27 +214,26 @@ export default function PluginSettings() {
|
|||
return o;
|
||||
}, []);
|
||||
|
||||
const sortedPlugins = useMemo(() => Object.values(Plugins)
|
||||
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
|
||||
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
||||
|
||||
const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL });
|
||||
|
||||
const search = searchValue.value.toLowerCase();
|
||||
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
||||
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
|
||||
|
||||
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
|
||||
const { status } = searchValue;
|
||||
const enabled = Vencord.Plugins.isPluginEnabled(plugin.name);
|
||||
if (enabled && status === SearchStatus.DISABLED) return false;
|
||||
if (!enabled && status === SearchStatus.ENABLED) return false;
|
||||
if (status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false;
|
||||
if (!search.length) return true;
|
||||
const enabled = settings.plugins[plugin.name]?.enabled;
|
||||
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
|
||||
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
|
||||
if (searchValue.status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false;
|
||||
if (!searchValue.value.length) return true;
|
||||
|
||||
const v = searchValue.value.toLowerCase();
|
||||
return (
|
||||
plugin.name.toLowerCase().includes(search) ||
|
||||
plugin.description.toLowerCase().includes(search) ||
|
||||
plugin.tags?.some(t => t.toLowerCase().includes(search))
|
||||
plugin.name.toLowerCase().includes(v) ||
|
||||
plugin.description.toLowerCase().includes(v) ||
|
||||
plugin.tags?.some(t => t.toLowerCase().includes(v))
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -282,48 +254,53 @@ export default function PluginSettings() {
|
|||
return lodash.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
|
||||
}));
|
||||
|
||||
const plugins = [] as JSX.Element[];
|
||||
const requiredPlugins = [] as JSX.Element[];
|
||||
type P = JSX.Element | JSX.Element[];
|
||||
let plugins: P, requiredPlugins: P;
|
||||
if (sortedPlugins?.length) {
|
||||
plugins = [];
|
||||
requiredPlugins = [];
|
||||
|
||||
const showApi = searchValue.value.includes("API");
|
||||
for (const p of sortedPlugins) {
|
||||
if (p.hidden || (!p.options && p.name.endsWith("API") && !showApi))
|
||||
continue;
|
||||
for (const p of sortedPlugins) {
|
||||
if (!p.options && p.name.endsWith("API") && searchValue.value !== "API")
|
||||
continue;
|
||||
|
||||
if (!pluginFilter(p)) continue;
|
||||
if (!pluginFilter(p)) continue;
|
||||
|
||||
const isRequired = p.required || p.isDependency || depMap[p.name]?.some(d => settings.plugins[d].enabled);
|
||||
const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled);
|
||||
|
||||
if (isRequired) {
|
||||
const tooltipText = p.required || !depMap[p.name]
|
||||
? "This plugin is required for Vencord to function."
|
||||
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
|
||||
if (isRequired) {
|
||||
const tooltipText = p.required
|
||||
? "This plugin is required for Vencord to function."
|
||||
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
|
||||
|
||||
requiredPlugins.push(
|
||||
<Tooltip text={tooltipText} key={p.name}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<PluginCard
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onRestartNeeded={name => changes.handleChange(name)}
|
||||
disabled={true}
|
||||
plugin={p}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
plugins.push(
|
||||
<PluginCard
|
||||
onRestartNeeded={name => changes.handleChange(name)}
|
||||
disabled={false}
|
||||
plugin={p}
|
||||
isNew={newPlugins?.includes(p.name)}
|
||||
key={p.name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
requiredPlugins.push(
|
||||
<Tooltip text={tooltipText} key={p.name}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<PluginCard
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onRestartNeeded={name => changes.handleChange(name)}
|
||||
disabled={true}
|
||||
plugin={p}
|
||||
key={p.name}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
plugins.push(
|
||||
<PluginCard
|
||||
onRestartNeeded={name => changes.handleChange(name)}
|
||||
disabled={false}
|
||||
plugin={p}
|
||||
isNew={newPlugins?.includes(p.name)}
|
||||
key={p.name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
plugins = requiredPlugins = <Text variant="text-md/normal">No plugins meet search criteria.</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -334,10 +311,11 @@ export default function PluginSettings() {
|
|||
Filters
|
||||
</Forms.FormTitle>
|
||||
|
||||
<div className={classes(Margins.bottom20, cl("filter-controls"))}>
|
||||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} />
|
||||
<div className={cl("filter-controls")}>
|
||||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
|
||||
<div className={InputStyles.inputWrapper}>
|
||||
<Select
|
||||
className={InputStyles.inputDefault}
|
||||
options={[
|
||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||
|
@ -348,25 +326,15 @@ export default function PluginSettings() {
|
|||
select={onStatusChange}
|
||||
isSelected={v => v === searchValue.status}
|
||||
closeOnSelect={true}
|
||||
className={InputStyles.inputDefault}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
|
||||
|
||||
{plugins.length || requiredPlugins.length
|
||||
? (
|
||||
<div className={cl("grid")}>
|
||||
{plugins.length
|
||||
? plugins
|
||||
: <Text variant="text-md/normal">No plugins meet the search criteria.</Text>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
: <ExcludedPluginsList search={search} />
|
||||
}
|
||||
|
||||
<div className={cl("grid")}>
|
||||
{plugins}
|
||||
</div>
|
||||
|
||||
<Forms.FormDivider className={Margins.top20} />
|
||||
|
||||
|
@ -374,10 +342,7 @@ export default function PluginSettings() {
|
|||
Required Plugins
|
||||
</Forms.FormTitle>
|
||||
<div className={cl("grid")}>
|
||||
{requiredPlugins.length
|
||||
? requiredPlugins
|
||||
: <Text variant="text-md/normal">No plugins meet the search criteria.</Text>
|
||||
}
|
||||
{requiredPlugins}
|
||||
</div>
|
||||
</SettingsTab >
|
||||
);
|
||||
|
|
|
@ -78,7 +78,6 @@
|
|||
|
||||
.vc-plugins-restart-card button {
|
||||
margin-top: 0.5em;
|
||||
background: var(--info-warning-foreground) !important;
|
||||
}
|
||||
|
||||
.vc-plugins-info-button svg:not(:hover, :focus) {
|
||||
|
|
|
@ -21,7 +21,7 @@ import "./addonCard.css";
|
|||
import { classNameFactory } from "@api/Styles";
|
||||
import { Badge } from "@components/Badge";
|
||||
import { Switch } from "@components/Switch";
|
||||
import { Text, useRef } from "@webpack/common";
|
||||
import { Text } from "@webpack/common";
|
||||
import type { MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
const cl = classNameFactory("vc-addon-");
|
||||
|
@ -42,8 +42,6 @@ interface Props {
|
|||
}
|
||||
|
||||
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
|
||||
const titleRef = useRef<HTMLDivElement>(null);
|
||||
const titleContainerRef = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<div
|
||||
className={cl("card", { "card-disabled": disabled })}
|
||||
|
@ -53,21 +51,7 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
|
|||
<div className={cl("header")}>
|
||||
<div className={cl("name-author")}>
|
||||
<Text variant="text-md/bold" className={cl("name")}>
|
||||
<div ref={titleContainerRef} className={cl("title-container")}>
|
||||
<div
|
||||
ref={titleRef}
|
||||
className={cl("title")}
|
||||
onMouseOver={() => {
|
||||
const title = titleRef.current!;
|
||||
const titleContainer = titleContainerRef.current!;
|
||||
|
||||
title.style.setProperty("--offset", `${titleContainer.clientWidth - title.scrollWidth}px`);
|
||||
title.style.setProperty("--duration", `${Math.max(0.5, (title.scrollWidth - titleContainer.clientWidth) / 7)}s`);
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</div>{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||
{name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||
</Text>
|
||||
{!!author && (
|
||||
<Text variant="text-md/normal" className={cl("author")}>
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
import { showNotification } from "@api/Notifications";
|
||||
import { Settings, useSettings } from "@api/Settings";
|
||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||
import { Grid } from "@components/Grid";
|
||||
import { Link } from "@components/Link";
|
||||
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
||||
import { Margins } from "@utils/margins";
|
||||
|
@ -40,7 +39,9 @@ function validateUrl(url: string) {
|
|||
async function eraseAllData() {
|
||||
const res = await fetch(new URL("/v1/", getCloudUrl()), {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: await getCloudAuth() }
|
||||
headers: new Headers({
|
||||
Authorization: await getCloudAuth()
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
@ -86,9 +87,7 @@ function SettingsSyncSection() {
|
|||
size={Button.Sizes.SMALL}
|
||||
disabled={!sectionEnabled}
|
||||
onClick={() => putCloudSettings(true)}
|
||||
>
|
||||
Sync to Cloud
|
||||
</Button>
|
||||
>Sync to Cloud</Button>
|
||||
<Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!">
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<Button
|
||||
|
@ -98,9 +97,7 @@ function SettingsSyncSection() {
|
|||
color={Button.Colors.RED}
|
||||
disabled={!sectionEnabled}
|
||||
onClick={() => getCloudSettings(true, true)}
|
||||
>
|
||||
Sync from Cloud
|
||||
</Button>
|
||||
>Sync from Cloud</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
<Button
|
||||
|
@ -108,9 +105,7 @@ function SettingsSyncSection() {
|
|||
color={Button.Colors.RED}
|
||||
disabled={!sectionEnabled}
|
||||
onClick={() => deleteCloudSettings()}
|
||||
>
|
||||
Delete Cloud Settings
|
||||
</Button>
|
||||
>Delete Cloud Settings</Button>
|
||||
</div>
|
||||
</Forms.FormSection>
|
||||
);
|
||||
|
@ -131,12 +126,7 @@ function CloudTab() {
|
|||
<Switch
|
||||
key="backend"
|
||||
value={settings.cloud.authenticated}
|
||||
onChange={v => {
|
||||
if (v)
|
||||
authorizeCloud();
|
||||
else
|
||||
settings.cloud.authenticated = v;
|
||||
}}
|
||||
onChange={v => { v && authorizeCloud(); if (!v) settings.cloud.authenticated = v; }}
|
||||
note="This will request authorization if you have not yet set up cloud integrations."
|
||||
>
|
||||
Enable Cloud Integrations
|
||||
|
@ -148,43 +138,23 @@ function CloudTab() {
|
|||
<CheckedTextInput
|
||||
key="backendUrl"
|
||||
value={settings.cloud.url}
|
||||
onChange={async v => {
|
||||
settings.cloud.url = v;
|
||||
settings.cloud.authenticated = false;
|
||||
deauthorizeCloud();
|
||||
}}
|
||||
onChange={v => { settings.cloud.url = v; settings.cloud.authenticated = false; deauthorizeCloud(); }}
|
||||
validate={validateUrl}
|
||||
/>
|
||||
|
||||
<Grid columns={2} gap="1em" className={Margins.top8}>
|
||||
<Button
|
||||
size={Button.Sizes.MEDIUM}
|
||||
disabled={!settings.cloud.authenticated}
|
||||
onClick={async () => {
|
||||
await deauthorizeCloud();
|
||||
settings.cloud.authenticated = false;
|
||||
await authorizeCloud();
|
||||
}}
|
||||
>
|
||||
Reauthorise
|
||||
</Button>
|
||||
<Button
|
||||
size={Button.Sizes.MEDIUM}
|
||||
color={Button.Colors.RED}
|
||||
disabled={!settings.cloud.authenticated}
|
||||
onClick={() => Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Once your data is erased, we cannot recover it. There's no going back!",
|
||||
onConfirm: eraseAllData,
|
||||
confirmText: "Erase it!",
|
||||
confirmColor: "vc-cloud-erase-data-danger-btn",
|
||||
cancelText: "Nevermind"
|
||||
})}
|
||||
>
|
||||
Erase All Data
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Button
|
||||
className={Margins.top8}
|
||||
size={Button.Sizes.MEDIUM}
|
||||
color={Button.Colors.RED}
|
||||
disabled={!settings.cloud.authenticated}
|
||||
onClick={() => Alerts.show({
|
||||
title: "Are you sure?",
|
||||
body: "Once your data is erased, we cannot recover it. There's no going back!",
|
||||
onConfirm: eraseAllData,
|
||||
confirmText: "Erase it!",
|
||||
confirmColor: "vc-cloud-erase-data-danger-btn",
|
||||
cancelText: "Nevermind"
|
||||
})}
|
||||
>Erase All Data</Button>
|
||||
<Forms.FormDivider className={Margins.top16} />
|
||||
</Forms.FormSection >
|
||||
<SettingsSyncSection />
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { useSettings } from "@api/Settings";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { Forms, Select, Slider, Text } from "@webpack/common";
|
||||
|
||||
import { ErrorCard } from "..";
|
||||
|
||||
export function NotificationSettings() {
|
||||
const settings = useSettings().notifications;
|
||||
|
||||
return (
|
||||
<div style={{ padding: "1em 0" }}>
|
||||
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
|
||||
{settings.useNative !== "never" && Notification?.permission === "denied" && (
|
||||
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
|
||||
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
|
||||
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
|
||||
</ErrorCard>
|
||||
)}
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
Some plugins may show you notifications. These come in two styles:
|
||||
<ul>
|
||||
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
|
||||
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
|
||||
</ul>
|
||||
</Forms.FormText>
|
||||
<Select
|
||||
placeholder="Notification Style"
|
||||
options={[
|
||||
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
|
||||
{ label: "Always use Desktop notifications", value: "always" },
|
||||
{ label: "Always use Vencord notifications", value: "never" },
|
||||
] satisfies Array<{ value: typeof settings["useNative"]; } & Record<string, any>>}
|
||||
closeOnSelect={true}
|
||||
select={v => settings.useNative = v}
|
||||
isSelected={v => v === settings.useNative}
|
||||
serialize={identity}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
|
||||
<Select
|
||||
isDisabled={settings.useNative === "always"}
|
||||
placeholder="Notification Position"
|
||||
options={[
|
||||
{ label: "Bottom Right", value: "bottom-right", default: true },
|
||||
{ label: "Top Right", value: "top-right" },
|
||||
] satisfies Array<{ value: typeof settings["position"]; } & Record<string, any>>}
|
||||
select={v => settings.position = v}
|
||||
isSelected={v => v === settings.position}
|
||||
serialize={identity}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
|
||||
<Slider
|
||||
disabled={settings.useNative === "always"}
|
||||
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
|
||||
minValue={0}
|
||||
maxValue={20_000}
|
||||
initialValue={settings.timeout}
|
||||
onValueChange={v => settings.timeout = v}
|
||||
onValueRender={v => (v / 1000).toFixed(2) + "s"}
|
||||
onMarkerRender={v => (v / 1000) + "s"}
|
||||
stickToMarkers={false}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom16}>
|
||||
The amount of notifications to save in the log until old ones are removed.
|
||||
Set to <code>0</code> to disable Notification log and <code>∞</code> to never automatically remove old Notifications
|
||||
</Forms.FormText>
|
||||
<Slider
|
||||
markers={[0, 25, 50, 75, 100, 200]}
|
||||
minValue={0}
|
||||
maxValue={200}
|
||||
stickToMarkers={true}
|
||||
initialValue={settings.logLimit}
|
||||
onValueChange={v => settings.logLimit = v}
|
||||
onValueRender={v => v === 200 ? "∞" : v}
|
||||
onMarkerRender={v => v === 200 ? "∞" : v}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function openNotificationSettingsModal() {
|
||||
openModal(props => (
|
||||
<ModalRoot {...props} size={ModalSize.MEDIUM}>
|
||||
<ModalHeader>
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Settings</Text>
|
||||
<ModalCloseButton onClick={props.onClose} />
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<NotificationSettings />
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
));
|
||||
}
|
|
@ -16,14 +16,15 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||
import { CodeBlock } from "@components/CodeBlock";
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||
import { makeCodeblock } from "@utils/text";
|
||||
import { Patch, ReplaceFn } from "@utils/types";
|
||||
import { ReplaceFn } from "@utils/types";
|
||||
import { search } from "@webpack";
|
||||
import { Button, Clipboard, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common";
|
||||
import { Button, Clipboard, Forms, Parser, React, Switch, TextInput } from "@webpack/common";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
|
@ -46,7 +47,7 @@ const findCandidates = debounce(function ({ find, setModule, setError }) {
|
|||
|
||||
interface ReplacementComponentProps {
|
||||
module: [id: number, factory: Function];
|
||||
match: string;
|
||||
match: string | RegExp;
|
||||
replacement: string | ReplaceFn;
|
||||
setReplacementError(error: any): void;
|
||||
}
|
||||
|
@ -57,13 +58,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
|||
|
||||
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
||||
const src: string = fact.toString().replaceAll("\n", "");
|
||||
|
||||
try {
|
||||
new RegExp(match);
|
||||
} catch (e) {
|
||||
return ["", [], []];
|
||||
}
|
||||
const canonicalMatch = canonicalizeMatch(new RegExp(match));
|
||||
const canonicalMatch = canonicalizeMatch(match);
|
||||
try {
|
||||
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
||||
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
||||
|
@ -113,7 +108,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
|||
function renderDiff() {
|
||||
return diff?.map(p => {
|
||||
const color = p.added ? "lime" : p.removed ? "red" : "grey";
|
||||
return <div style={{ color, userSelect: "text", wordBreak: "break-all", lineBreak: "anywhere" }}>{p.value}</div>;
|
||||
return <div style={{ color, userSelect: "text" }}>{p.value}</div>;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -185,8 +180,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
{/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */}
|
||||
<Forms.FormTitle className="">replacement</Forms.FormTitle>
|
||||
<Forms.FormTitle>replacement</Forms.FormTitle>
|
||||
<TextInput
|
||||
value={replacement?.toString()}
|
||||
onChange={onChange}
|
||||
|
@ -194,7 +188,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||
/>
|
||||
{!isFunc && (
|
||||
<div className="vc-text-selectable">
|
||||
<Forms.FormTitle className={Margins.top8}>Cheat Sheet</Forms.FormTitle>
|
||||
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
|
||||
{Object.entries({
|
||||
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
|
||||
"$$": "Insert a $",
|
||||
|
@ -224,66 +218,8 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||
);
|
||||
}
|
||||
|
||||
interface FullPatchInputProps {
|
||||
setFind(v: string): void;
|
||||
setParsedFind(v: string | RegExp): void;
|
||||
setMatch(v: string): void;
|
||||
setReplacement(v: string | ReplaceFn): void;
|
||||
}
|
||||
|
||||
function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: FullPatchInputProps) {
|
||||
const [fullPatch, setFullPatch] = React.useState<string>("");
|
||||
const [fullPatchError, setFullPatchError] = React.useState<string>("");
|
||||
|
||||
function update() {
|
||||
if (fullPatch === "") {
|
||||
setFullPatchError("");
|
||||
|
||||
setFind("");
|
||||
setParsedFind("");
|
||||
setMatch("");
|
||||
setReplacement("");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = (0, eval)(`([${fullPatch}][0])`) as Patch;
|
||||
|
||||
if (!parsed.find) throw new Error("No 'find' field");
|
||||
if (!parsed.replacement) throw new Error("No 'replacement' field");
|
||||
|
||||
if (parsed.replacement instanceof Array) {
|
||||
if (parsed.replacement.length === 0) throw new Error("Invalid replacement");
|
||||
|
||||
parsed.replacement = {
|
||||
match: parsed.replacement[0].match,
|
||||
replace: parsed.replacement[0].replace
|
||||
};
|
||||
}
|
||||
|
||||
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
|
||||
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
|
||||
|
||||
setFind(parsed.find instanceof RegExp ? parsed.find.toString() : parsed.find);
|
||||
setParsedFind(parsed.find);
|
||||
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
|
||||
setReplacement(parsed.replacement.replace);
|
||||
setFullPatchError("");
|
||||
} catch (e) {
|
||||
setFullPatchError((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<Forms.FormText className={Margins.bottom8}>Paste your full JSON patch here to fill out the fields</Forms.FormText>
|
||||
<TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} />
|
||||
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
|
||||
</>;
|
||||
}
|
||||
|
||||
function PatchHelper() {
|
||||
const [find, setFind] = React.useState<string>("");
|
||||
const [parsedFind, setParsedFind] = React.useState<string | RegExp>("");
|
||||
const [match, setMatch] = React.useState<string>("");
|
||||
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
||||
|
||||
|
@ -291,60 +227,40 @@ function PatchHelper() {
|
|||
|
||||
const [module, setModule] = React.useState<[number, Function]>();
|
||||
const [findError, setFindError] = React.useState<string>();
|
||||
const [matchError, setMatchError] = React.useState<string>();
|
||||
|
||||
const code = React.useMemo(() => {
|
||||
return `
|
||||
{
|
||||
find: ${parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind)},
|
||||
find: ${JSON.stringify(find)},
|
||||
replacement: {
|
||||
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
||||
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
||||
}
|
||||
}
|
||||
`.trim();
|
||||
}, [parsedFind, match, replacement]);
|
||||
}, [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 {
|
||||
let parsedFind = v as string | RegExp;
|
||||
if (/^\/.+?\/$/.test(v)) parsedFind = new RegExp(v.slice(1, -1));
|
||||
|
||||
new RegExp(v);
|
||||
setFindError(void 0);
|
||||
setParsedFind(parsedFind);
|
||||
|
||||
if (v.length) {
|
||||
findCandidates({ find: parsedFind, setModule, setError: setFindError });
|
||||
}
|
||||
setMatch(v);
|
||||
} catch (e: any) {
|
||||
setFindError((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
function onMatchChange(v: string) {
|
||||
setMatch(v);
|
||||
|
||||
try {
|
||||
new RegExp(v);
|
||||
setMatchError(void 0);
|
||||
} catch (e: any) {
|
||||
setMatchError((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsTab title="Patch Helper">
|
||||
<Forms.FormTitle>full patch</Forms.FormTitle>
|
||||
<FullPatchInput
|
||||
setFind={onFindChange}
|
||||
setParsedFind={setParsedFind}
|
||||
setMatch={onMatchChange}
|
||||
setReplacement={setReplacement}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle className={Margins.top8}>find</Forms.FormTitle>
|
||||
<Forms.FormTitle>find</Forms.FormTitle>
|
||||
<TextInput
|
||||
type="text"
|
||||
value={find}
|
||||
|
@ -352,15 +268,19 @@ function PatchHelper() {
|
|||
error={findError}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
|
||||
<TextInput
|
||||
type="text"
|
||||
<Forms.FormTitle>match</Forms.FormTitle>
|
||||
<CheckedTextInput
|
||||
value={match}
|
||||
onChange={onMatchChange}
|
||||
error={matchError}
|
||||
validate={v => {
|
||||
try {
|
||||
return (new RegExp(v), true);
|
||||
} catch (e) {
|
||||
return (e as Error).message;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={Margins.top8} />
|
||||
<ReplacementInput
|
||||
replacement={replacement}
|
||||
setReplacement={setReplacement}
|
||||
|
@ -371,7 +291,7 @@ function PatchHelper() {
|
|||
{module && (
|
||||
<ReplacementComponent
|
||||
module={module}
|
||||
match={match}
|
||||
match={new RegExp(match)}
|
||||
replacement={replacement}
|
||||
setReplacementError={setReplacementError}
|
||||
/>
|
||||
|
@ -382,7 +302,6 @@ function PatchHelper() {
|
|||
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
|
||||
<CodeBlock lang="js" content={code} />
|
||||
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
||||
<Button className={Margins.top8} onClick={() => Clipboard.copy("```ts\n" + code + "\n```")}>Copy as Codeblock</Button>
|
||||
</>
|
||||
)}
|
||||
</SettingsTab>
|
||||
|
|
|
@ -16,25 +16,21 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Settings, useSettings } from "@api/Settings";
|
||||
import { useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
|
||||
import { DeleteIcon } from "@components/Icons";
|
||||
import { Link } from "@components/Link";
|
||||
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||
import type { UserThemeHeader } from "@main/themes";
|
||||
import { openInviteModal } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { showItemInFolder } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { findLazy } from "@webpack";
|
||||
import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
||||
import { findByPropsLazy, findLazy } from "@webpack";
|
||||
import { Button, Card, FluxDispatcher, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
||||
import { UserThemeHeader } from "main/themes";
|
||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
|
||||
import { AddonCard } from "./AddonCard";
|
||||
import { QuickAction, QuickActionCard } from "./quickActions";
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
type FileInput = ComponentType<{
|
||||
|
@ -44,7 +40,9 @@ type FileInput = ComponentType<{
|
|||
filters?: { name?: string; extensions: string[]; }[];
|
||||
}>;
|
||||
|
||||
const InviteActions = findByPropsLazy("resolveInvite");
|
||||
const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
|
||||
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
||||
|
||||
const cl = classNameFactory("vc-settings-theme-");
|
||||
|
||||
|
@ -77,16 +75,8 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
|||
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
|
||||
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
||||
<div>
|
||||
{themeLinks.map(rawLink => {
|
||||
const { label, link } = (() => {
|
||||
const match = /^@(light|dark) (.*)/.exec(rawLink);
|
||||
if (!match) return { label: rawLink, link: rawLink };
|
||||
|
||||
const [, mode, link] = match;
|
||||
return { label: `[${mode} mode only] ${link}`, link };
|
||||
})();
|
||||
|
||||
return <Card style={{
|
||||
{themeLinks.map(link => (
|
||||
<Card style={{
|
||||
padding: ".5em",
|
||||
marginBottom: ".5em",
|
||||
marginTop: ".5em"
|
||||
|
@ -94,11 +84,11 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
|||
<Forms.FormTitle tag="h5" style={{
|
||||
overflowWrap: "break-word"
|
||||
}}>
|
||||
{label}
|
||||
{link}
|
||||
</Forms.FormTitle>
|
||||
<Validator link={link} />
|
||||
</Card>;
|
||||
})}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -135,7 +125,15 @@ function ThemeCard({ theme, enabled, onChange, onDelete }: ThemeCardProps) {
|
|||
href={`https://discord.gg/${theme.invite}`}
|
||||
onClick={async e => {
|
||||
e.preventDefault();
|
||||
theme.invite != null && openInviteModal(theme.invite).catch(() => showToast("Invalid or expired invite"));
|
||||
const { invite } = await InviteActions.resolveInvite(theme.invite, "Desktop Modal");
|
||||
if (!invite) return showToast("Invalid or expired invite");
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
type: "INVITE_MODAL_OPEN",
|
||||
invite,
|
||||
code: theme.invite,
|
||||
context: "APP"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Discord Server
|
||||
|
@ -220,52 +218,45 @@ function ThemesTab() {
|
|||
</Card>
|
||||
|
||||
<Forms.FormSection title="Local Themes">
|
||||
<QuickActionCard>
|
||||
<Card className="vc-settings-quick-actions-card">
|
||||
<>
|
||||
{IS_WEB ?
|
||||
(
|
||||
<QuickAction
|
||||
text={
|
||||
<span style={{ position: "relative" }}>
|
||||
Upload Theme
|
||||
<FileInput
|
||||
ref={fileInputRef}
|
||||
onChange={onFileUpload}
|
||||
multiple={true}
|
||||
filters={[{ extensions: ["css"] }]}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
Icon={PlusIcon}
|
||||
/>
|
||||
) : (
|
||||
<QuickAction
|
||||
text="Open Themes Folder"
|
||||
action={() => showItemInFolder(themeDir!)}
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={themeDirPending}
|
||||
Icon={FolderIcon}
|
||||
/>
|
||||
>
|
||||
Upload Theme
|
||||
<FileInput
|
||||
ref={fileInputRef}
|
||||
onChange={onFileUpload}
|
||||
multiple={true}
|
||||
filters={[{ extensions: ["css"] }]}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => showItemInFolder(themeDir!)}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={themeDirPending}
|
||||
>
|
||||
Open Themes Folder
|
||||
</Button>
|
||||
)}
|
||||
<QuickAction
|
||||
text="Load missing Themes"
|
||||
action={refreshLocalThemes}
|
||||
Icon={RestartIcon}
|
||||
/>
|
||||
<QuickAction
|
||||
text="Edit QuickCSS"
|
||||
action={() => VencordNative.quickCss.openEditor()}
|
||||
Icon={PaintbrushIcon}
|
||||
/>
|
||||
|
||||
{Settings.plugins.ClientTheme.enabled && (
|
||||
<QuickAction
|
||||
text="Edit ClientTheme"
|
||||
action={() => openPluginModal(Plugins.ClientTheme)}
|
||||
Icon={PencilIcon}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onClick={refreshLocalThemes}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Load missing Themes
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => VencordNative.quickCss.openEditor()}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Edit QuickCSS
|
||||
</Button>
|
||||
</>
|
||||
</QuickActionCard>
|
||||
</Card>
|
||||
|
||||
<div className={cl("grid")}>
|
||||
{userThemes?.map(theme => (
|
||||
|
@ -304,7 +295,6 @@ function ThemesTab() {
|
|||
<Card className="vc-settings-card vc-text-selectable">
|
||||
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
|
||||
<Forms.FormText>One link per line</Forms.FormText>
|
||||
<Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText>
|
||||
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
|
||||
</Card>
|
||||
|
||||
|
@ -312,7 +302,7 @@ function ThemesTab() {
|
|||
<TextArea
|
||||
value={themeText}
|
||||
onChange={setThemeText}
|
||||
className={"vc-settings-theme-links"}
|
||||
className={classes(TextAreaProps.textarea, "vc-settings-theme-links")}
|
||||
placeholder="Theme Links"
|
||||
spellCheck={false}
|
||||
onBlur={onBlur}
|
||||
|
|
|
@ -22,7 +22,6 @@ import { Flex } from "@components/Flex";
|
|||
import { Link } from "@components/Link";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
|
||||
|
@ -30,7 +29,7 @@ import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@web
|
|||
|
||||
import gitHash from "~git-hash";
|
||||
|
||||
import { handleSettingsTabError, SettingsTab, wrapTab } from "./shared";
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
||||
return async () => {
|
||||
|
@ -39,24 +38,21 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
|
|||
await action();
|
||||
} catch (e: any) {
|
||||
UpdateLogger.error("Failed to update", e);
|
||||
|
||||
let err: string;
|
||||
if (!e) {
|
||||
err = "An unknown error occurred (error is undefined).\nPlease try again.";
|
||||
var err = "An unknown error occurred (error is undefined).\nPlease try again.";
|
||||
} else if (e.code && e.cmd) {
|
||||
const { code, path, cmd, stderr } = e;
|
||||
|
||||
if (code === "ENOENT")
|
||||
err = `Command \`${path}\` not found.\nPlease install it and try again`;
|
||||
var err = `Command \`${path}\` not found.\nPlease install it and try again`;
|
||||
else {
|
||||
err = `An error occurred while running \`${cmd}\`:\n`;
|
||||
var err = `An error occurred while running \`${cmd}\`:\n`;
|
||||
err += stderr || `Code \`${code}\`. See the console for more info`;
|
||||
}
|
||||
|
||||
} else {
|
||||
err = "An unknown error occurred. See the console for more info.";
|
||||
var err = "An unknown error occurred. See the console for more info.";
|
||||
}
|
||||
|
||||
Alerts.show({
|
||||
title: "Oops!",
|
||||
body: (
|
||||
|
@ -85,12 +81,9 @@ function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string
|
|||
|
||||
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
|
||||
return (
|
||||
<Card style={{ padding: "0 0.5em" }}>
|
||||
<Card style={{ padding: ".5em" }}>
|
||||
{updates.map(({ hash, author, message }) => (
|
||||
<div style={{
|
||||
marginTop: "0.5em",
|
||||
marginBottom: "0.5em"
|
||||
}}>
|
||||
<div>
|
||||
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
|
||||
<span style={{
|
||||
marginLeft: "0.5em",
|
||||
|
@ -120,7 +113,7 @@ function Updatable(props: CommonProps) {
|
|||
</>
|
||||
) : (
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
{isOutdated ? (updates.length === 1 ? "There is 1 Update" : `There are ${updates.length} Updates`) : "Up to Date!"}
|
||||
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
|
||||
</Forms.FormText>
|
||||
)}
|
||||
|
||||
|
@ -190,7 +183,7 @@ function Newer(props: CommonProps) {
|
|||
}
|
||||
|
||||
function Updater() {
|
||||
const settings = useSettings(["autoUpdate", "autoUpdateNotification"]);
|
||||
const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
|
||||
|
||||
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
||||
|
||||
|
@ -207,6 +200,14 @@ function Updater() {
|
|||
return (
|
||||
<SettingsTab title="Vencord Updater">
|
||||
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||
<Switch
|
||||
value={settings.notifyAboutUpdates}
|
||||
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||
note="Shows a notification on startup"
|
||||
disabled={settings.autoUpdate}
|
||||
>
|
||||
Get notified about new updates
|
||||
</Switch>
|
||||
<Switch
|
||||
value={settings.autoUpdate}
|
||||
onChange={(v: boolean) => settings.autoUpdate = v}
|
||||
|
@ -249,20 +250,3 @@ function Updater() {
|
|||
}
|
||||
|
||||
export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");
|
||||
|
||||
export const openUpdaterModal = IS_UPDATER_DISABLED ? null : function () {
|
||||
const UpdaterTab = wrapTab(Updater, "Updater");
|
||||
|
||||
try {
|
||||
openModal(wrapTab((modalProps: ModalProps) => (
|
||||
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||
<ModalContent className="vc-updater-modal">
|
||||
<ModalCloseButton onClick={modalProps.onClose} className="vc-updater-modal-close-button" />
|
||||
<UpdaterTab />
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
), "UpdaterModal"));
|
||||
} catch {
|
||||
handleSettingsTabError();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -17,20 +17,16 @@
|
|||
*/
|
||||
|
||||
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
||||
import { useSettings } from "@api/Settings";
|
||||
import { Settings, useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import DonateButton from "@components/DonateButton";
|
||||
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||
import { gitRemote } from "@shared/vencordUserAgent";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity } from "@utils/misc";
|
||||
import { relaunch, showItemInFolder } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { Button, Card, Forms, React, Select, Switch } from "@webpack/common";
|
||||
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
|
||||
|
||||
import { Flex, FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "..";
|
||||
import { openNotificationSettingsModal } from "./NotificationSettings";
|
||||
import { QuickAction, QuickActionCard } from "./quickActions";
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
const cl = classNameFactory("vc-settings-");
|
||||
|
@ -42,7 +38,6 @@ type KeysOfType<Object, Type> = {
|
|||
[K in keyof Object]: Object[K] extends Type ? K : never;
|
||||
}[keyof Object];
|
||||
|
||||
|
||||
function VencordSettings() {
|
||||
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
|
||||
fallbackValue: "Loading..."
|
||||
|
@ -53,7 +48,6 @@ function VencordSettings() {
|
|||
|
||||
const isWindows = navigator.platform.toLowerCase().startsWith("win");
|
||||
const isMac = navigator.platform.toLowerCase().startsWith("mac");
|
||||
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
|
||||
|
||||
const Switches: Array<false | {
|
||||
key: KeysOfType<typeof settings, boolean>;
|
||||
|
@ -80,10 +74,10 @@ function VencordSettings() {
|
|||
title: "Use Windows' native title bar instead of Discord's custom one",
|
||||
note: "Requires a full restart"
|
||||
}),
|
||||
!IS_WEB && {
|
||||
!IS_WEB && false /* This causes electron to freeze / white screen for some people */ && {
|
||||
key: "transparent",
|
||||
title: "Enable window transparency.",
|
||||
note: "You need a theme that supports transparency or this will do nothing. WILL STOP THE WINDOW FROM BEING RESIZABLE!! Requires a full restart"
|
||||
title: "Enable window transparency",
|
||||
note: "Requires a full restart"
|
||||
},
|
||||
!IS_WEB && isWindows && {
|
||||
key: "winCtrlQ",
|
||||
|
@ -95,59 +89,56 @@ function VencordSettings() {
|
|||
title: "Disable minimum window size",
|
||||
note: "Requires a full restart"
|
||||
},
|
||||
IS_DISCORD_DESKTOP && isMac && {
|
||||
key: "macosTranslucency",
|
||||
title: "Enable translucent window",
|
||||
note: "Requires a full restart"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsTab title="Vencord Settings">
|
||||
<DonateCard image={donateImage} />
|
||||
<Forms.FormSection title="Quick Actions">
|
||||
<QuickActionCard>
|
||||
<QuickAction
|
||||
Icon={LogIcon}
|
||||
text="Notification Log"
|
||||
action={openNotificationLogModal}
|
||||
/>
|
||||
<QuickAction
|
||||
Icon={PaintbrushIcon}
|
||||
text="Edit QuickCSS"
|
||||
action={() => VencordNative.quickCss.openEditor()}
|
||||
/>
|
||||
{!IS_WEB && (
|
||||
<QuickAction
|
||||
Icon={RestartIcon}
|
||||
text="Relaunch Discord"
|
||||
action={relaunch}
|
||||
/>
|
||||
)}
|
||||
{!IS_WEB && (
|
||||
<QuickAction
|
||||
Icon={FolderIcon}
|
||||
text="Open Settings Folder"
|
||||
action={() => showItemInFolder(settingsDir)}
|
||||
/>
|
||||
)}
|
||||
<QuickAction
|
||||
Icon={GithubIcon}
|
||||
text="View Source Code"
|
||||
action={() => VencordNative.native.openExternal("https://github.com/" + gitRemote)}
|
||||
/>
|
||||
</QuickActionCard>
|
||||
<Card className={cl("quick-actions-card")}>
|
||||
<React.Fragment>
|
||||
{!IS_WEB && (
|
||||
<Button
|
||||
onClick={relaunch}
|
||||
size={Button.Sizes.SMALL}>
|
||||
Restart Client
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => VencordNative.quickCss.openEditor()}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDir === "Loading..."}>
|
||||
Open QuickCSS File
|
||||
</Button>
|
||||
{!IS_WEB && (
|
||||
<Button
|
||||
onClick={() => showItemInFolder(settingsDir)}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDirPending}>
|
||||
Open Settings Folder
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => VencordNative.native.openExternal("https://github.com/Vendicated/Vencord")}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDirPending}>
|
||||
Open in GitHub
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
</Card>
|
||||
</Forms.FormSection>
|
||||
|
||||
<Forms.FormDivider />
|
||||
|
||||
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
|
||||
<Forms.FormText className={Margins.bottom20} style={{ color: "var(--text-muted)" }}>
|
||||
Hint: You can change the position of this settings section in the
|
||||
{" "}<Button
|
||||
look={Button.Looks.BLANK}
|
||||
style={{ color: "var(--text-link)", display: "inline-block" }}
|
||||
onClick={() => openPluginModal(Vencord.Plugins.plugins.Settings)}
|
||||
>
|
||||
settings of the Settings plugin
|
||||
</Button>!
|
||||
<Forms.FormText className={Margins.bottom20}>
|
||||
Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
|
||||
</Forms.FormText>
|
||||
|
||||
{Switches.map(s => s && (
|
||||
<Switch
|
||||
key={s.key}
|
||||
|
@ -161,84 +152,94 @@ function VencordSettings() {
|
|||
</Forms.FormSection>
|
||||
|
||||
|
||||
{needsVibrancySettings && <>
|
||||
<Forms.FormTitle tag="h5">Window vibrancy style (requires restart)</Forms.FormTitle>
|
||||
<Select
|
||||
className={Margins.bottom20}
|
||||
placeholder="Window vibrancy style"
|
||||
options={[
|
||||
// Sorted from most opaque to most transparent
|
||||
{
|
||||
label: "No vibrancy", value: undefined
|
||||
},
|
||||
{
|
||||
label: "Under Page (window tinting)",
|
||||
value: "under-page"
|
||||
},
|
||||
{
|
||||
label: "Content",
|
||||
value: "content"
|
||||
},
|
||||
{
|
||||
label: "Window",
|
||||
value: "window"
|
||||
},
|
||||
{
|
||||
label: "Selection",
|
||||
value: "selection"
|
||||
},
|
||||
{
|
||||
label: "Titlebar",
|
||||
value: "titlebar"
|
||||
},
|
||||
{
|
||||
label: "Header",
|
||||
value: "header"
|
||||
},
|
||||
{
|
||||
label: "Sidebar",
|
||||
value: "sidebar"
|
||||
},
|
||||
{
|
||||
label: "Tooltip",
|
||||
value: "tooltip"
|
||||
},
|
||||
{
|
||||
label: "Menu",
|
||||
value: "menu"
|
||||
},
|
||||
{
|
||||
label: "Popover",
|
||||
value: "popover"
|
||||
},
|
||||
{
|
||||
label: "Fullscreen UI (transparent but slightly muted)",
|
||||
value: "fullscreen-ui"
|
||||
},
|
||||
{
|
||||
label: "HUD (Most transparent)",
|
||||
value: "hud"
|
||||
},
|
||||
]}
|
||||
select={v => settings.macosVibrancyStyle = v}
|
||||
isSelected={v => settings.macosVibrancyStyle === v}
|
||||
serialize={identity} />
|
||||
</>}
|
||||
|
||||
<Forms.FormSection className={Margins.top16} title="Vencord Notifications" tag="h5">
|
||||
<Flex>
|
||||
<Button onClick={openNotificationSettingsModal}>
|
||||
Notification Settings
|
||||
</Button>
|
||||
<Button onClick={openNotificationLogModal}>
|
||||
View Notification Log
|
||||
</Button>
|
||||
</Flex>
|
||||
</Forms.FormSection>
|
||||
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />}
|
||||
</SettingsTab>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationSection({ settings }: { settings: typeof Settings["notifications"]; }) {
|
||||
return (
|
||||
<>
|
||||
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
|
||||
{settings.useNative !== "never" && Notification?.permission === "denied" && (
|
||||
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
|
||||
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
|
||||
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
|
||||
</ErrorCard>
|
||||
)}
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
Some plugins may show you notifications. These come in two styles:
|
||||
<ul>
|
||||
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
|
||||
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
|
||||
</ul>
|
||||
</Forms.FormText>
|
||||
<Select
|
||||
placeholder="Notification Style"
|
||||
options={[
|
||||
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
|
||||
{ label: "Always use Desktop notifications", value: "always" },
|
||||
{ label: "Always use Vencord notifications", value: "never" },
|
||||
] satisfies Array<{ value: typeof settings["useNative"]; } & Record<string, any>>}
|
||||
closeOnSelect={true}
|
||||
select={v => settings.useNative = v}
|
||||
isSelected={v => v === settings.useNative}
|
||||
serialize={identity}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
|
||||
<Select
|
||||
isDisabled={settings.useNative === "always"}
|
||||
placeholder="Notification Position"
|
||||
options={[
|
||||
{ label: "Bottom Right", value: "bottom-right", default: true },
|
||||
{ label: "Top Right", value: "top-right" },
|
||||
] satisfies Array<{ value: typeof settings["position"]; } & Record<string, any>>}
|
||||
select={v => settings.position = v}
|
||||
isSelected={v => v === settings.position}
|
||||
serialize={identity}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
|
||||
<Slider
|
||||
disabled={settings.useNative === "always"}
|
||||
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
|
||||
minValue={0}
|
||||
maxValue={20_000}
|
||||
initialValue={settings.timeout}
|
||||
onValueChange={v => settings.timeout = v}
|
||||
onValueRender={v => (v / 1000).toFixed(2) + "s"}
|
||||
onMarkerRender={v => (v / 1000) + "s"}
|
||||
stickToMarkers={false}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom16}>
|
||||
The amount of notifications to save in the log until old ones are removed.
|
||||
Set to <code>0</code> to disable Notification log and <code>∞</code> to never automatically remove old Notifications
|
||||
</Forms.FormText>
|
||||
<Slider
|
||||
markers={[0, 25, 50, 75, 100, 200]}
|
||||
minValue={0}
|
||||
maxValue={200}
|
||||
stickToMarkers={true}
|
||||
initialValue={settings.logLimit}
|
||||
onValueChange={v => settings.logLimit = v}
|
||||
onValueRender={v => v === 200 ? "∞" : v}
|
||||
onMarkerRender={v => v === 200 ? "∞" : v}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={openNotificationLogModal}
|
||||
disabled={settings.logLimit === 0}
|
||||
>
|
||||
Open Notification Log
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface DonateCardProps {
|
||||
image: string;
|
||||
}
|
||||
|
|
|
@ -62,36 +62,3 @@
|
|||
.vc-addon-author::before {
|
||||
content: "by ";
|
||||
}
|
||||
|
||||
.vc-addon-title-container {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: 1.25em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vc-addon-title {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@keyframes vc-addon-title {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(var(--offset));
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.vc-addon-title:hover {
|
||||
overflow: visible;
|
||||
animation: vc-addon-title var(--duration) linear infinite;
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
.vc-settings-quickActions-card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, max-content));
|
||||
gap: 0.5em;
|
||||
justify-content: center;
|
||||
padding: 0.5em 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.vc-settings-quickActions-pill {
|
||||
all: unset;
|
||||
background: var(--background-secondary);
|
||||
color: var(--header-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
padding: 8px 12px;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.vc-settings-quickActions-pill:hover {
|
||||
background: var(--background-secondary-alt);
|
||||
}
|
||||
|
||||
.vc-settings-quickActions-pill:focus-visible {
|
||||
outline: 2px solid var(--focus-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.vc-settings-quickActions-img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./quickActions.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Card } from "@webpack/common";
|
||||
import type { ComponentType, PropsWithChildren, ReactNode } from "react";
|
||||
|
||||
const cl = classNameFactory("vc-settings-quickActions-");
|
||||
|
||||
export interface QuickActionProps {
|
||||
Icon: ComponentType<{ className?: string; }>;
|
||||
text: ReactNode;
|
||||
action?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function QuickAction(props: QuickActionProps) {
|
||||
const { Icon, action, text, disabled } = props;
|
||||
|
||||
return (
|
||||
<button className={cl("pill")} onClick={action} disabled={disabled}>
|
||||
<Icon className={cl("img")} />
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function QuickActionCard(props: PropsWithChildren) {
|
||||
return (
|
||||
<Card className={cl("card")}>
|
||||
{props.children}
|
||||
</Card>
|
||||
);
|
||||
}
|
|
@ -10,6 +10,17 @@
|
|||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.vc-settings-quick-actions-card {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
flex-grow: 1;
|
||||
flex-flow: row wrap;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.vc-settings-donate {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -33,20 +44,6 @@
|
|||
padding: 0.5em;
|
||||
border: 1px solid var(--background-modifier-accent);
|
||||
max-height: unset;
|
||||
background-color: transparent;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vc-settings-theme-links::placeholder {
|
||||
color: var(--header-secondary);
|
||||
}
|
||||
|
||||
.vc-settings-theme-links:focus {
|
||||
background-color: var(--background-tertiary);
|
||||
}
|
||||
|
||||
.vc-cloud-settings-sync-grid {
|
||||
|
@ -68,11 +65,3 @@
|
|||
/* discord also sets cursor: default which prevents the cursor from showing as text */
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
.vc-updater-modal {
|
||||
padding: 1.5em !important;
|
||||
}
|
||||
|
||||
.vc-updater-modal-close-button {
|
||||
float: right;
|
||||
}
|
||||
|
|
|
@ -42,11 +42,11 @@ export function SettingsTab({ title, children }: PropsWithChildren<{ title: stri
|
|||
);
|
||||
}
|
||||
|
||||
export const handleSettingsTabError = onlyOnce(handleComponentFailed);
|
||||
const onError = onlyOnce(handleComponentFailed);
|
||||
|
||||
export function wrapTab(component: ComponentType<any>, tab: string) {
|
||||
export function wrapTab(component: ComponentType, tab: string) {
|
||||
return ErrorBoundary.wrap(component, {
|
||||
message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`,
|
||||
onError: handleSettingsTabError,
|
||||
onError,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export * from "./Badge";
|
||||
export * from "./CheckedTextInput";
|
||||
export * from "./CodeBlock";
|
||||
export * from "./DonateButton";
|
||||
export { default as ErrorBoundary } from "./ErrorBoundary";
|
||||
export * from "./ErrorCard";
|
||||
export * from "./Flex";
|
||||
export * from "./Heart";
|
||||
export * from "./Icons";
|
||||
export * from "./Link";
|
||||
export * from "./Switch";
|
|
@ -18,14 +18,14 @@
|
|||
|
||||
import { Logger } from "@utils/Logger";
|
||||
|
||||
if (IS_DEV || IS_REPORTER) {
|
||||
if (IS_DEV) {
|
||||
var traces = {} as Record<string, [number, any[]]>;
|
||||
var logger = new Logger("Tracer", "#FFD166");
|
||||
}
|
||||
|
||||
const noop = function () { };
|
||||
|
||||
export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
|
||||
export const beginTrace = !IS_DEV ? noop :
|
||||
function beginTrace(name: string, ...args: any[]) {
|
||||
if (name in traces)
|
||||
throw new Error(`Trace ${name} already exists!`);
|
||||
|
@ -33,7 +33,7 @@ export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
|
|||
traces[name] = [performance.now(), args];
|
||||
};
|
||||
|
||||
export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) {
|
||||
export const finishTrace = !IS_DEV ? noop : function finishTrace(name: string) {
|
||||
const end = performance.now();
|
||||
|
||||
const [start, args] = traces[name];
|
||||
|
@ -48,7 +48,7 @@ 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 || IS_REPORTER)
|
||||
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>) {
|
||||
|
|
|
@ -1,182 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { canonicalizeMatch } from "@utils/patches";
|
||||
import * as Webpack from "@webpack";
|
||||
import { wreq } from "@webpack";
|
||||
|
||||
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
|
||||
|
||||
export async function loadLazyChunks() {
|
||||
try {
|
||||
LazyChunkLoaderLogger.log("Loading all chunks...");
|
||||
|
||||
const validChunks = new Set<number>();
|
||||
const invalidChunks = new Set<number>();
|
||||
const deferredRequires = new Set<number>();
|
||||
|
||||
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
|
||||
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
|
||||
|
||||
// True if resolved, false otherwise
|
||||
const chunksSearchPromises = [] as Array<() => boolean>;
|
||||
|
||||
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
|
||||
|
||||
let foundCssDebuggingLoad = false;
|
||||
|
||||
async function searchAndLoadLazyChunks(factoryCode: string) {
|
||||
// Workaround to avoid loading the CSS debugging chunk which turns the app pink
|
||||
const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
|
||||
|
||||
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
|
||||
const validChunkGroups = new Set<[chunkIds: number[], entryPoint: number]>();
|
||||
|
||||
const shouldForceDefer = false;
|
||||
|
||||
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
|
||||
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => Number(m[1])) : [];
|
||||
|
||||
if (chunkIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let invalidChunkGroup = false;
|
||||
|
||||
for (const id of chunkIds) {
|
||||
if (hasCssDebuggingLoad) {
|
||||
if (chunkIds.length > 1) {
|
||||
throw new Error("Found multiple chunks in factory that loads the CSS debugging chunk");
|
||||
}
|
||||
|
||||
invalidChunks.add(id);
|
||||
invalidChunkGroup = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
|
||||
|
||||
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
|
||||
.then(r => r.text())
|
||||
.then(t => t.includes("importScripts("));
|
||||
|
||||
if (isWorkerAsset) {
|
||||
invalidChunks.add(id);
|
||||
invalidChunkGroup = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
validChunks.add(id);
|
||||
}
|
||||
|
||||
if (!invalidChunkGroup) {
|
||||
validChunkGroups.add([chunkIds, Number(entryPoint)]);
|
||||
}
|
||||
}));
|
||||
|
||||
// Loads all found valid chunk groups
|
||||
await Promise.all(
|
||||
Array.from(validChunkGroups)
|
||||
.map(([chunkIds]) =>
|
||||
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
||||
)
|
||||
);
|
||||
|
||||
// Requires the entry points for all valid chunk groups
|
||||
for (const [, entryPoint] of validChunkGroups) {
|
||||
try {
|
||||
if (shouldForceDefer) {
|
||||
deferredRequires.add(entryPoint);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// setImmediate to only check if all chunks were loaded after this function resolves
|
||||
// We check if all chunks were loaded every time a factory is loaded
|
||||
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
|
||||
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
|
||||
setTimeout(() => {
|
||||
let allResolved = true;
|
||||
|
||||
for (let i = 0; i < chunksSearchPromises.length; i++) {
|
||||
const isResolved = chunksSearchPromises[i]();
|
||||
|
||||
if (isResolved) {
|
||||
// Remove finished promises to avoid having to iterate through a huge array everytime
|
||||
chunksSearchPromises.splice(i--, 1);
|
||||
} else {
|
||||
allResolved = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (allResolved) chunksSearchingResolve();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
Webpack.factoryListeners.add(factory => {
|
||||
let isResolved = false;
|
||||
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
|
||||
|
||||
chunksSearchPromises.push(() => isResolved);
|
||||
});
|
||||
|
||||
for (const factoryId in wreq.m) {
|
||||
let isResolved = false;
|
||||
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
|
||||
|
||||
chunksSearchPromises.push(() => isResolved);
|
||||
}
|
||||
|
||||
await chunksSearchingDone;
|
||||
|
||||
// Require deferred entry points
|
||||
for (const deferredRequire of deferredRequires) {
|
||||
wreq!(deferredRequire as any);
|
||||
}
|
||||
|
||||
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
||||
const allChunks = [] as number[];
|
||||
|
||||
// Matches "id" or id:
|
||||
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
|
||||
const id = currentMatch[1] ?? currentMatch[2];
|
||||
if (id == null) continue;
|
||||
|
||||
allChunks.push(Number(id));
|
||||
}
|
||||
|
||||
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
||||
|
||||
// Chunks that are not loaded (not used) by Discord code anymore
|
||||
const chunksLeft = allChunks.filter(id => {
|
||||
return !(validChunks.has(id) || invalidChunks.has(id));
|
||||
});
|
||||
|
||||
await Promise.all(chunksLeft.map(async id => {
|
||||
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
|
||||
.then(r => r.text())
|
||||
.then(t => t.includes("importScripts("));
|
||||
|
||||
// Loads and requires a chunk
|
||||
if (!isWorkerAsset) {
|
||||
await wreq.e(id as any);
|
||||
// Technically, the id of the chunk does not match the entry point
|
||||
// But, still try it because we have no way to get the actual entry point
|
||||
if (wreq.m[id]) wreq(id as any);
|
||||
}
|
||||
}));
|
||||
|
||||
LazyChunkLoaderLogger.log("Finished loading all chunks!");
|
||||
} catch (e) {
|
||||
LazyChunkLoaderLogger.log("A fatal error occurred:", e);
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import * as Webpack from "@webpack";
|
||||
import { patches } from "plugins";
|
||||
|
||||
import { loadLazyChunks } from "./loadLazyChunks";
|
||||
|
||||
const ReporterLogger = new Logger("Reporter");
|
||||
|
||||
async function runReporter() {
|
||||
try {
|
||||
ReporterLogger.log("Starting test...");
|
||||
|
||||
let loadLazyChunksResolve: (value: void | PromiseLike<void>) => void;
|
||||
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);
|
||||
|
||||
Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
|
||||
await loadLazyChunksDone;
|
||||
|
||||
for (const patch of patches) {
|
||||
if (!patch.all) {
|
||||
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) {
|
||||
let method = searchType;
|
||||
|
||||
if (searchType === "findComponent") method = "find";
|
||||
if (searchType === "findExportedComponent") method = "findByProps";
|
||||
if (searchType === "waitFor" || searchType === "waitForComponent") {
|
||||
if (typeof args[0] === "string") method = "findByProps";
|
||||
else method = "find";
|
||||
}
|
||||
if (searchType === "waitForStore") method = "findStore";
|
||||
|
||||
let result: any;
|
||||
try {
|
||||
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
|
||||
const [factory] = args;
|
||||
result = factory();
|
||||
} else if (method === "extractAndLoadChunks") {
|
||||
const [code, matcher] = args;
|
||||
|
||||
result = await Webpack.extractAndLoadChunks(code, matcher);
|
||||
if (result === false) result = null;
|
||||
} else if (method === "mapMangledModule") {
|
||||
const [code, mapper] = args;
|
||||
|
||||
result = Webpack.mapMangledModule(code, mapper);
|
||||
if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail");
|
||||
} else {
|
||||
// @ts-ignore
|
||||
result = Webpack[method](...args);
|
||||
}
|
||||
|
||||
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail");
|
||||
} catch (e) {
|
||||
let logMessage = searchType;
|
||||
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
||||
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
||||
else if (method === "mapMangledModule") {
|
||||
const failedMappings = Object.keys(args[1]).filter(key => result?.[key] == null);
|
||||
|
||||
logMessage += `("${args[0]}", {\n${failedMappings.map(mapping => `\t${mapping}: ${args[1][mapping].toString().slice(0, 147)}...`).join(",\n")}\n})`;
|
||||
}
|
||||
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
||||
|
||||
ReporterLogger.log("Webpack Find Fail:", logMessage);
|
||||
}
|
||||
}
|
||||
|
||||
ReporterLogger.log("Finished test");
|
||||
} catch (e) {
|
||||
ReporterLogger.log("A fatal error occurred:", e);
|
||||
}
|
||||
}
|
||||
|
||||
runReporter();
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue