Compare commits
2 Commits
v1.4.4
...
features/c
Author | SHA1 | Date | |
---|---|---|---|
|
c3da99eeee | ||
|
0e7bd87cee |
@ -2,36 +2,40 @@
|
|||||||
"root": true,
|
"root": true,
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"ignorePatterns": ["dist", "browser"],
|
"ignorePatterns": ["dist", "browser"],
|
||||||
"plugins": [
|
"plugins": ["header", "simple-import-sort", "unused-imports"],
|
||||||
"@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": {
|
"rules": {
|
||||||
// Since it's only been a month and Vencord has already been stolen
|
// 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
|
// by random skids who rebranded it to "AlphaCord" and erased all license
|
||||||
// information
|
// information
|
||||||
"simple-header/header": [
|
"header/header": [
|
||||||
"error",
|
2,
|
||||||
{
|
"block",
|
||||||
"files": ["scripts/header-new.txt", "scripts/header-old.txt"],
|
[
|
||||||
"templates": { "author": [".*", "Vendicated and contributors"] }
|
{
|
||||||
}
|
"pattern": "!?",
|
||||||
|
"template": " "
|
||||||
|
},
|
||||||
|
" * Vencord, a modification for Discord's desktop app",
|
||||||
|
{
|
||||||
|
"pattern": " \\* Copyright \\(c\\) \\d{4}",
|
||||||
|
"template": " * Copyright (c) 2022 Vendicated and contributors"
|
||||||
|
},
|
||||||
|
" *",
|
||||||
|
" * This program is free software: you can redistribute it and/or modify",
|
||||||
|
" * it under the terms of the GNU General Public License as published by",
|
||||||
|
" * the Free Software Foundation, either version 3 of the License, or",
|
||||||
|
" * (at your option) any later version.",
|
||||||
|
" *",
|
||||||
|
" * This program is distributed in the hope that it will be useful,",
|
||||||
|
" * but WITHOUT ANY WARRANTY; without even the implied warranty of",
|
||||||
|
" * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the",
|
||||||
|
" * GNU General Public License for more details.",
|
||||||
|
" *",
|
||||||
|
" * You should have received a copy of the GNU General Public License",
|
||||||
|
" * along with this program. If not, see <https://www.gnu.org/licenses/>.",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
2
|
||||||
],
|
],
|
||||||
"quotes": ["error", "double", { "avoidEscape": true }],
|
"quotes": ["error", "double", { "avoidEscape": true }],
|
||||||
"jsx-quotes": ["error", "prefer-double"],
|
"jsx-quotes": ["error", "prefer-double"],
|
||||||
@ -39,7 +43,7 @@
|
|||||||
"indent": ["error", 4, { "SwitchCase": 1 }],
|
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||||
"arrow-parens": ["error", "as-needed"],
|
"arrow-parens": ["error", "as-needed"],
|
||||||
"eol-last": ["error", "always"],
|
"eol-last": ["error", "always"],
|
||||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
"func-call-spacing": ["error", "never"],
|
||||||
"no-multi-spaces": "error",
|
"no-multi-spaces": "error",
|
||||||
"no-trailing-spaces": "error",
|
"no-trailing-spaces": "error",
|
||||||
"no-whitespace-before-property": "error",
|
"no-whitespace-before-property": "error",
|
||||||
@ -59,13 +63,9 @@
|
|||||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||||
"no-duplicate-imports": "error",
|
"no-duplicate-imports": "error",
|
||||||
"no-extra-semi": "error",
|
"no-extra-semi": "error",
|
||||||
|
"consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }],
|
||||||
"dot-notation": "error",
|
"dot-notation": "error",
|
||||||
"no-useless-escape": [
|
"no-useless-escape": "error",
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"extra": "i"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-fallthrough": "error",
|
"no-fallthrough": "error",
|
||||||
"for-direction": "error",
|
"for-direction": "error",
|
||||||
"no-async-promise-executor": "error",
|
"no-async-promise-executor": "error",
|
||||||
@ -88,8 +88,6 @@
|
|||||||
"simple-import-sort/imports": "error",
|
"simple-import-sort/imports": "error",
|
||||||
"simple-import-sort/exports": "error",
|
"simple-import-sort/exports": "error",
|
||||||
|
|
||||||
"unused-imports/no-unused-imports": "error",
|
"unused-imports/no-unused-imports": "error"
|
||||||
|
|
||||||
"path-alias/no-relative": "error"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1 +0,0 @@
|
|||||||
* text=auto eol=lf
|
|
66
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
66
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,66 +0,0 @@
|
|||||||
name: Bug/Crash Report
|
|
||||||
description: Create a bug or crash report for Vencord
|
|
||||||
labels: [bug]
|
|
||||||
title: "[Bug] <title>"
|
|
||||||
|
|
||||||
body:
|
|
||||||
- type: input
|
|
||||||
id: discord
|
|
||||||
attributes:
|
|
||||||
label: Discord Account
|
|
||||||
description: Who on Discord is making this request? Not required but encouraged for easier follow-up
|
|
||||||
placeholder: username#0000
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: bug-description
|
|
||||||
attributes:
|
|
||||||
label: What happens when the bug or crash occurs?
|
|
||||||
description: Where does this bug or crash occur, when does it occur, etc.
|
|
||||||
placeholder: The bug/crash happens sometimes when I do ..., causing this to not work/the app to crash. I think it happens because of ...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: expected-behaviour
|
|
||||||
attributes:
|
|
||||||
label: What is the expected behaviour?
|
|
||||||
description: Simply detail what the expected behaviour is.
|
|
||||||
placeholder: I expect Vencord/Discord to open the ... page instead of ..., it prevents me from doing ...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: steps-to-take
|
|
||||||
attributes:
|
|
||||||
label: How do you recreate this bug or crash?
|
|
||||||
description: Give us a list of steps in order to recreate the bug or crash.
|
|
||||||
placeholder: |
|
|
||||||
1. Do ...
|
|
||||||
2. Then ...
|
|
||||||
3. Do this ..., ... and then ...
|
|
||||||
4. Observe "the bug" or "the crash"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: crash-log
|
|
||||||
attributes:
|
|
||||||
label: Errors
|
|
||||||
description: Open the Developer Console with Ctrl/Cmd + Shift + i. Then look for any red errors (Ignore network errors like Failed to load resource) and paste them between the "```".
|
|
||||||
value: |
|
|
||||||
```
|
|
||||||
Replace this text with your crash-log.
|
|
||||||
```
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: agreement-check
|
|
||||||
attributes:
|
|
||||||
label: Request Agreement
|
|
||||||
description: We only accept reports for bugs that happen on Discord Stable. Canary and PTB are Development branches and may be unstable
|
|
||||||
options:
|
|
||||||
- label: I am using Discord Stable or tried on Stable and this bug happens there as well
|
|
||||||
required: true
|
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +0,0 @@
|
|||||||
blank_issues_enabled: true
|
|
||||||
contact_links:
|
|
||||||
- name: Vencord Support Server
|
|
||||||
url: https://discord.gg/D9uwnFnqmd
|
|
||||||
about: If you need help regarding Vencord, please join our support server!
|
|
||||||
- name: Vencord Installer
|
|
||||||
url: https://github.com/Vencord/Installer
|
|
||||||
about: You can find the Vencord Installer here
|
|
32
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
32
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -1,32 +0,0 @@
|
|||||||
name: Feature Request
|
|
||||||
description: Create a feature request for Vencord. To request new plugins, please use the Discussions tab
|
|
||||||
labels: [enhancement]
|
|
||||||
title: "[Feature Request] <title>"
|
|
||||||
|
|
||||||
body:
|
|
||||||
- type: input
|
|
||||||
id: discord
|
|
||||||
attributes:
|
|
||||||
label: Discord Account
|
|
||||||
description: Who on Discord is making this request? Not required but encouraged for easier follow-up
|
|
||||||
placeholder: username#0000
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: feature-basic-description
|
|
||||||
attributes:
|
|
||||||
label: What is it that you'd like to see?
|
|
||||||
description: Describe the feature you want added as detailed as possible
|
|
||||||
placeholder: I think ... would be a cool feature to add. This would be awesome, thanks!
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: agreement-check
|
|
||||||
attributes:
|
|
||||||
label: Request Agreement
|
|
||||||
description: DO NOT USE THIS TEMPLATE FOR PLUGIN REQUESTS!!! For plugin requests, **use discussions**
|
|
||||||
options:
|
|
||||||
- label: This is not a plugin request
|
|
||||||
required: true
|
|
68
.github/workflows/build.yml
vendored
68
.github/workflows/build.yml
vendored
@ -1,15 +1,8 @@
|
|||||||
name: Build DevBuild
|
name: Build latest
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
|
||||||
- .github/workflows/build.yml
|
|
||||||
- src/**
|
|
||||||
- browser/**
|
|
||||||
- scripts/build/**
|
|
||||||
- package.json
|
|
||||||
- pnpm-lock.yaml
|
|
||||||
env:
|
env:
|
||||||
FORCE_COLOR: true
|
FORCE_COLOR: true
|
||||||
|
|
||||||
@ -22,59 +15,42 @@ jobs:
|
|||||||
|
|
||||||
- uses: pnpm/action-setup@v2 # 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
|
- name: Use Node.js 18
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 19
|
node-version: 18
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build web
|
- name: Build web
|
||||||
run: pnpm buildWeb --standalone
|
run: pnpm buildWeb
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build --standalone
|
run: pnpm build --standalone
|
||||||
|
|
||||||
- name: Generate plugin list
|
|
||||||
run: pnpm generatePluginJson dist/plugins.json
|
|
||||||
|
|
||||||
- name: Clean up obsolete files
|
|
||||||
run: |
|
|
||||||
rm -rf dist/*-unpacked Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
|
|
||||||
|
|
||||||
- name: Get some values needed for the release
|
- name: Get some values needed for the release
|
||||||
id: release_values
|
id: vars
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
- name: Upload DevBuild as release
|
- uses: dev-drprasad/delete-tag-and-release@085c6969f18bad0de1b9f3fe6692a3cd01f64fe5 # v0.2.0
|
||||||
if: github.repository == 'Vendicated/Vencord'
|
with:
|
||||||
run: |
|
delete_release: true
|
||||||
gh release upload devbuild --clobber dist/*
|
tag_name: devbuild
|
||||||
gh release edit devbuild --title "DevBuild $RELEASE_TAG"
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
RELEASE_TAG: ${{ env.release_tag }}
|
|
||||||
|
|
||||||
- name: Upload DevBuild to builds repo
|
- name: Create the release
|
||||||
if: github.repository == 'Vendicated/Vencord'
|
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v1
|
||||||
run: |
|
|
||||||
git config --global user.name "$USERNAME"
|
|
||||||
git config --global user.email actions@github.com
|
|
||||||
|
|
||||||
git clone https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git upload
|
|
||||||
cd upload
|
|
||||||
|
|
||||||
GLOBIGNORE=.git:.gitignore:README.md:LICENSE
|
|
||||||
rm -rf *
|
|
||||||
cp -r ../dist/* .
|
|
||||||
|
|
||||||
git add -A
|
|
||||||
git commit -m "Builds for https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA"
|
|
||||||
git push --force https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git
|
|
||||||
env:
|
env:
|
||||||
API_TOKEN: ${{ secrets.BUILDS_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GH_REPO: Vencord/builds
|
with:
|
||||||
USERNAME: GitHub-Actions
|
tag_name: devbuild
|
||||||
|
name: Dev Build ${{ steps.vars.outputs.sha_short }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
files: |
|
||||||
|
dist/*
|
||||||
|
22
.github/workflows/codeberg-mirror.yml
vendored
22
.github/workflows/codeberg-mirror.yml
vendored
@ -1,22 +0,0 @@
|
|||||||
name: Sync to Codeberg
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 */6 * * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
codeberg:
|
|
||||||
if: github.repository == 'Vendicated/Vencord'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- 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:Ven/cord.git"
|
|
||||||
ssh_private_key: ${{ secrets.CODEBERG_SSH_PRIVATE_KEY }}
|
|
61
.github/workflows/publish.yml
vendored
61
.github/workflows/publish.yml
vendored
@ -1,61 +0,0 @@
|
|||||||
name: Release Browser Extension
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
Publish:
|
|
||||||
if: github.repository == 'Vendicated/Vencord'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: check that tag matches package.json version
|
|
||||||
run: |
|
|
||||||
pkg_version="v$(jq -r .version < package.json)"
|
|
||||||
if [[ "${{ github.ref_name }}" != "$pkg_version" ]]; then
|
|
||||||
echo "Tag ${{ github.ref_name }} does not match package.json version $pkg_version" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
|
||||||
|
|
||||||
- name: Use Node.js 19
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 19
|
|
||||||
cache: "pnpm"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build web
|
|
||||||
run: pnpm buildWeb --standalone
|
|
||||||
|
|
||||||
- name: Publish extension
|
|
||||||
run: |
|
|
||||||
# Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later
|
|
||||||
EXIT_CODE=0
|
|
||||||
|
|
||||||
# Chrome
|
|
||||||
cd dist/chromium-unpacked
|
|
||||||
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$?
|
|
||||||
|
|
||||||
# Firefox
|
|
||||||
cd ../firefox-unpacked
|
|
||||||
npm i -g web-ext@7.4.0 web-ext-submit@7.4.0
|
|
||||||
web-ext-submit || EXIT_CODE=$?
|
|
||||||
|
|
||||||
exit $EXIT_CODE
|
|
||||||
env:
|
|
||||||
# Chrome
|
|
||||||
EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
|
|
||||||
CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
|
|
||||||
CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
|
|
||||||
REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
|
||||||
|
|
||||||
# Firefox
|
|
||||||
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
|
|
||||||
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}
|
|
58
.github/workflows/reportBrokenPlugins.yml
vendored
58
.github/workflows/reportBrokenPlugins.yml
vendored
@ -1,58 +0,0 @@
|
|||||||
name: Test Patches
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
# Every day at midnight
|
|
||||||
- cron: 0 0 * * *
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
TestPlugins:
|
|
||||||
if: github.repository == 'Vendicated/Vencord'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
|
||||||
|
|
||||||
- name: Use Node.js 19
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 19
|
|
||||||
cache: "pnpm"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
pnpm add puppeteer
|
|
||||||
|
|
||||||
sudo apt-get install -y chromium-browser
|
|
||||||
|
|
||||||
- name: Build web
|
|
||||||
run: pnpm buildWeb --standalone
|
|
||||||
|
|
||||||
- name: Create Report
|
|
||||||
timeout-minutes: 10
|
|
||||||
run: |
|
|
||||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
|
||||||
export CHROMIUM_BIN=$(which chromium-browser)
|
|
||||||
|
|
||||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
|
||||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
|
||||||
env:
|
|
||||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
|
||||||
|
|
||||||
- name: Create Report (Canary)
|
|
||||||
timeout-minutes: 10
|
|
||||||
if: success() || failure() # even run if previous one failed
|
|
||||||
run: |
|
|
||||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
|
||||||
export CHROMIUM_BIN=$(which chromium-browser)
|
|
||||||
export USE_CANARY=true
|
|
||||||
|
|
||||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
|
||||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
|
||||||
env:
|
|
||||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
|
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v2 # 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 18
|
- name: Use Node.js 18
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
@ -23,11 +23,5 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Lint & Test if desktop version compiles
|
- name: Lint & Test if it compiles
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|
||||||
- name: Test if web version compiles
|
|
||||||
run: pnpm buildWeb
|
|
||||||
|
|
||||||
- name: Test if plugin structure is valid
|
|
||||||
run: pnpm generatePluginJson
|
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -5,7 +5,6 @@ node_modules
|
|||||||
vencord_installer
|
vencord_installer
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
yarn.lock
|
yarn.lock
|
||||||
package-lock.json
|
package-lock.json
|
||||||
@ -19,6 +18,3 @@ lerna-debug.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
src/userplugins
|
src/userplugins
|
||||||
|
|
||||||
ExtensionCache/
|
|
||||||
settings/
|
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "stylelint-config-standard",
|
|
||||||
"rules": {
|
|
||||||
"indentation": 4
|
|
||||||
}
|
|
||||||
}
|
|
10
.vscode/extensions.json
vendored
10
.vscode/extensions.json
vendored
@ -1,11 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [ "EditorConfig.EditorConfig" ]
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"eamodio.gitlens",
|
|
||||||
"EditorConfig.EditorConfig",
|
|
||||||
"ExodiusStudios.comment-anchors",
|
|
||||||
"formulahendry.auto-rename-tag",
|
|
||||||
"GregorBiswanger.json2ts",
|
|
||||||
"stylelint.vscode-stylelint"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
37
.vscode/launch.json
vendored
37
.vscode/launch.json
vendored
@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
// this allows you to debug Vencord from VSCode.
|
|
||||||
// How to use:
|
|
||||||
// You need to run Discord via the command line to pass some flags to it.
|
|
||||||
// If you want to debug the main (node.js) process (preload.ts, ipcMain/*, patcher.ts),
|
|
||||||
// add the --inspect flag
|
|
||||||
// To debug the renderer (99% of Vencord), add the --remote-debugging-port=9223 flag
|
|
||||||
//
|
|
||||||
// Now launch the desired configuration in VSCode and start Discord with the flags.
|
|
||||||
// For example, to debug both process, run Electron: All then launch Discord with
|
|
||||||
// discord --remote-debugging-port=9223 --inspect
|
|
||||||
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Electron: Main",
|
|
||||||
"type": "node",
|
|
||||||
"request": "attach",
|
|
||||||
"port": 9229,
|
|
||||||
"timeout": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Electron: Renderer",
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "attach",
|
|
||||||
"port": 9223,
|
|
||||||
"timeout": 30000,
|
|
||||||
"webRoot": "${workspaceFolder}/src"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"compounds": [
|
|
||||||
{
|
|
||||||
"name": "Electron: All",
|
|
||||||
"configurations": ["Electron: Main", "Electron: Renderer"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@ -12,12 +12,5 @@
|
|||||||
"javascript.format.semicolons": "insert",
|
"javascript.format.semicolons": "insert",
|
||||||
"typescript.format.semicolons": "insert",
|
"typescript.format.semicolons": "insert",
|
||||||
"typescript.preferences.quoteStyle": "double",
|
"typescript.preferences.quoteStyle": "double",
|
||||||
"javascript.preferences.quoteStyle": "double",
|
"javascript.preferences.quoteStyle": "double"
|
||||||
|
|
||||||
"gitlens.remotes": [
|
|
||||||
{
|
|
||||||
"domain": "codeberg.org",
|
|
||||||
"type": "Gitea"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
25
.vscode/tasks.json
vendored
25
.vscode/tasks.json
vendored
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
|
||||||
// for the documentation about the tasks.json format
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "Build",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "pnpm build",
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Watch",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "pnpm watch",
|
|
||||||
"problemMatcher": [],
|
|
||||||
"group": {
|
|
||||||
"kind": "build"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
# Code of Conduct
|
|
||||||
|
|
||||||
Our community is welcoming to everyone, regardless of their characteristics.
|
|
||||||
|
|
||||||
As such, we expect you to treat everyone with respect and contribute to an open and welcoming community.
|
|
||||||
|
|
||||||
DO
|
|
||||||
- have empathy and be nice to others
|
|
||||||
- be respectful of differing opinions, even if you disagree
|
|
||||||
- give and accept constructive criticism
|
|
||||||
|
|
||||||
DON'T
|
|
||||||
- use offensive or derogatory language
|
|
||||||
- troll or spam
|
|
||||||
- personally attack or harass others
|
|
||||||
|
|
||||||
Repetitive violations of these guidelines might get your access to the repository restricted.
|
|
||||||
|
|
||||||
|
|
||||||
If you feel like a user is violating these guidelines or feel treated unfairly, please refrain from publicly challenging them and instead contact a Moderator on our Discord server or send an email to vendicated+conduct@riseup.net!
|
|
83
README.md
83
README.md
@ -1,80 +1,49 @@
|
|||||||
# Vencord
|
# Vencord
|
||||||
|
|
||||||
[![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)
|
A Discord client mod that does things differently
|
||||||
|
|
||||||
The cutest Discord client mod
|
|
||||||
|
|
||||||
![](https://user-images.githubusercontent.com/45497981/235015332-0453d3eb-1da6-4601-963e-ef5e454123a1.png)
|
|
||||||
*A screenshot of Vencord featuring the [ClearVision-v6 theme](https://github.com/ClearVision/ClearVision-v6) (Vencord does not come with it pre-installed, it is only an example)*
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Super easy to install (Download Installer, open, click install button, done)
|
- Works on Discord's latest update that breaks all other mods
|
||||||
- 100+ plugins built in: [See a list](https://vencord.dev/plugins)
|
- Browser Support (experimental): Run Vencord in your Browser instead of the desktop app
|
||||||
- Some highlights: SpotifyControls, MessageLogger, Experiments, GameActivityToggle, Translate, NoTrack, QuickReply, Free Emotes/Stickers, PermissionsViewer, CustomCommands, ShowHiddenChannels, PronounDB
|
- Custom Css and Themes: Manually edit `%appdata%/Vencord/settings/quickCss.css` / `~/.config/Vencord/settings/quickCss.css` with your favourite editor and the client will automatically apply your changes. To import BetterDiscord themes, just add `@import url(theUrl)` on the top of this file. (Make sure the url is a github raw URL or similar and only contains plain text, and NOT a nice looking website)
|
||||||
- Fairly lightweight despite the many inbuilt plugins
|
- Many Useful™ plugins - [List](https://github.com/Vendicated/Vencord/tree/main/src/plugins)
|
||||||
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
- Experiments
|
||||||
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)
|
- Proper context isolation -> Works in newer Electron versions (Confirmed working on versions 13-22)
|
||||||
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
- Inline patches: Patch Discord's code with regex replacements! See [the experiments plugin](src/plugins/experiments.ts) for an example. While being more complex, this is more powerful than monkey patching since you can patch only small parts of functions instead of fully replacing them, access non exported/local variables and even replace constants (like in the aforementioned experiments patch!)
|
||||||
- Privacy friendly, blocks Discord analytics & crash reporting out of the box and has no telemetry
|
|
||||||
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
|
||||||
- Settings sync: Keep your plugins and their settings synchronised between devices / apps (optional)
|
|
||||||
|
|
||||||
|
|
||||||
## Installing / Uninstalling
|
## Installing / Uninstalling
|
||||||
|
|
||||||
Click the below button to install Vencord to the Discord Desktop app
|
Read [Megu's Installation Guide!](docs/1_INSTALLING.md)
|
||||||
|
|
||||||
[![Download and run the Installer](https://img.shields.io/github/v/release/Vencord/Installer?label=Download%20Vencord%20Installer&style=for-the-badge)](https://github.com/Vencord/Installer#vencord-installer)
|
|
||||||
|
|
||||||
## Installing on Browser
|
## Installing on Browser
|
||||||
|
|
||||||
[![Get it on the Firefox Webstore](https://blog.mozilla.org/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [![Get it on the Chrome Webstore](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
|
Run the same commands as in the regular install method. Now run
|
||||||
|
|
||||||
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that the CSS Editor, Themes loaded from remote sources and co. will not work in the UserScript. Use the extension if you need any of those
|
```sh
|
||||||
|
pnpm buildWeb
|
||||||
|
```
|
||||||
|
|
||||||
<details>
|
You will find the built extension at dist/extension.zip. Now just install this extension in your Browser
|
||||||
<summary>Alternative Downloads</summary>
|
|
||||||
|
|
||||||
## Vencord Desktop
|
## Installing Plugins
|
||||||
|
|
||||||
> **Warning**
|
Vencord comes with a bunch of plugins out of the box!
|
||||||
> This is an alternative app. It currently doesn't support keybinds and possibly some more features. If you just want to install to the normal Discord Desktop app, scroll up
|
However, if you want to install your own ones, create a `userplugins` folder in the `src` directory and create or clone your plugins in there.
|
||||||
|
Don't forget to rebuild!
|
||||||
|
|
||||||
As an alternative to the Discord Desktop app, Vencord also has its own standalone Desktop app that is snappier and lighter than Discord's official Desktop app
|
Want to learn how to create your own plugin, and maybe PR it into Vencord? See the [Contributing](#contributing) section below!
|
||||||
|
|
||||||
[![Download Vencord Desktop](https://img.shields.io/github/v/release/Vencord/Desktop?label=Download%20Vencord%20Desktop&style=for-the-badge)](https://github.com/Vencord/Desktop#vencord-desktop)
|
## Contributing
|
||||||
|
|
||||||
</details>
|
See [CONTRIBUTING.md](CONTRIBUTING.md) and [Megu's Plugin Guide!](docs/2_PLUGINS.md)
|
||||||
|
|
||||||
## Join our Support/Community Server
|
[contribute]: CONTRIBUTING.md
|
||||||
|
|
||||||
https://discord.gg/D9uwnFnqmd
|
[contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute]
|
||||||
|
|
||||||
## Star History
|
## Join
|
||||||
|
|
||||||
<a href="https://star-history.com/#Vendicated/Vencord&Timeline">
|
[join]: https://discord.gg/D9uwnFnqmd
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Vendicated/Vencord&type=Timeline&theme=dark" />
|
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Vendicated/Vencord&type=Timeline" />
|
|
||||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Vendicated/Vencord&type=Timeline" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
## Disclaimer
|
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
|
||||||
|
|
||||||
Discord is trademark of Discord Inc. and solely mentioned for the sake of descriptivity.
|
|
||||||
Mention of it does not imply any affiliation with or endorsement by Discord Inc.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Using Vencord violates Discord's terms of service</summary>
|
|
||||||
|
|
||||||
Client modifications are against Discord’s Terms of Service.
|
|
||||||
|
|
||||||
However, Discord is pretty indifferent about them and there are no known cases of users getting banned for using client mods! So you should generally be fine as long as you don’t use any plugins that implement abusive behaviour. But no worries, all inbuilt plugins are safe to use!
|
|
||||||
|
|
||||||
Regardless, if your account is very important to you and it getting disabled would be a disaster for you, you should probably not use any client mods (not exclusive to Vencord), just to be safe
|
|
||||||
|
|
||||||
Additionally, make sure not to post screenshots with Vencord in a server where you might get banned for it
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function parseHeaders(headers) {
|
|
||||||
if (!headers)
|
|
||||||
return {};
|
|
||||||
const result = {};
|
|
||||||
const headersArr = headers.trim().split("\n");
|
|
||||||
for (var i = 0; i < headersArr.length; i++) {
|
|
||||||
var row = headersArr[i];
|
|
||||||
var index = row.indexOf(":")
|
|
||||||
, key = row.slice(0, index).trim().toLowerCase()
|
|
||||||
, value = row.slice(index + 1).trim();
|
|
||||||
|
|
||||||
if (result[key] === undefined) {
|
|
||||||
result[key] = value;
|
|
||||||
} else if (Array.isArray(result[key])) {
|
|
||||||
result[key].push(value);
|
|
||||||
} else {
|
|
||||||
result[key] = [result[key], value];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function blobTo(to, blob) {
|
|
||||||
if (to === "arrayBuffer" && blob.arrayBuffer) return blob.arrayBuffer();
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
var fileReader = new FileReader();
|
|
||||||
fileReader.onload = event => resolve(event.target.result);
|
|
||||||
if (to === "arrayBuffer") fileReader.readAsArrayBuffer(blob);
|
|
||||||
else if (to === "text") fileReader.readAsText(blob, "utf-8");
|
|
||||||
else reject("unknown to");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function GM_fetch(url, opt) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest
|
|
||||||
const options = opt || {};
|
|
||||||
options.url = url;
|
|
||||||
options.data = options.body;
|
|
||||||
options.responseType = "blob";
|
|
||||||
options.onload = resp => {
|
|
||||||
var blob = resp.response;
|
|
||||||
resp.blob = () => Promise.resolve(blob);
|
|
||||||
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
|
|
||||||
resp.text = () => blobTo("text", blob);
|
|
||||||
resp.json = async () => JSON.parse(await blobTo("text", blob));
|
|
||||||
resp.headers = new Headers(parseHeaders(resp.responseHeaders));
|
|
||||||
resp.ok = resp.status >= 200 && resp.status < 300;
|
|
||||||
resolve(resp);
|
|
||||||
};
|
|
||||||
options.ontimeout = () => reject("fetch timeout");
|
|
||||||
options.onerror = () => reject("fetch error");
|
|
||||||
options.onabort = () => reject("fetch abort");
|
|
||||||
GM_xmlhttpRequest(options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
export const fetch = GM_fetch;
|
|
@ -16,86 +16,51 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/// <reference path="../src/modules.d.ts" />
|
import IpcEvents from "../src/utils/IpcEvents";
|
||||||
/// <reference path="../src/globals.d.ts" />
|
|
||||||
|
|
||||||
import monacoHtml from "~fileContent/../src/components/monacoWin.html";
|
|
||||||
import * as DataStore from "../src/api/DataStore";
|
import * as DataStore from "../src/api/DataStore";
|
||||||
import { debounce } from "../src/utils";
|
|
||||||
import { getTheme, Theme } from "../src/utils/discord";
|
|
||||||
import { getThemeInfo } from "../src/main/themes";
|
|
||||||
|
|
||||||
// Discord deletes this so need to store in variable
|
// Discord deletes this so need to store in variable
|
||||||
const { localStorage } = window;
|
const { localStorage } = window;
|
||||||
|
|
||||||
// listeners for ipc.on
|
// listeners for ipc.on
|
||||||
const cssListeners = new Set<(css: string) => void>();
|
const listeners = {} as Record<string, Set<Function>>;
|
||||||
const NOOP = () => { };
|
|
||||||
const NOOP_ASYNC = async () => { };
|
|
||||||
|
|
||||||
const setCssDebounced = debounce((css: string) => VencordNative.quickCss.set(css));
|
const handlers = {
|
||||||
|
[IpcEvents.GET_REPO]: () => "https://github.com/Vendicated/Vencord", // shrug
|
||||||
|
[IpcEvents.GET_SETTINGS_DIR]: () => "LocalStorage",
|
||||||
|
|
||||||
const themeStore = DataStore.createStore("VencordThemes", "VencordThemeData");
|
[IpcEvents.GET_QUICK_CSS]: () => DataStore.get("VencordQuickCss").then(s => s ?? ""),
|
||||||
|
[IpcEvents.SET_QUICK_CSS]: (css: string) => {
|
||||||
|
DataStore.set("VencordQuickCss", css);
|
||||||
|
listeners[IpcEvents.QUICK_CSS_UPDATE]?.forEach(l => l(null, css));
|
||||||
|
},
|
||||||
|
|
||||||
|
[IpcEvents.GET_SETTINGS]: () => localStorage.getItem("VencordSettings") || "{}",
|
||||||
|
[IpcEvents.SET_SETTINGS]: (s: string) => localStorage.setItem("VencordSettings", s),
|
||||||
|
|
||||||
|
[IpcEvents.GET_UPDATES]: () => ({ ok: true, value: [] }),
|
||||||
|
|
||||||
|
[IpcEvents.OPEN_EXTERNAL]: (url: string) => open(url, "_blank"),
|
||||||
|
};
|
||||||
|
|
||||||
|
function onEvent(event: string, ...args: any[]) {
|
||||||
|
const handler = handlers[event];
|
||||||
|
if (!handler) throw new Error(`Event ${event} not implemented.`);
|
||||||
|
return handler(...args);
|
||||||
|
}
|
||||||
|
|
||||||
// probably should make this less cursed at some point
|
// probably should make this less cursed at some point
|
||||||
window.VencordNative = {
|
window.VencordNative = {
|
||||||
themes: {
|
getVersions: () => ({}),
|
||||||
uploadTheme: (fileName: string, fileData: string) => DataStore.set(fileName, fileData, themeStore),
|
ipc: {
|
||||||
deleteTheme: (fileName: string) => DataStore.del(fileName, themeStore),
|
send: (event: string, ...args: any[]) => void onEvent(event, ...args),
|
||||||
getThemesDir: async () => "",
|
sendSync: onEvent,
|
||||||
getThemesList: () => DataStore.entries(themeStore).then(entries =>
|
on(event: string, listener: () => {}) {
|
||||||
entries.map(([name, css]) => getThemeInfo(css, name.toString()))
|
(listeners[event] ??= new Set()).add(listener);
|
||||||
),
|
|
||||||
getThemeData: (fileName: string) => DataStore.get(fileName, themeStore)
|
|
||||||
},
|
|
||||||
|
|
||||||
native: {
|
|
||||||
getVersions: () => ({}),
|
|
||||||
openExternal: async (url) => void open(url, "_blank")
|
|
||||||
},
|
|
||||||
|
|
||||||
updater: {
|
|
||||||
getRepo: async () => ({ ok: true, value: "https://github.com/Vendicated/Vencord" }),
|
|
||||||
getUpdates: async () => ({ ok: true, value: [] }),
|
|
||||||
update: async () => ({ ok: true, value: false }),
|
|
||||||
rebuild: async () => ({ ok: true, value: true }),
|
|
||||||
},
|
|
||||||
|
|
||||||
quickCss: {
|
|
||||||
get: () => DataStore.get("VencordQuickCss").then(s => s ?? ""),
|
|
||||||
set: async (css: string) => {
|
|
||||||
await DataStore.set("VencordQuickCss", css);
|
|
||||||
cssListeners.forEach(l => l(css));
|
|
||||||
},
|
},
|
||||||
addChangeListener(cb) {
|
off(event: string, listener: () => {}) {
|
||||||
cssListeners.add(cb);
|
return listeners[event]?.delete(listener);
|
||||||
},
|
|
||||||
addThemeChangeListener: NOOP,
|
|
||||||
openFile: NOOP_ASYNC,
|
|
||||||
async openEditor() {
|
|
||||||
const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
|
|
||||||
const win = open("about:blank", "VencordQuickCss", features);
|
|
||||||
if (!win) {
|
|
||||||
alert("Failed to open QuickCSS popup. Make sure to allow popups!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
win.setCss = setCssDebounced;
|
|
||||||
win.getCurrentCss = () => VencordNative.quickCss.get();
|
|
||||||
win.getTheme = () =>
|
|
||||||
getTheme() === Theme.Light
|
|
||||||
? "vs-light"
|
|
||||||
: "vs-dark";
|
|
||||||
|
|
||||||
win.document.write(monacoHtml);
|
|
||||||
},
|
},
|
||||||
|
invoke: (event: string, ...args: any[]) => Promise.resolve(onEvent(event, ...args))
|
||||||
},
|
},
|
||||||
|
|
||||||
settings: {
|
|
||||||
get: () => localStorage.getItem("VencordSettings") || "{}",
|
|
||||||
set: async (s: string) => localStorage.setItem("VencordSettings", s),
|
|
||||||
getSettingsDir: async () => "LocalStorage"
|
|
||||||
},
|
|
||||||
|
|
||||||
pluginHelpers: {} as any,
|
|
||||||
};
|
};
|
||||||
|
@ -1,32 +1,24 @@
|
|||||||
/**
|
if (typeof browser === "undefined") {
|
||||||
* @template T
|
var browser = chrome;
|
||||||
* @param {T[]} arr
|
|
||||||
* @param {(v: T) => boolean} predicate
|
|
||||||
*/
|
|
||||||
function removeFirst(arr, predicate) {
|
|
||||||
const idx = arr.findIndex(predicate);
|
|
||||||
if (idx !== -1) arr.splice(idx, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.webRequest.onHeadersReceived.addListener(
|
browser.webRequest.onHeadersReceived.addListener(({ responseHeaders, url }) => {
|
||||||
({ responseHeaders, type, url }) => {
|
const cspIdx = responseHeaders.findIndex(h => h.name === "content-security-policy");
|
||||||
if (!responseHeaders) return;
|
if (cspIdx !== -1)
|
||||||
|
responseHeaders.splice(cspIdx, 1);
|
||||||
|
|
||||||
if (type === "main_frame") {
|
if (url.endsWith(".css")) {
|
||||||
// In main frame requests, the CSP needs to be removed to enable fetching of custom css
|
const contentType = responseHeaders.find(h => h.name === "content-type");
|
||||||
// as desired by the user
|
if (contentType)
|
||||||
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-security-policy");
|
contentType.value = "text/css";
|
||||||
} else if (type === "stylesheet" && url.startsWith("https://raw.githubusercontent.com/")) {
|
else
|
||||||
// Most users will load css from GitHub, but GitHub doesn't set the correct content type,
|
|
||||||
// so we fix it here
|
|
||||||
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-type");
|
|
||||||
responseHeaders.push({
|
responseHeaders.push({
|
||||||
name: "Content-Type",
|
name: "content-type",
|
||||||
value: "text/css"
|
value: "text/json"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { responseHeaders };
|
|
||||||
},
|
return {
|
||||||
{ urls: ["https://raw.githubusercontent.com/*", "*://*.discord.com/*"], types: ["main_frame", "stylesheet"] },
|
responseHeaders
|
||||||
["blocking", "responseHeaders"]
|
};
|
||||||
);
|
}, { urls: ["*://*.discord.com/*"] }, ["blocking", "responseHeaders"]);
|
||||||
|
@ -2,18 +2,7 @@ if (typeof browser === "undefined") {
|
|||||||
var browser = chrome;
|
var browser = chrome;
|
||||||
}
|
}
|
||||||
|
|
||||||
const script = document.createElement("script");
|
var script = document.createElement("script");
|
||||||
script.src = browser.runtime.getURL("dist/Vencord.js");
|
script.src = browser.runtime.getURL("dist/Vencord.js");
|
||||||
|
// documentElement because we load before body/head are ready
|
||||||
const style = document.createElement("link");
|
document.documentElement.appendChild(script);
|
||||||
style.type = "text/css";
|
|
||||||
style.rel = "stylesheet";
|
|
||||||
style.href = browser.runtime.getURL("dist/Vencord.css");
|
|
||||||
|
|
||||||
document.documentElement.append(script);
|
|
||||||
|
|
||||||
document.addEventListener(
|
|
||||||
"DOMContentLoaded",
|
|
||||||
() => document.documentElement.append(style),
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
|
BIN
browser/icon.png
BIN
browser/icon.png
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
@ -1,45 +1,32 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 2,
|
||||||
"minimum_chrome_version": "91",
|
|
||||||
|
|
||||||
"name": "Vencord Web",
|
"name": "Vencord Web",
|
||||||
"description": "The cutest Discord mod now in your browser",
|
"description": "Yeee",
|
||||||
|
"version": "1.0.0",
|
||||||
"author": "Vendicated",
|
"author": "Vendicated",
|
||||||
"homepage_url": "https://github.com/Vendicated/Vencord",
|
"homepage_url": "https://github.com/Vendicated/Vencord",
|
||||||
"icons": {
|
"background": {
|
||||||
"128": "icon.png"
|
"scripts": [
|
||||||
|
"background.js"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
"host_permissions": [
|
|
||||||
"*://*.discord.com/*",
|
|
||||||
"https://raw.githubusercontent.com/*"
|
|
||||||
],
|
|
||||||
|
|
||||||
"permissions": ["declarativeNetRequest"],
|
|
||||||
|
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"matches": ["*://*.discord.com/*"],
|
"matches": [
|
||||||
"js": ["content.js"],
|
"*://*.discord.com/*"
|
||||||
"all_frames": true
|
],
|
||||||
|
"js": [
|
||||||
|
"content.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"permissions": [
|
||||||
|
"*://*.discord.com/*",
|
||||||
|
"webRequest",
|
||||||
|
"webRequestBlocking"
|
||||||
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
"dist/Vencord.js"
|
||||||
"resources": ["dist/Vencord.js", "dist/Vencord.css"],
|
]
|
||||||
"matches": ["*://*.discord.com/*"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"declarative_net_request": {
|
|
||||||
"rule_resources": [
|
|
||||||
{
|
|
||||||
"id": "modifyResponseHeaders",
|
|
||||||
"enabled": true,
|
|
||||||
"path": "modifyResponseHeaders.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 2,
|
|
||||||
"minimum_chrome_version": "91",
|
|
||||||
|
|
||||||
"name": "Vencord Web",
|
|
||||||
"description": "The cutest Discord mod now in your browser",
|
|
||||||
"author": "Vendicated",
|
|
||||||
"homepage_url": "https://github.com/Vendicated/Vencord",
|
|
||||||
"icons": {
|
|
||||||
"128": "icon.png"
|
|
||||||
},
|
|
||||||
|
|
||||||
"permissions": [
|
|
||||||
"webRequest",
|
|
||||||
"webRequestBlocking",
|
|
||||||
"*://*.discord.com/*",
|
|
||||||
"https://raw.githubusercontent.com/*"
|
|
||||||
],
|
|
||||||
|
|
||||||
"content_scripts": [
|
|
||||||
{
|
|
||||||
"run_at": "document_start",
|
|
||||||
"matches": ["*://*.discord.com/*"],
|
|
||||||
"js": ["content.js"],
|
|
||||||
"all_frames": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"background": {
|
|
||||||
"scripts": ["background.js"]
|
|
||||||
},
|
|
||||||
|
|
||||||
"web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"],
|
|
||||||
|
|
||||||
"browser_specific_settings": {
|
|
||||||
"gecko": {
|
|
||||||
"id": "vencord-firefox@vendicated.dev",
|
|
||||||
"strict_min_version": "91.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"action": {
|
|
||||||
"type": "modifyHeaders",
|
|
||||||
"responseHeaders": [
|
|
||||||
{
|
|
||||||
"header": "content-security-policy",
|
|
||||||
"operation": "remove"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header": "content-security-policy-report-only",
|
|
||||||
"operation": "remove"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"condition": {
|
|
||||||
"resourceTypes": ["main_frame", "sub_frame"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"action": {
|
|
||||||
"type": "modifyHeaders",
|
|
||||||
"responseHeaders": [
|
|
||||||
{
|
|
||||||
"header": "content-type",
|
|
||||||
"operation": "set",
|
|
||||||
"value": "text/css"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"condition": {
|
|
||||||
"resourceTypes": ["stylesheet"],
|
|
||||||
"urlFilter": "https://raw.githubusercontent.com/*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
@ -7,7 +7,7 @@
|
|||||||
// @supportURL https://github.com/Vendicated/Vencord
|
// @supportURL https://github.com/Vendicated/Vencord
|
||||||
// @license GPL-3.0
|
// @license GPL-3.0
|
||||||
// @match *://*.discord.com/*
|
// @match *://*.discord.com/*
|
||||||
// @grant GM_xmlhttpRequest
|
// @grant none
|
||||||
// @run-at document-start
|
// @run-at document-start
|
||||||
// @compatible chrome Chrome + Tampermonkey or Violentmonkey
|
// @compatible chrome Chrome + Tampermonkey or Violentmonkey
|
||||||
// @compatible firefox Firefox Tampermonkey
|
// @compatible firefox Firefox Tampermonkey
|
||||||
|
3
build.mjs
Normal file
3
build.mjs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// FIXME: Delete this soon, for now it is needed so people can update
|
||||||
|
|
||||||
|
import("./scripts/build/build.mjs");
|
@ -1,7 +1,3 @@
|
|||||||
> [!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
|
# Installation Guide
|
||||||
|
|
||||||
Welcome to Megu's Installation Guide! In this file, you will learn about how to download, install, and uninstall Vencord!
|
Welcome to Megu's Installation Guide! In this file, you will learn about how to download, install, and uninstall Vencord!
|
||||||
@ -14,6 +10,12 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
|
|||||||
- [Installing Vencord](#installing-vencord)
|
- [Installing Vencord](#installing-vencord)
|
||||||
- [Updating Vencord](#updating-vencord)
|
- [Updating Vencord](#updating-vencord)
|
||||||
- [Uninstalling Vencord](#uninstalling-vencord)
|
- [Uninstalling Vencord](#uninstalling-vencord)
|
||||||
|
- [Manually Installing Vencord](#manually-installing-vencord)
|
||||||
|
- [On Windows](#on-windows)
|
||||||
|
- [On Linux](#on-linux)
|
||||||
|
- [On MacOS](#on-macos)
|
||||||
|
- [Manual Patching](#manual-patching)
|
||||||
|
- [Manually Uninstalling Vencord](#manually-uninstalling-vencord)
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@ -22,16 +24,16 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
|
|||||||
|
|
||||||
## Installing Vencord
|
## Installing Vencord
|
||||||
|
|
||||||
|
> :exclamation: If this doesn't work, see [Manually Installing Vencord](#manually-installing-vencord)
|
||||||
|
|
||||||
Install `pnpm`:
|
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.
|
> :exclamation: this may need to be run as admin depending on your system, and you may need to close and reopen your terminal.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm i -g pnpm
|
npm i -g pnpm
|
||||||
```
|
```
|
||||||
|
|
||||||
> :exclamation: **IMPORTANT** Make sure you aren't using an admin/root terminal from here onwards. It **will** mess up your Discord/Vencord instance and you **will** most likely have to reinstall.
|
|
||||||
|
|
||||||
Clone Vencord:
|
Clone Vencord:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@ -96,4 +98,103 @@ Simply run:
|
|||||||
pnpm uninject
|
pnpm uninject
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The above command may ask you to also run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm uninject
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manually Installing Vencord
|
||||||
|
|
||||||
|
- [Windows](#on-windows)
|
||||||
|
- [Linux](#on-linux)
|
||||||
|
- [MacOS](#on-macos)
|
||||||
|
|
||||||
|
### On Windows
|
||||||
|
|
||||||
|
Press Win+R and enter: `%LocalAppData%` and hit enter. In this page, find the page (Discord, DiscordPTB, DiscordCanary, etc) that you want to patch.
|
||||||
|
|
||||||
|
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||||
|
|
||||||
|
### On Linux
|
||||||
|
|
||||||
|
The Discord folder is usually in one of the following paths:
|
||||||
|
|
||||||
|
- /usr/share
|
||||||
|
- /usr/lib64
|
||||||
|
- /opt
|
||||||
|
- /home/$USER/.local/share
|
||||||
|
|
||||||
|
If you use flatpak, it will usually be in one of the following paths:
|
||||||
|
|
||||||
|
- /var/lib/flatpak/app/com.discordapp.Discord/current/active/files
|
||||||
|
- /home/$USER/.local/share/flatpak/app/com.discordapp.Discord/current/active/files
|
||||||
|
|
||||||
|
You will need to give flatpak access to vencord with one of the following commands:
|
||||||
|
|
||||||
|
> :exclamation: If not on stable, replace `com.discordapp.Discord` with your branch name, e.g., `com.discordapp.DiscordCanary`
|
||||||
|
|
||||||
|
> :exclamation: Replace `/path/to/vencord/` with the path to your vencord folder (NOT the dist folder)
|
||||||
|
|
||||||
|
If Discord flatpak install is in /home/:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
flatpak override --user com.discordapp.Discord --filesystem="/path/to/vencord/"
|
||||||
|
```
|
||||||
|
|
||||||
|
If Discord flatpak install not in /home/:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo flatpak override com.discordapp.Discord --filesystem="/path/to/vencord"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||||
|
|
||||||
|
### On MacOS
|
||||||
|
|
||||||
|
Open finder and go to your Applications folder. Right-Click on the Discord application you want to patch, and view contents.
|
||||||
|
|
||||||
|
Go to the `Contents/Resources` folder.
|
||||||
|
|
||||||
|
Now follow the instructions at [Manual Patching](#manual-patching)
|
||||||
|
|
||||||
|
### Manual Patching
|
||||||
|
|
||||||
|
> :exclamation: If using Flatpak on linux, go to the folder that contains the `app.asar` file, and skip to where we create the `app` folder below.
|
||||||
|
|
||||||
|
> :exclamation: On Linux/MacOS, there's a chance there won't be an `app-<number>` folder, but there probably is a `resources` folder, so keep reading :)
|
||||||
|
|
||||||
|
Inside there, look for the `app-<number>` folders. If you have multiple, use the highest number. If that doesn't work, do it for the rest of the `app-<number>` folders.
|
||||||
|
|
||||||
|
Inside there, go to the `resources` folder. There should be a file called `app.asar`. If there isn't, look at a different `app-<number>` folder instead.
|
||||||
|
|
||||||
|
Make a new folder in `resources` called `app`. In here, we will make two files:
|
||||||
|
|
||||||
|
`package.json` and `index.js`
|
||||||
|
|
||||||
|
In `index.js`:
|
||||||
|
|
||||||
|
> :exclamation: Replace the path in the first line with the path to `patcher.js` in your vencord dist folder.
|
||||||
|
> On Windows, you can get this by shift-rightclicking the patcher.js file and selecting "copy as path"
|
||||||
|
|
||||||
|
```js
|
||||||
|
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
|
||||||
|
require("../app.asar");
|
||||||
|
```
|
||||||
|
|
||||||
|
And in `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "discord", "main": "index.js" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, fully close & reopen your Discord client and check to see that `Vencord` appears in settings!
|
||||||
|
|
||||||
|
### Manually Uninstalling Vencord
|
||||||
|
|
||||||
|
> :exclamation: Do not delete `app.asar` - Only delete the `app` folder we created.
|
||||||
|
|
||||||
|
Use the instructions above to find the `app` folder, and delete it. Then Close & Reopen Discord.
|
||||||
|
|
||||||
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
||||||
|
@ -15,7 +15,7 @@ You don't need to run `pnpm build` every time you make a change. Instead, use `p
|
|||||||
3. In `index.ts`, copy-paste the following template code:
|
3. In `index.ts`, copy-paste the following template code:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "../../utils/types";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Epic Plugin",
|
name: "Epic Plugin",
|
||||||
|
88
package.json
88
package.json
@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.4.4",
|
"version": "1.0.0",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "A Discord client mod that does things differently",
|
||||||
|
"keywords": [],
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Vendicated/Vencord/issues"
|
"url": "https://github.com/Vendicated/Vencord/issues"
|
||||||
@ -11,7 +12,7 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/Vendicated/Vencord.git"
|
"url": "git+https://github.com/Vendicated/Vencord.git"
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0",
|
||||||
"author": "Vendicated",
|
"author": "Vendicated",
|
||||||
"directories": {
|
"directories": {
|
||||||
"doc": "docs"
|
"doc": "docs"
|
||||||
@ -19,81 +20,32 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build/build.mjs",
|
"build": "node scripts/build/build.mjs",
|
||||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||||
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
"inject": "node scripts/patcher/install.js",
|
||||||
"inject": "node scripts/runInstaller.mjs",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
|
|
||||||
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
|
||||||
"lint:fix": "pnpm lint --fix",
|
"lint:fix": "pnpm lint --fix",
|
||||||
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
|
"test": "pnpm lint && pnpm build && pnpm testTsc",
|
||||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
|
||||||
"testTsc": "tsc --noEmit",
|
"testTsc": "tsc --noEmit",
|
||||||
"uninject": "node scripts/runInstaller.mjs",
|
"uninject": "node scripts/patcher/uninstall.js",
|
||||||
"watch": "node scripts/build/build.mjs --watch"
|
"watch": "node scripts/build/build.mjs --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
"console-menu": "^0.1.0",
|
||||||
"@vap/core": "0.0.12",
|
"fflate": "^0.7.4"
|
||||||
"@vap/shiki": "0.10.5",
|
|
||||||
"eslint-plugin-simple-header": "^1.0.2",
|
|
||||||
"fflate": "^0.7.4",
|
|
||||||
"nanoid": "^4.0.2",
|
|
||||||
"virtual-merge": "^1.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/diff": "^5.0.3",
|
"@types/node": "^18.7.13",
|
||||||
"@types/lodash": "^4.14.194",
|
"@types/react": "^18.0.17",
|
||||||
"@types/node": "^18.16.3",
|
|
||||||
"@types/react": "^18.2.0",
|
|
||||||
"@types/react-dom": "^18.2.1",
|
|
||||||
"@types/yazl": "^2.4.2",
|
"@types/yazl": "^2.4.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
"@typescript-eslint/parser": "^5.39.0",
|
||||||
"@typescript-eslint/parser": "^5.59.1",
|
|
||||||
"diff": "^5.1.0",
|
|
||||||
"discord-types": "^1.3.26",
|
"discord-types": "^1.3.26",
|
||||||
"esbuild": "^0.15.18",
|
"esbuild": "^0.15.5",
|
||||||
"eslint": "^8.46.0",
|
"eslint": "^8.24.0",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-plugin-header": "^3.1.1",
|
||||||
"eslint-plugin-path-alias": "^1.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
|
||||||
"eslint-plugin-unused-imports": "^2.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",
|
"standalone-electron-types": "^1.0.0",
|
||||||
"stylelint": "^15.6.0",
|
"type-fest": "^3.1.0",
|
||||||
"stylelint-config-standard": "^33.0.0",
|
"typescript": "^4.8.4"
|
||||||
"tsx": "^3.12.7",
|
|
||||||
"type-fest": "^3.9.0",
|
|
||||||
"typescript": "^5.0.4"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.1.1",
|
"packageManager": "pnpm@7.13.4"
|
||||||
"pnpm": {
|
|
||||||
"patchedDependencies": {
|
|
||||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
|
||||||
"eslint@8.46.0": "patches/eslint@8.46.0.patch"
|
|
||||||
},
|
|
||||||
"peerDependencyRules": {
|
|
||||||
"ignoreMissing": [
|
|
||||||
"eslint-plugin-import",
|
|
||||||
"eslint"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"allowedDeprecatedVersions": {
|
|
||||||
"source-map-resolve": "*",
|
|
||||||
"resolve-url": "*",
|
|
||||||
"source-map-url": "*",
|
|
||||||
"urix": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"webExt": {
|
|
||||||
"artifactsDir": "./dist",
|
|
||||||
"build": {
|
|
||||||
"overwriteDest": true
|
|
||||||
},
|
|
||||||
"sourceDir": "./dist/extension-v2-unpacked"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18",
|
|
||||||
"pnpm": ">=8"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
diff --git a/lib/rules/no-relative.js b/lib/rules/no-relative.js
|
|
||||||
index 71594c83f1f4f733ffcc6047d7f7084348335dbe..d8623d87c89499c442171db3272cba07c9efabbe 100644
|
|
||||||
--- a/lib/rules/no-relative.js
|
|
||||||
+++ b/lib/rules/no-relative.js
|
|
||||||
@@ -41,7 +41,7 @@ module.exports = {
|
|
||||||
ImportDeclaration(node) {
|
|
||||||
const importPath = node.source.value;
|
|
||||||
|
|
||||||
- if (!/^(\.?\.\/)/.test(importPath)) {
|
|
||||||
+ if (!/^(\.\.\/)/.test(importPath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
|||||||
diff --git a/lib/rules/no-useless-escape.js b/lib/rules/no-useless-escape.js
|
|
||||||
index 0e0f6f09f2c35f3276173c08f832cde9f2cf56a0..7dc22851715f3574d935f513c1b5e35552985711 100644
|
|
||||||
--- a/lib/rules/no-useless-escape.js
|
|
||||||
+++ b/lib/rules/no-useless-escape.js
|
|
||||||
@@ -65,13 +65,31 @@ module.exports = {
|
|
||||||
escapeBackslash: "Replace the `\\` with `\\\\` to include the actual backslash character."
|
|
||||||
},
|
|
||||||
|
|
||||||
- schema: []
|
|
||||||
+ schema: [{
|
|
||||||
+ type: "object",
|
|
||||||
+ properties: {
|
|
||||||
+ extra: {
|
|
||||||
+ type: "string",
|
|
||||||
+ default: ""
|
|
||||||
+ },
|
|
||||||
+ extraCharClass: {
|
|
||||||
+ type: "string",
|
|
||||||
+ default: ""
|
|
||||||
+ },
|
|
||||||
+ },
|
|
||||||
+ additionalProperties: false
|
|
||||||
+ }]
|
|
||||||
},
|
|
||||||
|
|
||||||
create(context) {
|
|
||||||
+ const options = context.options[0] || {};
|
|
||||||
+ const { extra, extraCharClass } = options;
|
|
||||||
const sourceCode = context.sourceCode;
|
|
||||||
const parser = new RegExpParser();
|
|
||||||
|
|
||||||
+ const NON_CHARCLASS_ESCAPES = union(REGEX_NON_CHARCLASS_ESCAPES, new Set(extra));
|
|
||||||
+ const CHARCLASS_ESCAPES = union(REGEX_GENERAL_ESCAPES, new Set(extraCharClass));
|
|
||||||
+
|
|
||||||
/**
|
|
||||||
* Reports a node
|
|
||||||
* @param {ASTNode} node The node to report
|
|
||||||
@@ -200,9 +218,9 @@ module.exports = {
|
|
||||||
let allowedEscapes;
|
|
||||||
|
|
||||||
if (characterClassStack.length) {
|
|
||||||
- allowedEscapes = unicodeSets ? REGEX_CLASSSET_CHARACTER_ESCAPES : REGEX_GENERAL_ESCAPES;
|
|
||||||
+ allowedEscapes = unicodeSets ? REGEX_CLASSSET_CHARACTER_ESCAPES : CHARCLASS_ESCAPES;
|
|
||||||
} else {
|
|
||||||
- allowedEscapes = REGEX_NON_CHARCLASS_ESCAPES;
|
|
||||||
+ allowedEscapes = NON_CHARCLASS_ESCAPES;
|
|
||||||
}
|
|
||||||
if (allowedEscapes.has(escapedChar)) {
|
|
||||||
return;
|
|
2873
pnpm-lock.yaml
generated
2873
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -19,19 +19,22 @@
|
|||||||
|
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
|
|
||||||
import { commonOpts, globPlugins, isStandalone, VERSION, watch } from "./common.mjs";
|
import { commonOpts, gitHash, globPlugins, isStandalone } from "./common.mjs";
|
||||||
|
|
||||||
const defines = {
|
const defines = {
|
||||||
IS_STANDALONE: isStandalone,
|
IS_STANDALONE: isStandalone
|
||||||
IS_DEV: JSON.stringify(watch),
|
|
||||||
VERSION: JSON.stringify(VERSION),
|
|
||||||
BUILD_TIMESTAMP: Date.now(),
|
|
||||||
};
|
};
|
||||||
if (defines.IS_STANDALONE === "false")
|
if (defines.IS_STANDALONE === "false")
|
||||||
// If this is a local build (not standalone), optimise
|
// If this is a local build (not standalone), optimise
|
||||||
// for the specific platform we're on
|
// for the specific platform we're on
|
||||||
defines["process.platform"] = JSON.stringify(process.platform);
|
defines["process.platform"] = JSON.stringify(process.platform);
|
||||||
|
|
||||||
|
const header = `
|
||||||
|
// Vencord ${gitHash}
|
||||||
|
// Standalone: ${defines.IS_STANDALONE}
|
||||||
|
// Platform: ${defines["process.platform"] || "Universal"}
|
||||||
|
`.trim();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.BuildOptions}
|
* @type {esbuild.BuildOptions}
|
||||||
*/
|
*/
|
||||||
@ -40,26 +43,29 @@ const nodeCommonOpts = {
|
|||||||
format: "cjs",
|
format: "cjs",
|
||||||
platform: "node",
|
platform: "node",
|
||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
|
minify: true,
|
||||||
|
bundle: true,
|
||||||
external: ["electron", ...commonOpts.external],
|
external: ["electron", ...commonOpts.external],
|
||||||
define: defines,
|
define: defines,
|
||||||
|
banner: {
|
||||||
|
js: header
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
|
|
||||||
const sourcemap = watch ? "inline" : "external";
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// Discord Desktop main & renderer & preload
|
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...nodeCommonOpts,
|
...nodeCommonOpts,
|
||||||
entryPoints: ["src/main/index.ts"],
|
entryPoints: ["src/preload.ts"],
|
||||||
|
outfile: "dist/preload.js",
|
||||||
|
footer: { js: "//# sourceURL=VencordPreload\n//# sourceMappingURL=vencord://preload.js.map" },
|
||||||
|
sourcemap: "external",
|
||||||
|
}),
|
||||||
|
esbuild.build({
|
||||||
|
...nodeCommonOpts,
|
||||||
|
entryPoints: ["src/patcher.ts"],
|
||||||
outfile: "dist/patcher.js",
|
outfile: "dist/patcher.js",
|
||||||
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
|
footer: { js: "//# sourceURL=VencordPatcher\n//# sourceMappingURL=vencord://patcher.js.map" },
|
||||||
sourcemap,
|
sourcemap: "external",
|
||||||
define: {
|
|
||||||
...defines,
|
|
||||||
IS_DISCORD_DESKTOP: true,
|
|
||||||
IS_VESKTOP: false
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOpts,
|
...commonOpts,
|
||||||
@ -67,76 +73,16 @@ await Promise.all([
|
|||||||
outfile: "dist/renderer.js",
|
outfile: "dist/renderer.js",
|
||||||
format: "iife",
|
format: "iife",
|
||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
footer: { js: "//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
|
footer: { js: "//# sourceURL=VencordRenderer\n//# sourceMappingURL=vencord://renderer.js.map" },
|
||||||
globalName: "Vencord",
|
globalName: "Vencord",
|
||||||
sourcemap,
|
sourcemap: "external",
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins("discordDesktop"),
|
globPlugins,
|
||||||
...commonOpts.plugins
|
...commonOpts.plugins
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
...defines,
|
IS_WEB: "false",
|
||||||
IS_WEB: false,
|
IS_STANDALONE: isStandalone
|
||||||
IS_DISCORD_DESKTOP: true,
|
|
||||||
IS_VESKTOP: false
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
esbuild.build({
|
|
||||||
...nodeCommonOpts,
|
|
||||||
entryPoints: ["src/preload.ts"],
|
|
||||||
outfile: "dist/preload.js",
|
|
||||||
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
|
|
||||||
sourcemap,
|
|
||||||
define: {
|
|
||||||
...defines,
|
|
||||||
IS_DISCORD_DESKTOP: true,
|
|
||||||
IS_VESKTOP: false
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Vencord Desktop main & renderer & preload
|
|
||||||
esbuild.build({
|
|
||||||
...nodeCommonOpts,
|
|
||||||
entryPoints: ["src/main/index.ts"],
|
|
||||||
outfile: "dist/vencordDesktopMain.js",
|
|
||||||
footer: { js: "//# sourceURL=VencordDesktopMain\n" + sourceMapFooter("vencordDesktopMain") },
|
|
||||||
sourcemap,
|
|
||||||
define: {
|
|
||||||
...defines,
|
|
||||||
IS_DISCORD_DESKTOP: false,
|
|
||||||
IS_VESKTOP: true
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
esbuild.build({
|
|
||||||
...commonOpts,
|
|
||||||
entryPoints: ["src/Vencord.ts"],
|
|
||||||
outfile: "dist/vencordDesktopRenderer.js",
|
|
||||||
format: "iife",
|
|
||||||
target: ["esnext"],
|
|
||||||
footer: { js: "//# sourceURL=VencordDesktopRenderer\n" + sourceMapFooter("vencordDesktopRenderer") },
|
|
||||||
globalName: "Vencord",
|
|
||||||
sourcemap,
|
|
||||||
plugins: [
|
|
||||||
globPlugins("vencordDesktop"),
|
|
||||||
...commonOpts.plugins
|
|
||||||
],
|
|
||||||
define: {
|
|
||||||
...defines,
|
|
||||||
IS_WEB: false,
|
|
||||||
IS_DISCORD_DESKTOP: false,
|
|
||||||
IS_VESKTOP: true
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
esbuild.build({
|
|
||||||
...nodeCommonOpts,
|
|
||||||
entryPoints: ["src/preload.ts"],
|
|
||||||
outfile: "dist/vencordDesktopPreload.js",
|
|
||||||
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("vencordDesktopPreload") },
|
|
||||||
sourcemap,
|
|
||||||
define: {
|
|
||||||
...defines,
|
|
||||||
IS_DISCORD_DESKTOP: false,
|
|
||||||
IS_VESKTOP: true
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
]).catch(err => {
|
]).catch(err => {
|
||||||
|
112
scripts/build/buildWeb.mjs
Normal file → Executable file
112
scripts/build/buildWeb.mjs
Normal file → Executable file
@ -20,11 +20,13 @@
|
|||||||
|
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
import { zip } from "fflate";
|
import { zip } from "fflate";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
import { readFile } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
import { commonOpts, globPlugins, VERSION, watch } from "./common.mjs";
|
// wtf is this assert syntax
|
||||||
|
import PackageJSON from "../../package.json" assert { type: "json" };
|
||||||
|
import { commonOpts, fileIncludePlugin, gitHashPlugin, gitRemotePlugin, globPlugins } from "./common.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.BuildOptions}
|
* @type {esbuild.BuildOptions}
|
||||||
@ -34,20 +36,17 @@ const commonOptions = {
|
|||||||
entryPoints: ["browser/Vencord.ts"],
|
entryPoints: ["browser/Vencord.ts"],
|
||||||
globalName: "Vencord",
|
globalName: "Vencord",
|
||||||
format: "iife",
|
format: "iife",
|
||||||
external: ["plugins", "git-hash", "/assets/*"],
|
external: ["plugins", "git-hash"],
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins("web"),
|
globPlugins,
|
||||||
...commonOpts.plugins,
|
gitHashPlugin,
|
||||||
|
gitRemotePlugin,
|
||||||
|
fileIncludePlugin
|
||||||
],
|
],
|
||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
define: {
|
define: {
|
||||||
IS_WEB: "true",
|
IS_WEB: "true",
|
||||||
IS_STANDALONE: "true",
|
IS_STANDALONE: "true"
|
||||||
IS_DEV: JSON.stringify(watch),
|
|
||||||
IS_DISCORD_DESKTOP: "false",
|
|
||||||
IS_VESKTOP: "false",
|
|
||||||
VERSION: JSON.stringify(VERSION),
|
|
||||||
BUILD_TIMESTAMP: Date.now(),
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -60,89 +59,32 @@ await Promise.all(
|
|||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
|
|
||||||
define: {
|
|
||||||
"window": "unsafeWindow",
|
|
||||||
...(commonOptions?.define)
|
|
||||||
},
|
|
||||||
outfile: "dist/Vencord.user.js",
|
outfile: "dist/Vencord.user.js",
|
||||||
banner: {
|
banner: {
|
||||||
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${VERSION}.${new Date().getTime()}`)
|
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", PackageJSON.version)
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
|
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
|
||||||
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
|
js: "Object.defineProperty(window,'Vencord',{get:()=>Vencord});"
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
zip({
|
||||||
* @type {(target: string, files: string[], shouldZip: boolean) => Promise<void>}
|
dist: {
|
||||||
*/
|
"Vencord.js": readFileSync("dist/browser.js")
|
||||||
async function buildPluginZip(target, files, shouldZip) {
|
},
|
||||||
const entries = {
|
...Object.fromEntries(await Promise.all(["background.js", "content.js", "manifest.json"].map(async f => [
|
||||||
"dist/Vencord.js": await readFile("dist/browser.js"),
|
f,
|
||||||
"dist/Vencord.css": await readFile("dist/browser.css"),
|
await readFile(join("browser", f))
|
||||||
...Object.fromEntries(await Promise.all(files.map(async f => {
|
]))),
|
||||||
let content = await readFile(join("browser", f));
|
}, {}, (err, data) => {
|
||||||
if (f.startsWith("manifest")) {
|
if (err) {
|
||||||
const json = JSON.parse(content.toString("utf-8"));
|
console.error(err);
|
||||||
json.version = VERSION;
|
process.exitCode = 1;
|
||||||
content = new TextEncoder().encode(JSON.stringify(json));
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
f.startsWith("manifest") ? "manifest.json" : f,
|
|
||||||
content
|
|
||||||
];
|
|
||||||
}))),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (shouldZip) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
zip(entries, {}, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
const out = join("dist", target);
|
|
||||||
writeFile(out, data).then(() => {
|
|
||||||
console.info("Extension written to " + out);
|
|
||||||
resolve();
|
|
||||||
}).catch(reject);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await rm(target, { recursive: true, force: true });
|
writeFileSync("dist/extension.zip", data);
|
||||||
await Promise.all(Object.entries(entries).map(async ([file, content]) => {
|
console.info("Extension written to dist/extension.zip");
|
||||||
const dest = join("dist", target, file);
|
|
||||||
const parentDirectory = join(dest, "..");
|
|
||||||
await mkdir(parentDirectory, { recursive: true });
|
|
||||||
await writeFile(dest, content);
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.info("Unpacked Extension written to dist/" + target);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => {
|
|
||||||
const cssRuntime = `
|
|
||||||
;document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(
|
|
||||||
Object.assign(document.createElement("style"), {
|
|
||||||
textContent: \`${content.replaceAll("`", "\\`")}\`,
|
|
||||||
id: "vencord-css-core"
|
|
||||||
})
|
|
||||||
), { once: true });
|
|
||||||
`;
|
|
||||||
|
|
||||||
return appendFile("dist/Vencord.user.js", cssRuntime);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
appendCssRuntime,
|
|
||||||
buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true),
|
|
||||||
buildPluginZip("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
|
|
||||||
buildPluginZip("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"], false),
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
@ -16,37 +16,19 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "../suppressExperimentalWarnings.js";
|
|
||||||
import "../checkNodeVersion.js";
|
|
||||||
|
|
||||||
import { exec, execSync } from "child_process";
|
import { exec, execSync } from "child_process";
|
||||||
import { existsSync, readFileSync } from "fs";
|
import esbuild from "esbuild";
|
||||||
|
import { existsSync } from "fs";
|
||||||
import { readdir, readFile } from "fs/promises";
|
import { readdir, readFile } from "fs/promises";
|
||||||
import { join, relative } from "path";
|
import { join } from "path";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
// wtf is this assert syntax
|
|
||||||
import PackageJSON from "../../package.json" assert { type: "json" };
|
|
||||||
import { getPluginTarget } from "../utils.mjs";
|
|
||||||
|
|
||||||
export const VERSION = PackageJSON.version;
|
|
||||||
export const BUILD_TIMESTAMP = Date.now();
|
|
||||||
export const watch = process.argv.includes("--watch");
|
export const watch = process.argv.includes("--watch");
|
||||||
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
||||||
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
|
||||||
export const banner = {
|
|
||||||
js: `
|
|
||||||
// Vencord ${gitHash}
|
|
||||||
// Standalone: ${isStandalone}
|
|
||||||
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
|
|
||||||
`.trim()
|
|
||||||
};
|
|
||||||
|
|
||||||
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
|
|
||||||
|
|
||||||
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {esbuild.Plugin}
|
||||||
*/
|
*/
|
||||||
export const makeAllPackagesExternalPlugin = {
|
export const makeAllPackagesExternalPlugin = {
|
||||||
name: "make-all-packages-external",
|
name: "make-all-packages-external",
|
||||||
@ -57,9 +39,9 @@ export const makeAllPackagesExternalPlugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {(kind: "web" | "discordDesktop" | "vencordDesktop") => import("esbuild").Plugin}
|
* @type {esbuild.Plugin}
|
||||||
*/
|
*/
|
||||||
export const globPlugins = kind => ({
|
export const globPlugins = {
|
||||||
name: "glob-plugins",
|
name: "glob-plugins",
|
||||||
setup: build => {
|
setup: build => {
|
||||||
const filter = /^~plugins$/;
|
const filter = /^~plugins$/;
|
||||||
@ -71,7 +53,7 @@ export const globPlugins = kind => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
|
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
|
||||||
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins"];
|
const pluginDirs = ["plugins", "userplugins"];
|
||||||
let code = "";
|
let code = "";
|
||||||
let plugins = "\n";
|
let plugins = "\n";
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@ -79,20 +61,11 @@ export const globPlugins = kind => ({
|
|||||||
if (!existsSync(`./src/${dir}`)) continue;
|
if (!existsSync(`./src/${dir}`)) continue;
|
||||||
const files = await readdir(`./src/${dir}`);
|
const files = await readdir(`./src/${dir}`);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.startsWith("_") || file.startsWith(".")) continue;
|
if (file === "index.ts") {
|
||||||
if (file === "index.ts") continue;
|
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 mod = `p${i}`;
|
const mod = `p${i}`;
|
||||||
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
|
code += `import ${mod} from "./${dir}/${file.replace(/.tsx?$/, "")}";\n`;
|
||||||
plugins += `[${mod}.name]:${mod},\n`;
|
plugins += `[${mod}.name]:${mod},\n`;
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
@ -104,10 +77,11 @@ export const globPlugins = kind => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {esbuild.Plugin}
|
||||||
*/
|
*/
|
||||||
export const gitHashPlugin = {
|
export const gitHashPlugin = {
|
||||||
name: "git-hash-plugin",
|
name: "git-hash-plugin",
|
||||||
@ -123,7 +97,7 @@ export const gitHashPlugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {esbuild.Plugin}
|
||||||
*/
|
*/
|
||||||
export const gitRemotePlugin = {
|
export const gitRemotePlugin = {
|
||||||
name: "git-remote-plugin",
|
name: "git-remote-plugin",
|
||||||
@ -145,7 +119,7 @@ export const gitRemotePlugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {esbuild.Plugin}
|
||||||
*/
|
*/
|
||||||
export const fileIncludePlugin = {
|
export const fileIncludePlugin = {
|
||||||
name: "file-include-plugin",
|
name: "file-include-plugin",
|
||||||
@ -167,33 +141,8 @@ export const fileIncludePlugin = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const styleModule = readFileSync("./scripts/build/module/style.js", "utf-8");
|
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {esbuild.BuildOptions}
|
||||||
*/
|
|
||||||
export const stylePlugin = {
|
|
||||||
name: "style-plugin",
|
|
||||||
setup: ({ onResolve, onLoad }) => {
|
|
||||||
onResolve({ filter: /\.css\?managed$/, namespace: "file" }, ({ path, resolveDir }) => ({
|
|
||||||
path: relative(process.cwd(), join(resolveDir, path.replace("?managed", ""))),
|
|
||||||
namespace: "managed-style",
|
|
||||||
}));
|
|
||||||
onLoad({ filter: /\.css$/, namespace: "managed-style" }, async ({ path }) => {
|
|
||||||
const css = await readFile(path, "utf-8");
|
|
||||||
const name = relative(process.cwd(), path).replaceAll("\\", "/");
|
|
||||||
|
|
||||||
return {
|
|
||||||
loader: "js",
|
|
||||||
contents: styleModule
|
|
||||||
.replaceAll("STYLE_SOURCE", JSON.stringify(css))
|
|
||||||
.replaceAll("STYLE_NAME", JSON.stringify(name))
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {import("esbuild").BuildOptions}
|
|
||||||
*/
|
*/
|
||||||
export const commonOpts = {
|
export const commonOpts = {
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
@ -202,12 +151,6 @@ export const commonOpts = {
|
|||||||
minify: !watch,
|
minify: !watch,
|
||||||
sourcemap: watch ? "inline" : "",
|
sourcemap: watch ? "inline" : "",
|
||||||
legalComments: "linked",
|
legalComments: "linked",
|
||||||
banner,
|
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin],
|
||||||
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
external: ["~plugins", "~git-hash", "~git-remote"]
|
||||||
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
|
||||||
inject: ["./scripts/build/inject/react.mjs"],
|
|
||||||
jsxFactory: "VencordCreateElement",
|
|
||||||
jsxFragment: "VencordFragment",
|
|
||||||
// Work around https://github.com/evanw/esbuild/issues/2460
|
|
||||||
tsconfig: "./scripts/build/tsconfig.esbuild.json"
|
|
||||||
};
|
};
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment");
|
|
||||||
export let VencordCreateElement =
|
|
||||||
(...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);
|
|
@ -1,7 +0,0 @@
|
|||||||
// Work around https://github.com/evanw/esbuild/issues/2460
|
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "react"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (Number(process.versions.node.split(".")[0]) < 18)
|
|
||||||
throw `Your node version (${process.version}) is too old, please update to v18 or higher https://nodejs.org/en/download/`;
|
|
@ -1,212 +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 { Dirent, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
||||||
import { access, readFile } from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
|
|
||||||
|
|
||||||
import { getPluginTarget } from "./utils.mjs";
|
|
||||||
|
|
||||||
interface Dev {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PluginData {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
tags: string[];
|
|
||||||
authors: Dev[];
|
|
||||||
dependencies: string[];
|
|
||||||
hasPatches: boolean;
|
|
||||||
hasCommands: boolean;
|
|
||||||
required: boolean;
|
|
||||||
enabledByDefault: boolean;
|
|
||||||
target: "discordDesktop" | "vencordDesktop" | "web" | "dev";
|
|
||||||
}
|
|
||||||
|
|
||||||
const devs = {} as Record<string, Dev>;
|
|
||||||
|
|
||||||
function getName(node: NamedDeclaration) {
|
|
||||||
return node.name && isIdentifier(node.name) ? node.name.text : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasName(node: NamedDeclaration, name: string) {
|
|
||||||
return getName(node) === name;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getObjectProp(node: ObjectLiteralExpression, name: string) {
|
|
||||||
const prop = node.properties.find(p => hasName(p, name));
|
|
||||||
if (prop && isPropertyAssignment(prop)) return prop.initializer;
|
|
||||||
return prop;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseDevs() {
|
|
||||||
const file = createSourceFile("constants.ts", readFileSync("src/utils/constants.ts", "utf8"), ScriptTarget.Latest);
|
|
||||||
|
|
||||||
for (const child of file.getChildAt(0).getChildren()) {
|
|
||||||
if (!isVariableStatement(child)) continue;
|
|
||||||
|
|
||||||
const devsDeclaration = child.declarationList.declarations.find(d => hasName(d, "Devs"));
|
|
||||||
if (!devsDeclaration?.initializer || !isCallExpression(devsDeclaration.initializer)) continue;
|
|
||||||
|
|
||||||
const value = devsDeclaration.initializer.arguments[0];
|
|
||||||
|
|
||||||
if (!isSatisfiesExpression(value) || !isObjectLiteralExpression(value.expression)) throw new Error("Failed to parse devs: not an object literal");
|
|
||||||
|
|
||||||
for (const prop of value.expression.properties) {
|
|
||||||
const name = (prop.name as Identifier).text;
|
|
||||||
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
|
|
||||||
|
|
||||||
if (!isObjectLiteralExpression(value)) throw new Error(`Failed to parse devs: ${name} is not an object literal`);
|
|
||||||
|
|
||||||
devs[name] = {
|
|
||||||
name: (getObjectProp(value, "name") as StringLiteral).text,
|
|
||||||
id: (getObjectProp(value, "id") as BigIntLiteral).text.slice(0, -1)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Could not find Devs constant");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function parseFile(fileName: string) {
|
|
||||||
const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest);
|
|
||||||
|
|
||||||
const fail = (reason: string) => {
|
|
||||||
return new Error(`Invalid plugin ${fileName}, because ${reason}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const node of file.getChildAt(0).getChildren()) {
|
|
||||||
if (!isExportAssignment(node) || !isCallExpression(node.expression)) continue;
|
|
||||||
|
|
||||||
const call = node.expression;
|
|
||||||
if (!isIdentifier(call.expression) || call.expression.text !== "definePlugin") continue;
|
|
||||||
|
|
||||||
const pluginObj = node.expression.arguments[0];
|
|
||||||
if (!isObjectLiteralExpression(pluginObj)) throw fail("no object literal passed to definePlugin");
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
hasPatches: false,
|
|
||||||
hasCommands: false,
|
|
||||||
enabledByDefault: false,
|
|
||||||
required: false,
|
|
||||||
tags: [] as string[]
|
|
||||||
} as PluginData;
|
|
||||||
|
|
||||||
for (const prop of pluginObj.properties) {
|
|
||||||
const key = getName(prop);
|
|
||||||
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
|
|
||||||
|
|
||||||
switch (key) {
|
|
||||||
case "name":
|
|
||||||
case "description":
|
|
||||||
if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);
|
|
||||||
data[key] = value.text;
|
|
||||||
break;
|
|
||||||
case "patches":
|
|
||||||
data.hasPatches = true;
|
|
||||||
break;
|
|
||||||
case "commands":
|
|
||||||
data.hasCommands = true;
|
|
||||||
break;
|
|
||||||
case "authors":
|
|
||||||
if (!isArrayLiteralExpression(value)) throw fail("authors is not an array literal");
|
|
||||||
data.authors = value.elements.map(e => {
|
|
||||||
if (!isPropertyAccessExpression(e)) throw fail("authors array contains non-property access expressions");
|
|
||||||
const d = devs[getName(e)!];
|
|
||||||
if (!d) throw fail(`couldn't look up author ${getName(e)}`);
|
|
||||||
return d;
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "tags":
|
|
||||||
if (!isArrayLiteralExpression(value)) throw fail("tags is not an array literal");
|
|
||||||
data.tags = value.elements.map(e => {
|
|
||||||
if (!isStringLiteral(e)) throw fail("tags array contains non-string literals");
|
|
||||||
return e.text;
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "dependencies":
|
|
||||||
if (!isArrayLiteralExpression(value)) throw fail("dependencies is not an array literal");
|
|
||||||
const { elements } = value;
|
|
||||||
if (elements.some(e => !isStringLiteral(e))) throw fail("dependencies array contains non-string elements");
|
|
||||||
data.dependencies = (elements as NodeArray<StringLiteral>).map(e => e.text);
|
|
||||||
break;
|
|
||||||
case "required":
|
|
||||||
case "enabledByDefault":
|
|
||||||
data[key] = value.kind === SyntaxKind.TrueKeyword;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.name || !data.description || !data.authors) throw fail("name, description or authors are missing");
|
|
||||||
|
|
||||||
const target = getPluginTarget(fileName);
|
|
||||||
if (target) {
|
|
||||||
if (!["web", "discordDesktop", "vencordDesktop", "dev"].includes(target)) throw fail(`invalid target ${target}`);
|
|
||||||
data.target = target as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw fail("no default export called 'definePlugin' found");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getEntryPoint(dir: string, dirent: Dirent) {
|
|
||||||
const base = join(dir, dirent.name);
|
|
||||||
if (!dirent.isDirectory()) return base;
|
|
||||||
|
|
||||||
for (const name of ["index.ts", "index.tsx"]) {
|
|
||||||
const full = join(base, name);
|
|
||||||
try {
|
|
||||||
await access(full);
|
|
||||||
return full;
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`${dirent.name}: Couldn't find entry point`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPluginFile({ name }: { name: string; }) {
|
|
||||||
if (name === "index.ts") return false;
|
|
||||||
return !name.startsWith("_") && !name.startsWith(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
parseDevs();
|
|
||||||
|
|
||||||
const plugins = ["src/plugins", "src/plugins/_core"].flatMap(dir =>
|
|
||||||
readdirSync(dir, { withFileTypes: true })
|
|
||||||
.filter(isPluginFile)
|
|
||||||
.map(async dirent =>
|
|
||||||
parseFile(await getEntryPoint(dir, dirent))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = JSON.stringify(await Promise.all(plugins));
|
|
||||||
|
|
||||||
if (process.argv.length > 2) {
|
|
||||||
writeFileSync(process.argv[2], data);
|
|
||||||
} else {
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
})();
|
|
@ -1,295 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// eslint-disable-next-line spaced-comment
|
|
||||||
/// <reference types="../src/globals" />
|
|
||||||
// eslint-disable-next-line spaced-comment
|
|
||||||
/// <reference types="../src/modules" />
|
|
||||||
|
|
||||||
import { readFileSync } from "fs";
|
|
||||||
import pup, { JSHandle } from "puppeteer-core";
|
|
||||||
|
|
||||||
for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
|
|
||||||
if (!process.env[variable]) {
|
|
||||||
console.error(`Missing environment variable ${variable}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const CANARY = process.env.USE_CANARY === "true";
|
|
||||||
|
|
||||||
const browser = await pup.launch({
|
|
||||||
headless: true,
|
|
||||||
executablePath: process.env.CHROMIUM_BIN
|
|
||||||
});
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
function maybeGetError(handle: JSHandle) {
|
|
||||||
return (handle as JSHandle<Error>)?.getProperty("message")
|
|
||||||
.then(m => m.jsonValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
const report = {
|
|
||||||
badPatches: [] as {
|
|
||||||
plugin: string;
|
|
||||||
type: string;
|
|
||||||
id: string;
|
|
||||||
match: string;
|
|
||||||
error?: string;
|
|
||||||
}[],
|
|
||||||
badStarts: [] as {
|
|
||||||
plugin: string;
|
|
||||||
error: string;
|
|
||||||
}[],
|
|
||||||
otherErrors: [] as string[]
|
|
||||||
};
|
|
||||||
|
|
||||||
function toCodeBlock(s: string) {
|
|
||||||
s = s.replace(/```/g, "`\u200B`\u200B`");
|
|
||||||
return "```" + s + " ```";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function printReport() {
|
|
||||||
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)}`);
|
|
||||||
if (p.error) console.log(` - Error: ${toCodeBlock(p.error)}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
console.log("## Bad Starts");
|
|
||||||
report.badStarts.forEach(p => {
|
|
||||||
console.log(`- ${p.plugin}`);
|
|
||||||
console.log(` - Error: ${toCodeBlock(p.error)}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("## Discord Errors");
|
|
||||||
report.otherErrors.forEach(e => {
|
|
||||||
console.log(`- ${toCodeBlock(e)}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (process.env.DISCORD_WEBHOOK) {
|
|
||||||
// this code was written almost entirely by Copilot xD
|
|
||||||
await fetch(process.env.DISCORD_WEBHOOK, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
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",
|
|
||||||
description: report.badPatches.map(p => {
|
|
||||||
const lines = [
|
|
||||||
`**__${p.plugin} (${p.type}):__**`,
|
|
||||||
`ID: \`${p.id}\``,
|
|
||||||
`Match: ${toCodeBlock(p.match)}`
|
|
||||||
];
|
|
||||||
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 Starts",
|
|
||||||
description: report.badStarts.map(p => {
|
|
||||||
const lines = [
|
|
||||||
`**__${p.plugin}:__**`,
|
|
||||||
toCodeBlock(p.error)
|
|
||||||
];
|
|
||||||
return lines.join("\n");
|
|
||||||
}
|
|
||||||
).join("\n\n") || "None",
|
|
||||||
color: report.badStarts.length ? 0xff0000 : 0x00ff00
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Discord Errors",
|
|
||||||
description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n")) : "None",
|
|
||||||
color: report.otherErrors.length ? 0xff0000 : 0x00ff00
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}).then(res => {
|
|
||||||
if (!res.ok) console.error(`Webhook failed with status ${res.status}`);
|
|
||||||
else console.error("Posted to Discord Webhook successfully");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
page.on("console", async e => {
|
|
||||||
const level = e.type();
|
|
||||||
const args = e.args();
|
|
||||||
|
|
||||||
const firstArg = (await args[0]?.jsonValue());
|
|
||||||
if (firstArg === "PUPPETEER_TEST_DONE_SIGNAL") {
|
|
||||||
await browser.close();
|
|
||||||
await printReport();
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
const isVencord = (await args[0]?.jsonValue()) === "[Vencord]";
|
|
||||||
const isDebug = (await args[0]?.jsonValue()) === "[PUP_DEBUG]";
|
|
||||||
|
|
||||||
if (isVencord) {
|
|
||||||
// make ci fail
|
|
||||||
process.exitCode = 1;
|
|
||||||
|
|
||||||
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 [, 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,
|
|
||||||
error: cause
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "PluginManager:":
|
|
||||||
const [, name] = (message as string).match(/Failed to start (.+)/)!;
|
|
||||||
report.badStarts.push({
|
|
||||||
plugin: name,
|
|
||||||
error: cause
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (isDebug) {
|
|
||||||
console.error(e.text());
|
|
||||||
} else if (level === "error") {
|
|
||||||
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")) {
|
|
||||||
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 occured");
|
|
||||||
console.error("[PUP_DEBUG]", e);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.evaluateOnNewDocument(`
|
|
||||||
${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");
|
|
@ -1,3 +0,0 @@
|
|||||||
Vencord, a Discord client mod
|
|
||||||
Copyright (c) {year} {author}
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
@ -1,17 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) {year} {author}
|
|
||||||
*
|
|
||||||
* 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/>.
|
|
||||||
*/
|
|
341
scripts/patcher/common.js
Normal file
341
scripts/patcher/common.js
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const readline = require("readline");
|
||||||
|
const fs = require("fs");
|
||||||
|
const menu = require("console-menu");
|
||||||
|
|
||||||
|
const BRANCH_NAMES = [
|
||||||
|
"Discord",
|
||||||
|
"DiscordPTB",
|
||||||
|
"DiscordCanary",
|
||||||
|
"DiscordDevelopment",
|
||||||
|
"discord",
|
||||||
|
"discordptb",
|
||||||
|
"discordcanary",
|
||||||
|
"discorddevelopment",
|
||||||
|
"discord-ptb",
|
||||||
|
"discord-canary",
|
||||||
|
"discord-development",
|
||||||
|
// Flatpak
|
||||||
|
"com.discordapp.Discord",
|
||||||
|
"com.discordapp.DiscordPTB",
|
||||||
|
"com.discordapp.DiscordCanary",
|
||||||
|
"com.discordapp.DiscordDevelopment",
|
||||||
|
];
|
||||||
|
|
||||||
|
const MACOS_DISCORD_DIRS = [
|
||||||
|
"Discord.app",
|
||||||
|
"Discord PTB.app",
|
||||||
|
"Discord Canary.app",
|
||||||
|
"Discord Development.app",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.platform === "linux" && process.env.SUDO_USER) {
|
||||||
|
process.env.HOME = fs
|
||||||
|
.readFileSync("/etc/passwd", "utf-8")
|
||||||
|
.match(new RegExp(`^${process.env.SUDO_USER}.+$`, "m"))[0]
|
||||||
|
.split(":")[5];
|
||||||
|
}
|
||||||
|
|
||||||
|
const LINUX_DISCORD_DIRS = [
|
||||||
|
"/usr/share",
|
||||||
|
"/usr/lib64",
|
||||||
|
"/opt",
|
||||||
|
`${process.env.HOME}/.local/share`,
|
||||||
|
"/var/lib/flatpak/app",
|
||||||
|
`${process.env.HOME}/.local/share/flatpak/app`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const FLATPAK_NAME_MAPPING = {
|
||||||
|
DiscordCanary: "discord-canary",
|
||||||
|
DiscordPTB: "discord-ptb",
|
||||||
|
DiscordDevelopment: "discord-development",
|
||||||
|
Discord: "discord",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ENTRYPOINT = path
|
||||||
|
.join(process.cwd(), "dist", "patcher.js")
|
||||||
|
.replace(/\\/g, "/");
|
||||||
|
|
||||||
|
function question(question) {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
terminal: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
rl.question(question, answer => {
|
||||||
|
rl.close();
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMenuItem(installations) {
|
||||||
|
const menuItems = installations.map(info => ({
|
||||||
|
title: info.patched ? "[MODIFIED] " + info.location : info.location,
|
||||||
|
info,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await menu(
|
||||||
|
[
|
||||||
|
...menuItems,
|
||||||
|
{ title: "Specify custom path", info: "custom" },
|
||||||
|
{ title: "Exit without patching", exit: true }
|
||||||
|
],
|
||||||
|
{
|
||||||
|
header: "Select a Discord installation to patch:",
|
||||||
|
border: true,
|
||||||
|
helpMessage:
|
||||||
|
"Use the up/down arrow keys to select an option. " +
|
||||||
|
"Press ENTER to confirm.",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result || !result.info || result.exit) {
|
||||||
|
console.log("No installation selected.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.info === "custom") {
|
||||||
|
const customPath = await question("Please enter the path: ");
|
||||||
|
if (!customPath || !fs.existsSync(customPath)) {
|
||||||
|
console.log("No such Path or not specifed.");
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceDir = path.join(customPath, "resources");
|
||||||
|
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
||||||
|
console.log("Unsupported Install. resources/app.asar not found");
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const appDir = path.join(resourceDir, "app");
|
||||||
|
result.info = {
|
||||||
|
branch: "unknown",
|
||||||
|
patched: fs.existsSync(appDir),
|
||||||
|
location: customPath,
|
||||||
|
versions: [{
|
||||||
|
path: appDir,
|
||||||
|
name: null
|
||||||
|
}],
|
||||||
|
arch: process.platform === "linux" ? "linux" : "win32",
|
||||||
|
isFlatpak: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.info.patched) {
|
||||||
|
const answer = await question(
|
||||||
|
"This installation has already been modified. Overwrite? [Y/n]: "
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!["y", "yes", "yeah", ""].includes(answer.toLowerCase())) {
|
||||||
|
console.log("Not patching.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.info;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWindowsDirs() {
|
||||||
|
const dirs = [];
|
||||||
|
for (const dir of fs.readdirSync(process.env.LOCALAPPDATA)) {
|
||||||
|
if (!BRANCH_NAMES.includes(dir)) continue;
|
||||||
|
|
||||||
|
const location = path.join(process.env.LOCALAPPDATA, dir);
|
||||||
|
if (!fs.statSync(location).isDirectory()) continue;
|
||||||
|
|
||||||
|
const appDirs = fs
|
||||||
|
.readdirSync(location, { withFileTypes: true })
|
||||||
|
.filter(file => file.isDirectory())
|
||||||
|
.filter(file => file.name.startsWith("app-"))
|
||||||
|
.map(file => path.join(location, file.name));
|
||||||
|
|
||||||
|
const versions = [];
|
||||||
|
let patched = false;
|
||||||
|
|
||||||
|
for (const fqAppDir of appDirs) {
|
||||||
|
const resourceDir = path.join(fqAppDir, "resources");
|
||||||
|
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const appDir = path.join(resourceDir, "app");
|
||||||
|
if (fs.existsSync(appDir)) {
|
||||||
|
patched = true;
|
||||||
|
}
|
||||||
|
versions.push({
|
||||||
|
path: appDir,
|
||||||
|
name: /app-([0-9.]+)/.exec(fqAppDir)[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appDirs.length) {
|
||||||
|
dirs.push({
|
||||||
|
branch: dir,
|
||||||
|
patched,
|
||||||
|
location,
|
||||||
|
versions,
|
||||||
|
arch: "win32",
|
||||||
|
flatpak: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDarwinDirs() {
|
||||||
|
const dirs = [];
|
||||||
|
for (const dir of fs.readdirSync("/Applications")) {
|
||||||
|
if (!MACOS_DISCORD_DIRS.includes(dir)) continue;
|
||||||
|
|
||||||
|
const location = path.join("/Applications", dir, "Contents");
|
||||||
|
if (!fs.existsSync(location)) continue;
|
||||||
|
if (!fs.statSync(location).isDirectory()) continue;
|
||||||
|
|
||||||
|
const appDirs = fs
|
||||||
|
.readdirSync(location, { withFileTypes: true })
|
||||||
|
.filter(file => file.isDirectory())
|
||||||
|
.filter(file => file.name.startsWith("Resources"))
|
||||||
|
.map(file => path.join(location, file.name));
|
||||||
|
|
||||||
|
const versions = [];
|
||||||
|
let patched = false;
|
||||||
|
|
||||||
|
for (const resourceDir of appDirs) {
|
||||||
|
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const appDir = path.join(resourceDir, "app");
|
||||||
|
if (fs.existsSync(appDir)) {
|
||||||
|
patched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
versions.push({
|
||||||
|
path: appDir,
|
||||||
|
name: null, // MacOS installs have no version number
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appDirs.length) {
|
||||||
|
dirs.push({
|
||||||
|
branch: dir,
|
||||||
|
patched,
|
||||||
|
location,
|
||||||
|
versions,
|
||||||
|
arch: "win32",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLinuxDirs() {
|
||||||
|
const dirs = [];
|
||||||
|
for (const dir of LINUX_DISCORD_DIRS) {
|
||||||
|
if (!fs.existsSync(dir)) continue;
|
||||||
|
for (const branch of fs.readdirSync(dir)) {
|
||||||
|
if (!BRANCH_NAMES.includes(branch)) continue;
|
||||||
|
|
||||||
|
const location = path.join(dir, branch);
|
||||||
|
if (!fs.statSync(location).isDirectory()) continue;
|
||||||
|
|
||||||
|
const isFlatpak = location.includes("/flatpak/");
|
||||||
|
|
||||||
|
let appDirs = [];
|
||||||
|
|
||||||
|
if (isFlatpak) {
|
||||||
|
const fqDir = path.join(location, "current", "active", "files");
|
||||||
|
if (!/com\.discordapp\.(\w+)\//.test(fqDir)) continue;
|
||||||
|
const branchName = /com\.discordapp\.(\w+)\//.exec(fqDir)[1];
|
||||||
|
if (!Object.keys(FLATPAK_NAME_MAPPING).includes(branchName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const appDir = path.join(
|
||||||
|
fqDir,
|
||||||
|
FLATPAK_NAME_MAPPING[branchName]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(appDir)) continue;
|
||||||
|
if (!fs.statSync(appDir).isDirectory()) continue;
|
||||||
|
|
||||||
|
const resourceDir = path.join(appDir, "resources");
|
||||||
|
|
||||||
|
appDirs.push(resourceDir);
|
||||||
|
} else {
|
||||||
|
appDirs = fs
|
||||||
|
.readdirSync(location, { withFileTypes: true })
|
||||||
|
.filter(file => file.isDirectory())
|
||||||
|
.filter(
|
||||||
|
file =>
|
||||||
|
file.name.startsWith("app-") ||
|
||||||
|
file.name === "resources"
|
||||||
|
)
|
||||||
|
.map(file => path.join(location, file.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = [];
|
||||||
|
let patched = false;
|
||||||
|
|
||||||
|
for (const resourceDir of appDirs) {
|
||||||
|
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const appDir = path.join(resourceDir, "app");
|
||||||
|
if (fs.existsSync(appDir)) {
|
||||||
|
patched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = /app-([0-9.]+)/.exec(resourceDir);
|
||||||
|
|
||||||
|
versions.push({
|
||||||
|
path: appDir,
|
||||||
|
name: version && version.length > 1 ? version[1] : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appDirs.length) {
|
||||||
|
dirs.push({
|
||||||
|
branch,
|
||||||
|
patched,
|
||||||
|
location,
|
||||||
|
versions,
|
||||||
|
arch: "linux",
|
||||||
|
isFlatpak,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirs;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
BRANCH_NAMES,
|
||||||
|
MACOS_DISCORD_DIRS,
|
||||||
|
LINUX_DISCORD_DIRS,
|
||||||
|
FLATPAK_NAME_MAPPING,
|
||||||
|
ENTRYPOINT,
|
||||||
|
question,
|
||||||
|
getMenuItem,
|
||||||
|
getWindowsDirs,
|
||||||
|
getDarwinDirs,
|
||||||
|
getLinuxDirs,
|
||||||
|
};
|
130
scripts/patcher/install.js
Executable file
130
scripts/patcher/install.js
Executable file
@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/node
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const { execSync } = require("child_process");
|
||||||
|
|
||||||
|
console.log("\nVencord Installer\n");
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
|
||||||
|
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(process.cwd(), "dist", "patcher.js"))) {
|
||||||
|
console.log("You need to build the project first. Run:", "pnpm build");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
getMenuItem,
|
||||||
|
getWindowsDirs,
|
||||||
|
getDarwinDirs,
|
||||||
|
getLinuxDirs,
|
||||||
|
ENTRYPOINT,
|
||||||
|
} = require("./common");
|
||||||
|
|
||||||
|
switch (process.platform) {
|
||||||
|
case "win32":
|
||||||
|
install(getWindowsDirs());
|
||||||
|
break;
|
||||||
|
case "darwin":
|
||||||
|
install(getDarwinDirs());
|
||||||
|
break;
|
||||||
|
case "linux":
|
||||||
|
install(getLinuxDirs());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Unknown OS");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function install(installations) {
|
||||||
|
const selected = await getMenuItem(installations);
|
||||||
|
|
||||||
|
// Attempt to give flatpak perms
|
||||||
|
if (selected.isFlatpak) {
|
||||||
|
try {
|
||||||
|
const { branch } = selected;
|
||||||
|
const cwd = process.cwd();
|
||||||
|
const globalCmd = `flatpak override ${branch} --filesystem=${cwd}`;
|
||||||
|
const userCmd = `flatpak override --user ${branch} --filesystem=${cwd}`;
|
||||||
|
const cmd = selected.location.startsWith("/home")
|
||||||
|
? userCmd
|
||||||
|
: globalCmd;
|
||||||
|
execSync(cmd);
|
||||||
|
console.log("Successfully gave write perms to Discord Flatpak.");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to give write perms to Discord Flatpak.");
|
||||||
|
console.log(
|
||||||
|
"Try running this script as an administrator:",
|
||||||
|
"sudo pnpm inject"
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const version of selected.versions) {
|
||||||
|
const dir = version.path;
|
||||||
|
// Check if we have write perms to the install directory...
|
||||||
|
try {
|
||||||
|
fs.accessSync(selected.location, fs.constants.W_OK);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("No write access to", selected.location);
|
||||||
|
console.error(
|
||||||
|
"Try running this script as an administrator:",
|
||||||
|
"sudo pnpm inject"
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(dir) && fs.lstatSync(dir).isDirectory()) {
|
||||||
|
fs.rmSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, "index.js"),
|
||||||
|
`require("${ENTRYPOINT}");`
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(dir, "package.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
name: "discord",
|
||||||
|
main: "index.js",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const requiredFiles = ["index.js", "package.json"];
|
||||||
|
|
||||||
|
if (requiredFiles.every(f => fs.existsSync(path.join(dir, f)))) {
|
||||||
|
console.log(
|
||||||
|
"Successfully patched",
|
||||||
|
version.name
|
||||||
|
? `${selected.branch} ${version.name}`
|
||||||
|
: selected.branch
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log("Failed to patch", dir);
|
||||||
|
console.log("Files in directory:", fs.readdirSync(dir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
scripts/patcher/uninstall.js
Executable file
78
scripts/patcher/uninstall.js
Executable file
@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/node
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
console.log("\nVencord Uninstaller\n");
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
|
||||||
|
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
getMenuItem,
|
||||||
|
getWindowsDirs,
|
||||||
|
getDarwinDirs,
|
||||||
|
getLinuxDirs,
|
||||||
|
} = require("./common");
|
||||||
|
|
||||||
|
switch (process.platform) {
|
||||||
|
case "win32":
|
||||||
|
uninstall(getWindowsDirs());
|
||||||
|
break;
|
||||||
|
case "darwin":
|
||||||
|
uninstall(getDarwinDirs());
|
||||||
|
break;
|
||||||
|
case "linux":
|
||||||
|
uninstall(getLinuxDirs());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Unknown OS");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uninstall(installations) {
|
||||||
|
const selected = await getMenuItem(installations);
|
||||||
|
|
||||||
|
for (const version of selected.versions) {
|
||||||
|
const dir = version.path;
|
||||||
|
// Check if we have write perms to the install directory...
|
||||||
|
try {
|
||||||
|
fs.accessSync(selected.location, fs.constants.W_OK);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("No write access to", selected.location);
|
||||||
|
console.error(
|
||||||
|
"Try running this script as an administrator:",
|
||||||
|
"sudo pnpm uninject"
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(dir)) {
|
||||||
|
fs.rmSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"Successfully unpatched",
|
||||||
|
version.name
|
||||||
|
? `${selected.branch} ${version.name}`
|
||||||
|
: selected.branch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,128 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import "./checkNodeVersion.js";
|
|
||||||
|
|
||||||
import { execFileSync, execSync } from "child_process";
|
|
||||||
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
||||||
import { dirname, join } from "path";
|
|
||||||
import { Readable } from "stream";
|
|
||||||
import { finished } from "stream/promises";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
const BASE_URL = "https://github.com/Vencord/Installer/releases/latest/download/";
|
|
||||||
const INSTALLER_PATH_DARWIN = "VencordInstaller.app/Contents/MacOS/VencordInstaller";
|
|
||||||
|
|
||||||
const BASE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const FILE_DIR = join(BASE_DIR, "dist", "Installer");
|
|
||||||
const ETAG_FILE = join(FILE_DIR, "etag.txt");
|
|
||||||
|
|
||||||
function getFilename() {
|
|
||||||
switch (process.platform) {
|
|
||||||
case "win32":
|
|
||||||
return "VencordInstaller.exe";
|
|
||||||
case "darwin":
|
|
||||||
return "VencordInstaller.MacOS.zip";
|
|
||||||
case "linux":
|
|
||||||
return "VencordInstaller-" + (process.env.WAYLAND_DISPLAY ? "wayland" : "x11");
|
|
||||||
default:
|
|
||||||
throw new Error("Unsupported platform: " + process.platform);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureBinary() {
|
|
||||||
const filename = getFilename();
|
|
||||||
console.log("Downloading " + filename);
|
|
||||||
|
|
||||||
mkdirSync(FILE_DIR, { recursive: true });
|
|
||||||
|
|
||||||
const downloadName = join(FILE_DIR, filename);
|
|
||||||
const outputFile = process.platform === "darwin"
|
|
||||||
? join(FILE_DIR, "VencordInstaller")
|
|
||||||
: downloadName;
|
|
||||||
|
|
||||||
const etag = existsSync(outputFile) && existsSync(ETAG_FILE)
|
|
||||||
? readFileSync(ETAG_FILE, "utf-8")
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const res = await fetch(BASE_URL + filename, {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": "Vencord (https://github.com/Vendicated/Vencord)",
|
|
||||||
"If-None-Match": etag
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 304) {
|
|
||||||
console.log("Up to date, not redownloading!");
|
|
||||||
return outputFile;
|
|
||||||
}
|
|
||||||
if (!res.ok)
|
|
||||||
throw new Error(`Failed to download installer: ${res.status} ${res.statusText}`);
|
|
||||||
|
|
||||||
writeFileSync(ETAG_FILE, res.headers.get("etag"));
|
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
|
||||||
console.log("Unzipping...");
|
|
||||||
const zip = new Uint8Array(await res.arrayBuffer());
|
|
||||||
|
|
||||||
const ff = await import("fflate");
|
|
||||||
const bytes = ff.unzipSync(zip, {
|
|
||||||
filter: f => f.name === INSTALLER_PATH_DARWIN
|
|
||||||
})[INSTALLER_PATH_DARWIN];
|
|
||||||
|
|
||||||
writeFileSync(outputFile, bytes, { mode: 0o755 });
|
|
||||||
|
|
||||||
console.log("Overriding security policy for installer binary (this is required to run it)");
|
|
||||||
console.log("xattr might error, that's okay");
|
|
||||||
|
|
||||||
const logAndRun = cmd => {
|
|
||||||
console.log("Running", cmd);
|
|
||||||
try {
|
|
||||||
execSync(cmd);
|
|
||||||
} catch { }
|
|
||||||
};
|
|
||||||
logAndRun(`sudo spctl --add '${outputFile}' --label "Vencord Installer"`);
|
|
||||||
logAndRun(`sudo xattr -d com.apple.quarantine '${outputFile}'`);
|
|
||||||
} else {
|
|
||||||
// WHY DOES NODE FETCH RETURN A WEB STREAM OH MY GOD
|
|
||||||
const body = Readable.fromWeb(res.body);
|
|
||||||
await finished(body.pipe(createWriteStream(outputFile, {
|
|
||||||
mode: 0o755,
|
|
||||||
autoClose: true
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Finished downloading!");
|
|
||||||
|
|
||||||
return outputFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const installerBin = await ensureBinary();
|
|
||||||
|
|
||||||
console.log("Now running Installer...");
|
|
||||||
|
|
||||||
execFileSync(installerBin, {
|
|
||||||
stdio: "inherit",
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
VENCORD_USER_DATA_DIR: BASE_DIR,
|
|
||||||
VENCORD_DEV_INSTALL: "1"
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,30 +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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} filePath
|
|
||||||
* @returns {string | null}
|
|
||||||
*/
|
|
||||||
export function getPluginTarget(filePath) {
|
|
||||||
const pathParts = filePath.split(/[/\\]/);
|
|
||||||
if (/^index\.tsx?$/.test(filePath.at(-1))) pathParts.pop();
|
|
||||||
|
|
||||||
const identifier = pathParts.at(-1).replace(/\.tsx?$/, "");
|
|
||||||
const identiferBits = identifier.split(".");
|
|
||||||
return identiferBits.length === 1 ? null : identiferBits.at(-1);
|
|
||||||
}
|
|
120
src/Vencord.ts
120
src/Vencord.ts
@ -22,121 +22,45 @@ export * as Util from "./utils";
|
|||||||
export * as QuickCss from "./utils/quickCss";
|
export * as QuickCss from "./utils/quickCss";
|
||||||
export * as Updater from "./utils/updater";
|
export * as Updater from "./utils/updater";
|
||||||
export * as Webpack from "./webpack";
|
export * as Webpack from "./webpack";
|
||||||
export { PlainSettings, Settings };
|
|
||||||
|
|
||||||
import "./utils/quickCss";
|
import { popNotice, showNotice } from "./api/Notices";
|
||||||
|
import { PlainSettings,Settings } from "./api/settings";
|
||||||
|
import { startAllPlugins } from "./plugins";
|
||||||
|
|
||||||
|
export { PlainSettings,Settings };
|
||||||
|
|
||||||
import "./webpack/patchWebpack";
|
import "./webpack/patchWebpack";
|
||||||
|
import "./utils/quickCss";
|
||||||
|
|
||||||
import { get as dsGet } from "./api/DataStore";
|
import { checkForUpdates, UpdateLogger } from "./utils/updater";
|
||||||
import { showNotification } from "./api/Notifications";
|
|
||||||
import { PlainSettings, Settings } from "./api/Settings";
|
|
||||||
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
|
||||||
import { localStorage } from "./utils/localStorage";
|
|
||||||
import { relaunch } from "./utils/native";
|
|
||||||
import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
|
|
||||||
import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
|
|
||||||
import { onceReady } from "./webpack";
|
import { onceReady } from "./webpack";
|
||||||
import { SettingsRouter } from "./webpack/common";
|
import { Router } from "./webpack/common";
|
||||||
|
|
||||||
async function syncSettings() {
|
export let Components: any;
|
||||||
// pre-check for local shared settings
|
|
||||||
if (
|
|
||||||
Settings.cloud.authenticated &&
|
|
||||||
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({
|
|
||||||
title: "Cloud Integrations",
|
|
||||||
body: "We've noticed you have cloud integrations enabled in another client! Due to limitations, you will " +
|
|
||||||
"need to re-authenticate to continue using them. Click here to go to the settings page to do so!",
|
|
||||||
color: "var(--yellow-360)",
|
|
||||||
onClick: () => SettingsRouter.open("VencordCloud")
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
Settings.cloud.settingsSync && // if it's enabled
|
|
||||||
Settings.cloud.authenticated // if cloud integrations are enabled
|
|
||||||
) {
|
|
||||||
if (localStorage.Vencord_settingsDirty) {
|
|
||||||
await putCloudSettings();
|
|
||||||
delete localStorage.Vencord_settingsDirty;
|
|
||||||
} else if (await getCloudSettings(false)) { // if we synchronized something (false means no sync)
|
|
||||||
// we show a notification here instead of allowing getCloudSettings() to show one to declutter the amount of
|
|
||||||
// potential notifications that might occur. getCloudSettings() will always send a notification regardless if
|
|
||||||
// there was an error to notify the user, but besides that we only want to show one notification instead of all
|
|
||||||
// of the possible ones it has (such as when your settings are newer).
|
|
||||||
showNotification({
|
|
||||||
title: "Cloud Settings",
|
|
||||||
body: "Your settings have been updated! Click here to restart to fully apply changes!",
|
|
||||||
color: "var(--green-360)",
|
|
||||||
onClick: relaunch
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await onceReady;
|
await onceReady;
|
||||||
startAllPlugins();
|
startAllPlugins();
|
||||||
|
Components = await import("./components");
|
||||||
syncSettings();
|
|
||||||
|
|
||||||
if (!IS_WEB) {
|
if (!IS_WEB) {
|
||||||
try {
|
try {
|
||||||
const isOutdated = await checkForUpdates();
|
const isOutdated = await checkForUpdates();
|
||||||
if (!isOutdated) return;
|
if (isOutdated && Settings.notifyAboutUpdates)
|
||||||
|
setTimeout(() => {
|
||||||
if (Settings.autoUpdate) {
|
showNotice(
|
||||||
await update();
|
"A Vencord update is available!",
|
||||||
if (Settings.autoUpdateNotification)
|
"View Update",
|
||||||
setTimeout(() => showNotification({
|
() => {
|
||||||
title: "Vencord has been updated!",
|
popNotice();
|
||||||
body: "Click here to restart",
|
Router.open("VencordUpdater");
|
||||||
permanent: true,
|
}
|
||||||
noPersist: true,
|
);
|
||||||
onClick: relaunch
|
}, 10000);
|
||||||
}), 10_000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
} catch (err) {
|
||||||
UpdateLogger.error("Failed to check for updates", err);
|
UpdateLogger.error("Failed to check for updates", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_DEV) {
|
|
||||||
const pendingPatches = patches.filter(p => !p.all && p.predicate?.() !== false);
|
|
||||||
if (pendingPatches.length)
|
|
||||||
PMLogger.warn(
|
|
||||||
"Webpack has finished initialising, but some patches haven't been applied yet.",
|
|
||||||
"This might be expected since some Modules are lazy loaded, but please verify",
|
|
||||||
"that all plugins are working as intended.",
|
|
||||||
"You are seeing this warning because this is a Development build of Vencord.",
|
|
||||||
"\nThe following patches have not been applied:",
|
|
||||||
"\n\n" + pendingPatches.map(p => `${p.plugin}: ${p.find}`).join("\n")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
@ -1,71 +1,50 @@
|
|||||||
/*
|
/*
|
||||||
* Vencord, a Discord client mod
|
* Vencord, a modification for Discord's desktop app
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
* Copyright (c) 2022
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
*
|
||||||
*/
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
import { IpcEvents } from "@utils/IpcEvents";
|
import { IpcRenderer, ipcRenderer } from "electron";
|
||||||
import { IpcRes } from "@utils/types";
|
|
||||||
import { ipcRenderer } from "electron";
|
|
||||||
import type { UserThemeHeader } from "main/themes";
|
|
||||||
|
|
||||||
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
|
import IPC_EVENTS from "./utils/IpcEvents";
|
||||||
return ipcRenderer.invoke(event, ...args) as Promise<T>;
|
|
||||||
|
function assertEventAllowed(event: string) {
|
||||||
|
if (!(event in IPC_EVENTS)) throw new Error(`Event ${event} not allowed.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendSync<T = any>(event: IpcEvents, ...args: any[]) {
|
|
||||||
return ipcRenderer.sendSync(event, ...args) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
themes: {
|
getVersions: () => process.versions,
|
||||||
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData),
|
ipc: {
|
||||||
deleteTheme: (fileName: string) => invoke<void>(IpcEvents.DELETE_THEME, fileName),
|
send(event: string, ...args: any[]) {
|
||||||
getThemesDir: () => invoke<string>(IpcEvents.GET_THEMES_DIR),
|
assertEventAllowed(event);
|
||||||
getThemesList: () => invoke<Array<UserThemeHeader>>(IpcEvents.GET_THEMES_LIST),
|
ipcRenderer.send(event, ...args);
|
||||||
getThemeData: (fileName: string) => invoke<string | undefined>(IpcEvents.GET_THEME_DATA, fileName)
|
|
||||||
},
|
|
||||||
|
|
||||||
updater: {
|
|
||||||
getUpdates: () => invoke<IpcRes<Record<"hash" | "author" | "message", string>[]>>(IpcEvents.GET_UPDATES),
|
|
||||||
update: () => invoke<IpcRes<boolean>>(IpcEvents.UPDATE),
|
|
||||||
rebuild: () => invoke<IpcRes<boolean>>(IpcEvents.BUILD),
|
|
||||||
getRepo: () => invoke<IpcRes<string>>(IpcEvents.GET_REPO),
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: {
|
|
||||||
get: () => sendSync<string>(IpcEvents.GET_SETTINGS),
|
|
||||||
set: (settings: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings),
|
|
||||||
getSettingsDir: () => invoke<string>(IpcEvents.GET_SETTINGS_DIR),
|
|
||||||
},
|
|
||||||
|
|
||||||
quickCss: {
|
|
||||||
get: () => invoke<string>(IpcEvents.GET_QUICK_CSS),
|
|
||||||
set: (css: string) => invoke<void>(IpcEvents.SET_QUICK_CSS, css),
|
|
||||||
|
|
||||||
addChangeListener(cb: (newCss: string) => void) {
|
|
||||||
ipcRenderer.on(IpcEvents.QUICK_CSS_UPDATE, (_, css) => cb(css));
|
|
||||||
},
|
},
|
||||||
|
sendSync<T = any>(event: string, ...args: any[]): T {
|
||||||
addThemeChangeListener(cb: () => void) {
|
assertEventAllowed(event);
|
||||||
ipcRenderer.on(IpcEvents.THEME_UPDATE, cb);
|
return ipcRenderer.sendSync(event, ...args);
|
||||||
},
|
},
|
||||||
|
on(event: string, listener: Parameters<IpcRenderer["on"]>[1]) {
|
||||||
openFile: () => invoke<void>(IpcEvents.OPEN_QUICKCSS),
|
assertEventAllowed(event);
|
||||||
openEditor: () => invoke<void>(IpcEvents.OPEN_MONACO_EDITOR),
|
ipcRenderer.on(event, listener);
|
||||||
},
|
|
||||||
|
|
||||||
native: {
|
|
||||||
getVersions: () => process.versions as Partial<NodeJS.ProcessVersions>,
|
|
||||||
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url)
|
|
||||||
},
|
|
||||||
|
|
||||||
pluginHelpers: {
|
|
||||||
OpenInApp: {
|
|
||||||
resolveRedirect: (url: string) => invoke<string>(IpcEvents.OPEN_IN_APP__RESOLVE_REDIRECT, url),
|
|
||||||
},
|
},
|
||||||
VoiceMessages: {
|
off(event: string, listener: Parameters<IpcRenderer["off"]>[1]) {
|
||||||
readRecording: (path: string) => invoke<Uint8Array | null>(IpcEvents.VOICE_MESSAGES_READ_RECORDING, path),
|
assertEventAllowed(event);
|
||||||
|
ipcRenderer.off(event, listener);
|
||||||
|
},
|
||||||
|
invoke<T = any>(event: string, ...args: any[]): Promise<T> {
|
||||||
|
assertEventAllowed(event);
|
||||||
|
return ipcRenderer.invoke(event, ...args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,110 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { User } from "discord-types/general";
|
|
||||||
import { ComponentType, HTMLProps } from "react";
|
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
|
||||||
|
|
||||||
export const enum BadgePosition {
|
|
||||||
START,
|
|
||||||
END
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProfileBadge {
|
|
||||||
/** The tooltip to show on hover. Required for image badges */
|
|
||||||
description?: string;
|
|
||||||
/** Custom component for the badge (tooltip not included) */
|
|
||||||
component?: ComponentType<ProfileBadge & BadgeUserArgs>;
|
|
||||||
/** The custom image to use */
|
|
||||||
image?: string;
|
|
||||||
link?: string;
|
|
||||||
/** Action to perform when you click the badge */
|
|
||||||
onClick?(): void;
|
|
||||||
/** Should the user display this badge? */
|
|
||||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
|
||||||
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
|
||||||
props?: HTMLProps<HTMLImageElement>;
|
|
||||||
/** Insert at start or end? */
|
|
||||||
position?: BadgePosition;
|
|
||||||
/** The badge name to display, Discord uses this. Required for component badges */
|
|
||||||
key?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Badges = new Set<ProfileBadge>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a new badge with the Badges API
|
|
||||||
* @param badge The badge to register
|
|
||||||
*/
|
|
||||||
export function addBadge(badge: ProfileBadge) {
|
|
||||||
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
|
|
||||||
Badges.add(badge);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregister a badge from the Badges API
|
|
||||||
* @param badge The badge to remove
|
|
||||||
*/
|
|
||||||
export function removeBadge(badge: ProfileBadge) {
|
|
||||||
return Badges.delete(badge);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject badges into the profile badges array.
|
|
||||||
* You probably don't need to use this.
|
|
||||||
*/
|
|
||||||
export function _getBadges(args: BadgeUserArgs) {
|
|
||||||
const badges = [] as ProfileBadge[];
|
|
||||||
for (const badge of Badges) {
|
|
||||||
if (!badge.shouldShow || badge.shouldShow(args)) {
|
|
||||||
badge.position === BadgePosition.START
|
|
||||||
? badges.unshift({ ...badge, ...args })
|
|
||||||
: badges.push({ ...badge, ...args });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
user: User;
|
|
||||||
profile: Profile;
|
|
||||||
premiumSince: Date;
|
|
||||||
premiumGuildSince?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConnectedAccount {
|
|
||||||
type: string;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
verified: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Profile {
|
|
||||||
connectedAccounts: ConnectedAccount[];
|
|
||||||
premiumType: number;
|
|
||||||
premiumSince: string;
|
|
||||||
premiumGuildSince?: any;
|
|
||||||
lastFetched: number;
|
|
||||||
profileFetchFailed: boolean;
|
|
||||||
application?: any;
|
|
||||||
}
|
|
@ -16,16 +16,18 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mergeDefaults } from "@utils/misc";
|
|
||||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
|
||||||
import { SnowflakeUtils } from "@webpack/common";
|
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
import type { PartialDeep } from "type-fest";
|
import type { PartialDeep } from "type-fest";
|
||||||
|
|
||||||
|
import { lazyWebpack, mergeDefaults } from "../../utils/misc";
|
||||||
|
import { filters, waitFor } from "../../webpack";
|
||||||
import { Argument } from "./types";
|
import { Argument } from "./types";
|
||||||
|
|
||||||
const createBotMessage = findByCodeLazy('username:"Clyde"');
|
const createBotMessage = lazyWebpack(filters.byCode('username:"Clyde"'));
|
||||||
const MessageSender = findByPropsLazy("receiveMessage");
|
const MessageSender = lazyWebpack(filters.byProps(["receiveMessage"]));
|
||||||
|
|
||||||
|
let SnowflakeUtils: any;
|
||||||
|
waitFor("fromTimestamp", m => SnowflakeUtils = m);
|
||||||
|
|
||||||
export function generateId() {
|
export function generateId() {
|
||||||
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
|
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
|
||||||
|
@ -16,10 +16,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { makeCodeblock } from "@utils/text";
|
import { makeCodeblock } from "../../utils/misc";
|
||||||
|
import { generateId, sendBotMessage } from "./commandHelpers";
|
||||||
import { sendBotMessage } from "./commandHelpers";
|
import { ApplicationCommandInputType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
|
||||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
|
|
||||||
|
|
||||||
export * from "./commandHelpers";
|
export * from "./commandHelpers";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
@ -80,12 +79,7 @@ export const _handleCommand = function (cmd: Command, args: Argument[], ctx: Com
|
|||||||
}
|
}
|
||||||
} as never;
|
} as never;
|
||||||
|
|
||||||
|
function modifyOpt(opt: Option | Command) {
|
||||||
/**
|
|
||||||
* Prepare a Command Option for Discord by filling missing fields
|
|
||||||
* @param opt
|
|
||||||
*/
|
|
||||||
export function prepareOption<O extends Option | Command>(opt: O): O {
|
|
||||||
opt.displayName ||= opt.name;
|
opt.displayName ||= opt.name;
|
||||||
opt.displayDescription ||= opt.description;
|
opt.displayDescription ||= opt.description;
|
||||||
opt.options?.forEach((opt, i, opts) => {
|
opt.options?.forEach((opt, i, opts) => {
|
||||||
@ -94,37 +88,11 @@ export function prepareOption<O extends Option | Command>(opt: O): O {
|
|||||||
else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;
|
else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;
|
||||||
opt.choices?.forEach(x => x.displayName ||= x.name);
|
opt.choices?.forEach(x => x.displayName ||= x.name);
|
||||||
|
|
||||||
prepareOption(opts[i]);
|
modifyOpt(opts[i]);
|
||||||
});
|
|
||||||
return opt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Yes, Discord registers individual commands for each subcommand
|
|
||||||
// TODO: This probably doesn't support nested subcommands. If that is ever needed,
|
|
||||||
// investigate
|
|
||||||
function registerSubCommands(cmd: Command, plugin: string) {
|
|
||||||
cmd.options?.forEach(o => {
|
|
||||||
if (o.type !== ApplicationCommandOptionType.SUB_COMMAND)
|
|
||||||
throw new Error("When specifying sub-command options, all options must be sub-commands.");
|
|
||||||
const subCmd = {
|
|
||||||
...cmd,
|
|
||||||
...o,
|
|
||||||
type: ApplicationCommandType.CHAT_INPUT,
|
|
||||||
name: `${cmd.name} ${o.name}`,
|
|
||||||
id: `${o.name}-${cmd.id}`,
|
|
||||||
displayName: `${cmd.name} ${o.name}`,
|
|
||||||
subCommandPath: [{
|
|
||||||
name: o.name,
|
|
||||||
type: o.type,
|
|
||||||
displayName: o.name
|
|
||||||
}],
|
|
||||||
rootCommand: cmd
|
|
||||||
};
|
|
||||||
registerCommand(subCmd as any, plugin);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerCommand<C extends Command>(command: C, plugin: string) {
|
export function registerCommand(command: Command, plugin: string) {
|
||||||
if (!BUILT_IN) {
|
if (!BUILT_IN) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[CommandsAPI]",
|
"[CommandsAPI]",
|
||||||
@ -138,19 +106,13 @@ export function registerCommand<C extends Command>(command: C, plugin: string) {
|
|||||||
throw new Error(`Command '${command.name}' already exists.`);
|
throw new Error(`Command '${command.name}' already exists.`);
|
||||||
|
|
||||||
command.isVencordCommand = true;
|
command.isVencordCommand = true;
|
||||||
command.id ??= `-${BUILT_IN.length + 1}`;
|
command.id ??= generateId();
|
||||||
command.applicationId ??= "-1"; // BUILT_IN;
|
command.applicationId ??= "-1"; // BUILT_IN;
|
||||||
command.type ??= ApplicationCommandType.CHAT_INPUT;
|
command.type ??= ApplicationCommandType.CHAT_INPUT;
|
||||||
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
|
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
|
||||||
command.plugin ||= plugin;
|
command.plugin ||= plugin;
|
||||||
|
|
||||||
prepareOption(command);
|
modifyOpt(command);
|
||||||
|
|
||||||
if (command.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND) {
|
|
||||||
registerSubCommands(command, plugin);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
commands[command.name] = command;
|
commands[command.name] = command;
|
||||||
BUILT_IN.push(command);
|
BUILT_IN.push(command);
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ export interface CommandContext {
|
|||||||
guild?: Guild;
|
guild?: Guild;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum ApplicationCommandOptionType {
|
export enum ApplicationCommandOptionType {
|
||||||
SUB_COMMAND = 1,
|
SUB_COMMAND = 1,
|
||||||
SUB_COMMAND_GROUP = 2,
|
SUB_COMMAND_GROUP = 2,
|
||||||
STRING = 3,
|
STRING = 3,
|
||||||
@ -38,7 +38,7 @@ export const enum ApplicationCommandOptionType {
|
|||||||
ATTACHMENT = 11,
|
ATTACHMENT = 11,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum ApplicationCommandInputType {
|
export enum ApplicationCommandInputType {
|
||||||
BUILT_IN = 0,
|
BUILT_IN = 0,
|
||||||
BUILT_IN_TEXT = 1,
|
BUILT_IN_TEXT = 1,
|
||||||
BUILT_IN_INTEGRATION = 2,
|
BUILT_IN_INTEGRATION = 2,
|
||||||
@ -64,7 +64,7 @@ export interface ChoicesOption {
|
|||||||
displayName?: string;
|
displayName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum ApplicationCommandType {
|
export enum ApplicationCommandType {
|
||||||
CHAT_INPUT = 1,
|
CHAT_INPUT = 1,
|
||||||
USER = 2,
|
USER = 2,
|
||||||
MESSAGE = 3,
|
MESSAGE = 3,
|
||||||
@ -81,7 +81,6 @@ export interface Argument {
|
|||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
focused: undefined;
|
focused: undefined;
|
||||||
options: Argument[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Command {
|
export interface Command {
|
||||||
|
@ -1,158 +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 { Logger } from "@utils/Logger";
|
|
||||||
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>) => 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>) => ContextMenuPatchCallbackReturn;
|
|
||||||
|
|
||||||
const ContextMenuLogger = new Logger("ContextMenu");
|
|
||||||
|
|
||||||
export const navPatches = new Map<string, Set<NavContextMenuPatchCallback>>();
|
|
||||||
export const globalPatches = new Set<GlobalContextMenuPatchCallback>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a context menu patch
|
|
||||||
* @param navId The navId(s) for the context menu(s) to patch
|
|
||||||
* @param patch The patch to be applied
|
|
||||||
*/
|
|
||||||
export function addContextMenuPatch(navId: string | Array<string>, patch: NavContextMenuPatchCallback) {
|
|
||||||
if (!Array.isArray(navId)) navId = [navId];
|
|
||||||
for (const id of navId) {
|
|
||||||
let contextMenuPatches = navPatches.get(id);
|
|
||||||
if (!contextMenuPatches) {
|
|
||||||
contextMenuPatches = new Set();
|
|
||||||
navPatches.set(id, contextMenuPatches);
|
|
||||||
}
|
|
||||||
|
|
||||||
contextMenuPatches.add(patch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a global context menu patch that fires the patch for all context menus
|
|
||||||
* @param patch The patch to be applied
|
|
||||||
*/
|
|
||||||
export function addGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback) {
|
|
||||||
globalPatches.add(patch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a context menu patch
|
|
||||||
* @param navId The navId(s) for the context menu(s) to remove the patch
|
|
||||||
* @param patch The patch to be removed
|
|
||||||
* @returns Wheter the patch was sucessfully removed from the context menu(s)
|
|
||||||
*/
|
|
||||||
export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> {
|
|
||||||
const navIds = Array.isArray(navId) ? navId : [navId as string];
|
|
||||||
|
|
||||||
const results = navIds.map(id => navPatches.get(id)?.delete(patch) ?? false);
|
|
||||||
|
|
||||||
return (Array.isArray(navId) ? results : results[0]) as T extends string ? boolean : Array<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a global context menu patch
|
|
||||||
* @param patch The patch to be removed
|
|
||||||
* @returns Wheter the patch was sucessfully removed
|
|
||||||
*/
|
|
||||||
export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean {
|
|
||||||
return globalPatches.delete(patch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper function for finding the children array of a group nested inside a context menu based on the id(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
|
|
||||||
*/
|
|
||||||
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(id) && id.some(id => child.props?.id === id))
|
|
||||||
|| child.props?.id === id
|
|
||||||
) return _itemsArray ?? null;
|
|
||||||
|
|
||||||
let nextChildren = child.props?.children;
|
|
||||||
if (nextChildren) {
|
|
||||||
if (!Array.isArray(nextChildren)) {
|
|
||||||
nextChildren = [nextChildren];
|
|
||||||
child.props.children = nextChildren;
|
|
||||||
}
|
|
||||||
|
|
||||||
const found = findGroupChildrenByChildId(id, nextChildren, nextChildren);
|
|
||||||
if (found !== null) return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContextMenuProps {
|
|
||||||
contextMenuApiArguments?: Array<any>;
|
|
||||||
navId: string;
|
|
||||||
children: Array<ReactElement | null>;
|
|
||||||
"aria-label": string;
|
|
||||||
onSelect: (() => void) | undefined;
|
|
||||||
onClose: (callback: (...args: Array<any>) => any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const patchedMenus = new WeakSet();
|
|
||||||
|
|
||||||
export function _patchContextMenu(props: ContextMenuProps) {
|
|
||||||
props.contextMenuApiArguments ??= [];
|
|
||||||
const contextMenuPatches = navPatches.get(props.navId);
|
|
||||||
|
|
||||||
if (!Array.isArray(props.children)) props.children = [props.children];
|
|
||||||
|
|
||||||
if (contextMenuPatches) {
|
|
||||||
for (const patch of contextMenuPatches) {
|
|
||||||
try {
|
|
||||||
const callback = patch(props.children, ...props.contextMenuApiArguments);
|
|
||||||
if (!patchedMenus.has(props)) callback?.();
|
|
||||||
} catch (err) {
|
|
||||||
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const patch of globalPatches) {
|
|
||||||
try {
|
|
||||||
const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments);
|
|
||||||
if (!patchedMenus.has(props)) callback?.();
|
|
||||||
} catch (err) {
|
|
||||||
ContextMenuLogger.error("Global patch errored,", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patchedMenus.add(props);
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
/* eslint-disable simple-header/header */
|
/* eslint-disable header/header */
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* idb-keyval v6.2.0
|
* idb-keyval v6.2.0
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Channel, User } from "discord-types/general/index.js";
|
|
||||||
|
|
||||||
interface DecoratorProps {
|
|
||||||
activities: any[];
|
|
||||||
canUseAvatarDecorations: boolean;
|
|
||||||
channel: Channel;
|
|
||||||
/**
|
|
||||||
* Only for DM members
|
|
||||||
*/
|
|
||||||
channelName?: string;
|
|
||||||
/**
|
|
||||||
* Only for server members
|
|
||||||
*/
|
|
||||||
currentUser?: User;
|
|
||||||
guildId?: string;
|
|
||||||
isMobile: boolean;
|
|
||||||
isOwner?: boolean;
|
|
||||||
isTyping: boolean;
|
|
||||||
selected: boolean;
|
|
||||||
status: string;
|
|
||||||
user: User;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
export type Decorator = (props: DecoratorProps) => JSX.Element | null;
|
|
||||||
type OnlyIn = "guilds" | "dms";
|
|
||||||
|
|
||||||
export const decorators = new Map<string, { decorator: Decorator, onlyIn?: OnlyIn; }>();
|
|
||||||
|
|
||||||
export function addDecorator(identifier: string, decorator: Decorator, onlyIn?: OnlyIn) {
|
|
||||||
decorators.set(identifier, { decorator, onlyIn });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeDecorator(identifier: string) {
|
|
||||||
decorators.delete(identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function __addDecoratorsToList(props: DecoratorProps): (JSX.Element | null)[] {
|
|
||||||
const isInGuild = !!(props.guildId);
|
|
||||||
return [...decorators.values()].map(decoratorObj => {
|
|
||||||
const { decorator, onlyIn } = decoratorObj;
|
|
||||||
// this can most likely be done cleaner
|
|
||||||
if (!onlyIn || (onlyIn === "guilds" && isInGuild) || (onlyIn === "dms" && !isInGuild)) {
|
|
||||||
return decorator(props);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type AccessoryCallback = (props: Record<string, any>) => JSX.Element | null | Array<JSX.Element | null>;
|
export type AccessoryCallback = (props: Record<string, any>) => JSX.Element;
|
||||||
export type Accessory = {
|
export type Accessory = {
|
||||||
callback: AccessoryCallback;
|
callback: AccessoryCallback;
|
||||||
position?: number;
|
position?: number;
|
||||||
@ -44,15 +44,6 @@ export function _modifyAccessories(
|
|||||||
props: Record<string, any>
|
props: Record<string, any>
|
||||||
) {
|
) {
|
||||||
for (const accessory of accessories.values()) {
|
for (const accessory of accessories.values()) {
|
||||||
let accessories = accessory.callback(props);
|
|
||||||
if (accessories == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!Array.isArray(accessories))
|
|
||||||
accessories = [accessories];
|
|
||||||
else if (accessories.length === 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
elements.splice(
|
elements.splice(
|
||||||
accessory.position != null
|
accessory.position != null
|
||||||
? accessory.position < 0
|
? accessory.position < 0
|
||||||
@ -60,7 +51,7 @@ export function _modifyAccessories(
|
|||||||
: accessory.position
|
: accessory.position
|
||||||
: elements.length,
|
: elements.length,
|
||||||
0,
|
0,
|
||||||
...accessories.filter(e => e != null) as JSX.Element[]
|
accessory.callback(props)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Channel, Message } from "discord-types/general/index.js";
|
|
||||||
|
|
||||||
interface DecorationProps {
|
|
||||||
author: {
|
|
||||||
/**
|
|
||||||
* Will be username if the user has no nickname
|
|
||||||
*/
|
|
||||||
nick: string;
|
|
||||||
iconRoleId: string;
|
|
||||||
guildMemberAvatar: string;
|
|
||||||
colorRoleName: string;
|
|
||||||
colorString: string;
|
|
||||||
};
|
|
||||||
channel: Channel;
|
|
||||||
compact: boolean;
|
|
||||||
decorations: {
|
|
||||||
/**
|
|
||||||
* Element for the [BOT] tag if there is one
|
|
||||||
*/
|
|
||||||
0: JSX.Element | null;
|
|
||||||
/**
|
|
||||||
* Other decorations (including ones added with this api)
|
|
||||||
*/
|
|
||||||
1: JSX.Element[];
|
|
||||||
};
|
|
||||||
message: Message;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
export type Decoration = (props: DecorationProps) => JSX.Element | null;
|
|
||||||
|
|
||||||
export const decorations = new Map<string, Decoration>();
|
|
||||||
|
|
||||||
export function addDecoration(identifier: string, decoration: Decoration) {
|
|
||||||
decorations.set(identifier, decoration);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeDecoration(identifier: string) {
|
|
||||||
decorations.delete(identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function __addDecorationsToMessage(props: DecorationProps): (JSX.Element | null)[] {
|
|
||||||
return [...decorations.values()].map(decoration => {
|
|
||||||
return decoration(props);
|
|
||||||
});
|
|
||||||
}
|
|
@ -16,91 +16,46 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Logger } from "@utils/Logger";
|
import type { Channel,Message } from "discord-types/general";
|
||||||
import { MessageStore } from "@webpack/common";
|
|
||||||
import { CustomEmoji } from "@webpack/types";
|
import Logger from "../utils/logger";
|
||||||
import type { Channel, Message } from "discord-types/general";
|
|
||||||
import type { Promisable } from "type-fest";
|
|
||||||
|
|
||||||
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
||||||
|
|
||||||
|
export interface Emoji {
|
||||||
|
require_colons: boolean,
|
||||||
|
originalName: string,
|
||||||
|
animated: boolean;
|
||||||
|
guildId: string,
|
||||||
|
name: string,
|
||||||
|
url: string,
|
||||||
|
id: string,
|
||||||
|
}
|
||||||
|
|
||||||
export interface MessageObject {
|
export interface MessageObject {
|
||||||
content: string,
|
content: string,
|
||||||
validNonShortcutEmojis: CustomEmoji[];
|
validNonShortcutEmojis: Emoji[];
|
||||||
invalidEmojis: any[];
|
|
||||||
tts: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Upload {
|
export type SendListener = (channelId: string, messageObj: MessageObject, extra: any) => void;
|
||||||
classification: string;
|
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
|
||||||
currentSize: number;
|
|
||||||
description: string | null;
|
|
||||||
filename: string;
|
|
||||||
id: string;
|
|
||||||
isImage: boolean;
|
|
||||||
isVideo: boolean;
|
|
||||||
item: {
|
|
||||||
file: File;
|
|
||||||
platform: number;
|
|
||||||
};
|
|
||||||
loaded: number;
|
|
||||||
mimeType: string;
|
|
||||||
preCompressionSize: number;
|
|
||||||
responseUrl: string;
|
|
||||||
sensitive: boolean;
|
|
||||||
showLargeMessageDialog: boolean;
|
|
||||||
spoiler: boolean;
|
|
||||||
status: "NOT_STARTED" | "STARTED" | "UPLOADING" | "ERROR" | "COMPLETED" | "CANCELLED";
|
|
||||||
uniqueId: string;
|
|
||||||
uploadedFilename: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageReplyOptions {
|
|
||||||
messageReference: Message["messageReference"];
|
|
||||||
allowedMentions?: {
|
|
||||||
parse: Array<string>;
|
|
||||||
repliedUser: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageExtra {
|
|
||||||
stickers?: string[];
|
|
||||||
uploads?: Upload[];
|
|
||||||
replyOptions: MessageReplyOptions;
|
|
||||||
content: string;
|
|
||||||
channel: Channel;
|
|
||||||
type?: any;
|
|
||||||
openWarningPopout: (props: any) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
|
|
||||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void>;
|
|
||||||
|
|
||||||
const sendListeners = new Set<SendListener>();
|
const sendListeners = new Set<SendListener>();
|
||||||
const editListeners = new Set<EditListener>();
|
const editListeners = new Set<EditListener>();
|
||||||
|
|
||||||
export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra, replyOptions: MessageReplyOptions) {
|
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: any) {
|
||||||
extra.replyOptions = replyOptions;
|
|
||||||
for (const listener of sendListeners) {
|
for (const listener of sendListeners) {
|
||||||
try {
|
try {
|
||||||
const result = await listener(channelId, messageObj, extra);
|
listener(channelId, messageObj, extra);
|
||||||
if (result && result.cancel === true) {
|
} catch (e) { MessageEventsLogger.error(`MessageSendHandler: Listener encoutered an unknown error. (${e})`); }
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
MessageEventsLogger.error("MessageSendHandler: Listener encountered an unknown error\n", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
|
export function _handlePreEdit(channeld: string, messageId: string, messageObj: MessageObject) {
|
||||||
for (const listener of editListeners) {
|
for (const listener of editListeners) {
|
||||||
try {
|
try {
|
||||||
await listener(channelId, messageId, messageObj);
|
listener(channeld, messageId, messageObj);
|
||||||
} catch (e) {
|
} catch (e) { MessageEventsLogger.error(`MessageEditHandler: Listener encoutered an unknown error. (${e})`); }
|
||||||
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,14 +87,10 @@ type ClickListener = (message: Message, channel: Channel, event: MouseEvent) =>
|
|||||||
const listeners = new Set<ClickListener>();
|
const listeners = new Set<ClickListener>();
|
||||||
|
|
||||||
export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
|
export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
|
||||||
// message object may be outdated, so (try to) fetch latest one
|
|
||||||
message = MessageStore.getMessage(channel.id, message.id) ?? message;
|
|
||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
try {
|
try {
|
||||||
listener(message, channel, event);
|
listener(message, channel, event);
|
||||||
} catch (e) {
|
} catch (e) { MessageEventsLogger.error(`MessageClickHandler: Listener encoutered an unknown error. (${e})`); }
|
||||||
MessageEventsLogger.error("MessageClickHandler: Listener encountered an unknown error\n", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Logger } from "@utils/Logger";
|
|
||||||
import { Channel, Message } from "discord-types/general";
|
|
||||||
import type { MouseEventHandler } from "react";
|
|
||||||
|
|
||||||
const logger = new Logger("MessagePopover");
|
|
||||||
|
|
||||||
export interface ButtonItem {
|
|
||||||
key?: string,
|
|
||||||
label: string,
|
|
||||||
icon: React.ComponentType<any>,
|
|
||||||
message: Message,
|
|
||||||
channel: Channel,
|
|
||||||
onClick?: MouseEventHandler<HTMLButtonElement>,
|
|
||||||
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type getButtonItem = (message: Message) => ButtonItem | null;
|
|
||||||
|
|
||||||
export const buttons = new Map<string, getButtonItem>();
|
|
||||||
|
|
||||||
export function addButton(
|
|
||||||
identifier: string,
|
|
||||||
item: getButtonItem,
|
|
||||||
) {
|
|
||||||
buttons.set(identifier, item);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeButton(identifier: string) {
|
|
||||||
buttons.delete(identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function _buildPopoverElements(
|
|
||||||
msg: Message,
|
|
||||||
makeButton: (item: ButtonItem) => React.ComponentType
|
|
||||||
) {
|
|
||||||
const items = [] as React.ComponentType[];
|
|
||||||
|
|
||||||
for (const [identifier, getItem] of buttons.entries()) {
|
|
||||||
try {
|
|
||||||
const item = getItem(msg);
|
|
||||||
if (item) {
|
|
||||||
item.key ??= identifier;
|
|
||||||
items.push(makeButton(item));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`[${identifier}]`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { waitFor } from "@webpack";
|
import { waitFor } from "../webpack";
|
||||||
|
|
||||||
let NoticesModule: any;
|
let NoticesModule: any;
|
||||||
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
|
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
|
||||||
|
@ -1,123 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import "./styles.css";
|
|
||||||
|
|
||||||
import { useSettings } from "@api/Settings";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { classes } from "@utils/misc";
|
|
||||||
import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
|
|
||||||
|
|
||||||
import { NotificationData } from "./Notifications";
|
|
||||||
|
|
||||||
export default ErrorBoundary.wrap(function NotificationComponent({
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
richBody,
|
|
||||||
color,
|
|
||||||
icon,
|
|
||||||
onClick,
|
|
||||||
onClose,
|
|
||||||
image,
|
|
||||||
permanent,
|
|
||||||
className,
|
|
||||||
dismissOnClick
|
|
||||||
}: NotificationData & { className?: string; }) {
|
|
||||||
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
|
|
||||||
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
|
|
||||||
|
|
||||||
const [isHover, setIsHover] = useState(false);
|
|
||||||
const [elapsed, setElapsed] = useState(0);
|
|
||||||
|
|
||||||
const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isHover || !hasFocus || timeout === 0 || permanent) return void setElapsed(0);
|
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
const elapsed = Date.now() - start;
|
|
||||||
if (elapsed >= timeout)
|
|
||||||
onClose!();
|
|
||||||
else
|
|
||||||
setElapsed(elapsed);
|
|
||||||
}, 10);
|
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
}, [timeout, isHover, hasFocus]);
|
|
||||||
|
|
||||||
const timeoutProgress = elapsed / timeout;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={classes("vc-notification-root", className)}
|
|
||||||
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
|
|
||||||
onClick={() => {
|
|
||||||
onClick?.();
|
|
||||||
if (dismissOnClick !== false)
|
|
||||||
onClose!();
|
|
||||||
}}
|
|
||||||
onContextMenu={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
onClose!();
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => setIsHover(true)}
|
|
||||||
onMouseLeave={() => setIsHover(false)}
|
|
||||||
>
|
|
||||||
<div className="vc-notification">
|
|
||||||
{icon && <img className="vc-notification-icon" src={icon} alt="" />}
|
|
||||||
<div className="vc-notification-content">
|
|
||||||
<div className="vc-notification-header">
|
|
||||||
<h2 className="vc-notification-title">{title}</h2>
|
|
||||||
<button
|
|
||||||
className="vc-notification-close-btn"
|
|
||||||
onClick={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
onClose!();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
role="img"
|
|
||||||
aria-labelledby="vc-notification-dismiss-title"
|
|
||||||
>
|
|
||||||
<title id="vc-notification-dismiss-title">Dismiss Notification</title>
|
|
||||||
<path fill="currentColor" d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{image && <img className="vc-notification-img" src={image} alt="" />}
|
|
||||||
{timeout !== 0 && !permanent && (
|
|
||||||
<div
|
|
||||||
className="vc-notification-progressbar"
|
|
||||||
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}, {
|
|
||||||
onError: ({ props }) => props.onClose!()
|
|
||||||
});
|
|
@ -1,110 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Settings } from "@api/Settings";
|
|
||||||
import { Queue } from "@utils/Queue";
|
|
||||||
import { ReactDOM } from "@webpack/common";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import type { Root } from "react-dom/client";
|
|
||||||
|
|
||||||
import NotificationComponent from "./NotificationComponent";
|
|
||||||
import { persistNotification } from "./notificationLog";
|
|
||||||
|
|
||||||
const NotificationQueue = new Queue();
|
|
||||||
|
|
||||||
let reactRoot: Root;
|
|
||||||
let id = 42;
|
|
||||||
|
|
||||||
function getRoot() {
|
|
||||||
if (!reactRoot) {
|
|
||||||
const container = document.createElement("div");
|
|
||||||
container.id = "vc-notification-container";
|
|
||||||
document.body.append(container);
|
|
||||||
reactRoot = ReactDOM.createRoot(container);
|
|
||||||
}
|
|
||||||
return reactRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NotificationData {
|
|
||||||
title: string;
|
|
||||||
body: string;
|
|
||||||
/**
|
|
||||||
* Same as body but can be a custom component.
|
|
||||||
* Will be used over body if present.
|
|
||||||
* Not supported on desktop notifications, those will fall back to body */
|
|
||||||
richBody?: ReactNode;
|
|
||||||
/** Small icon. This is for things like profile pictures and should be square */
|
|
||||||
icon?: string;
|
|
||||||
/** Large image. Optimally, this should be around 16x9 but it doesn't matter much. Desktop Notifications might not support this */
|
|
||||||
image?: string;
|
|
||||||
onClick?(): void;
|
|
||||||
onClose?(): void;
|
|
||||||
color?: string;
|
|
||||||
/** Whether this notification should not have a timeout */
|
|
||||||
permanent?: boolean;
|
|
||||||
/** Whether this notification should not be persisted in the Notification Log */
|
|
||||||
noPersist?: boolean;
|
|
||||||
/** Whether this notification should be dismissed when clicked (defaults to true) */
|
|
||||||
dismissOnClick?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _showNotification(notification: NotificationData, id: number) {
|
|
||||||
const root = getRoot();
|
|
||||||
return new Promise<void>(resolve => {
|
|
||||||
root.render(
|
|
||||||
<NotificationComponent key={id} {...notification} onClose={() => {
|
|
||||||
notification.onClose?.();
|
|
||||||
root.render(null);
|
|
||||||
resolve();
|
|
||||||
}} />,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldBeNative() {
|
|
||||||
if (typeof Notification === "undefined") return false;
|
|
||||||
|
|
||||||
const { useNative } = Settings.notifications;
|
|
||||||
if (useNative === "always") return true;
|
|
||||||
if (useNative === "not-focused") return !document.hasFocus();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function requestPermission() {
|
|
||||||
return (
|
|
||||||
Notification.permission === "granted" ||
|
|
||||||
(Notification.permission !== "denied" && (await Notification.requestPermission()) === "granted")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function showNotification(data: NotificationData) {
|
|
||||||
persistNotification(data);
|
|
||||||
|
|
||||||
if (shouldBeNative() && await requestPermission()) {
|
|
||||||
const { title, body, icon, image, onClick = null, onClose = null } = data;
|
|
||||||
const n = new Notification(title, {
|
|
||||||
body,
|
|
||||||
icon,
|
|
||||||
image
|
|
||||||
});
|
|
||||||
n.onclick = onClick;
|
|
||||||
n.onclose = onClose;
|
|
||||||
} else {
|
|
||||||
NotificationQueue.push(() => _showNotification(data, id++));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from "./Notifications";
|
|
@ -1,203 +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 * as DataStore from "@api/DataStore";
|
|
||||||
import { Settings } from "@api/Settings";
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
|
||||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
|
||||||
import { useAwaiter } from "@utils/react";
|
|
||||||
import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import type { DispatchWithoutAction } from "react";
|
|
||||||
|
|
||||||
import NotificationComponent from "./NotificationComponent";
|
|
||||||
import type { NotificationData } from "./Notifications";
|
|
||||||
|
|
||||||
interface PersistentNotificationData extends Pick<NotificationData, "title" | "body" | "image" | "icon" | "color"> {
|
|
||||||
timestamp: number;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const KEY = "notification-log";
|
|
||||||
|
|
||||||
const getLog = async () => {
|
|
||||||
const log = await DataStore.get(KEY) as PersistentNotificationData[] | undefined;
|
|
||||||
return log ?? [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const cl = classNameFactory("vc-notification-log-");
|
|
||||||
const signals = new Set<DispatchWithoutAction>();
|
|
||||||
|
|
||||||
export async function persistNotification(notification: NotificationData) {
|
|
||||||
if (notification.noPersist) return;
|
|
||||||
|
|
||||||
const limit = Settings.notifications.logLimit;
|
|
||||||
if (limit === 0) return;
|
|
||||||
|
|
||||||
await DataStore.update(KEY, (old: PersistentNotificationData[] | undefined) => {
|
|
||||||
const log = old ?? [];
|
|
||||||
|
|
||||||
// Omit stuff we don't need
|
|
||||||
const {
|
|
||||||
onClick, onClose, richBody, permanent, noPersist, dismissOnClick,
|
|
||||||
...pureNotification
|
|
||||||
} = notification;
|
|
||||||
|
|
||||||
log.unshift({
|
|
||||||
...pureNotification,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
id: nanoid()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (log.length > limit && limit !== 200)
|
|
||||||
log.length = limit;
|
|
||||||
|
|
||||||
return log;
|
|
||||||
});
|
|
||||||
|
|
||||||
signals.forEach(x => x());
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteNotification(timestamp: number) {
|
|
||||||
const log = await getLog();
|
|
||||||
const index = log.findIndex(x => x.timestamp === timestamp);
|
|
||||||
if (index === -1) return;
|
|
||||||
|
|
||||||
log.splice(index, 1);
|
|
||||||
await DataStore.set(KEY, log);
|
|
||||||
signals.forEach(x => x());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useLogs() {
|
|
||||||
const [signal, setSignal] = useReducer(x => x + 1, 0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
signals.add(setSignal);
|
|
||||||
return () => void signals.delete(setSignal);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [log, _, pending] = useAwaiter(getLog, {
|
|
||||||
fallbackValue: [],
|
|
||||||
deps: [signal]
|
|
||||||
});
|
|
||||||
|
|
||||||
return [log, pending] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
|
||||||
const [removing, setRemoving] = useState(false);
|
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const div = ref.current!;
|
|
||||||
|
|
||||||
const setHeight = () => {
|
|
||||||
if (div.clientHeight === 0) return requestAnimationFrame(setHeight);
|
|
||||||
div.style.height = `${div.clientHeight}px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
setHeight();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cl("wrapper", { removing })} ref={ref}>
|
|
||||||
<NotificationComponent
|
|
||||||
{...data}
|
|
||||||
permanent={true}
|
|
||||||
dismissOnClick={false}
|
|
||||||
onClose={() => {
|
|
||||||
if (removing) return;
|
|
||||||
setRemoving(true);
|
|
||||||
|
|
||||||
setTimeout(() => deleteNotification(data.timestamp), 200);
|
|
||||||
}}
|
|
||||||
richBody={
|
|
||||||
<div className={cl("body")}>
|
|
||||||
{data.body}
|
|
||||||
<Timestamp timestamp={moment(data.timestamp)} className={cl("timestamp")} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) {
|
|
||||||
if (!log.length && !pending)
|
|
||||||
return (
|
|
||||||
<div className={cl("container")}>
|
|
||||||
<div className={cl("empty")} />
|
|
||||||
<Forms.FormText style={{ textAlign: "center" }}>
|
|
||||||
No notifications yet
|
|
||||||
</Forms.FormText>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cl("container")}>
|
|
||||||
{log.map(n => <NotificationEntry data={n} key={n.id} />)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) {
|
|
||||||
const [log, pending] = useLogs();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
|
|
||||||
<ModalHeader>
|
|
||||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
|
|
||||||
<ModalCloseButton onClick={close} />
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalContent>
|
|
||||||
<NotificationLog log={log} pending={pending} />
|
|
||||||
</ModalContent>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openNotificationLogModal() {
|
|
||||||
const key = openModal(modalProps => (
|
|
||||||
<LogModal
|
|
||||||
modalProps={modalProps}
|
|
||||||
close={() => closeModal(key)}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
}
|
|
@ -1,122 +0,0 @@
|
|||||||
.vc-notification-root {
|
|
||||||
/* clear default button styles */
|
|
||||||
all: unset;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
color: var(--text-normal);
|
|
||||||
background-color: var(--background-secondary-alt);
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 2147483647;
|
|
||||||
right: 1rem;
|
|
||||||
width: 25vw;
|
|
||||||
min-height: 10vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
padding: 1.25rem;
|
|
||||||
gap: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-title {
|
|
||||||
color: var(--header-primary);
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-close-btn {
|
|
||||||
all: unset;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--interactive-normal);
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-close-btn:hover {
|
|
||||||
color: var(--interactive-hover);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-icon {
|
|
||||||
height: 4rem;
|
|
||||||
width: 4rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-progressbar {
|
|
||||||
height: 0.25rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-p {
|
|
||||||
margin: 0.5rem 0 0;
|
|
||||||
line-height: 140%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-empty {
|
|
||||||
height: 218px;
|
|
||||||
background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 1em;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-wrapper {
|
|
||||||
transition: 200ms ease;
|
|
||||||
transition-property: height, opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-wrapper:not(:last-child) {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-removing {
|
|
||||||
height: 0 !important;
|
|
||||||
opacity: 0;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-timestamp {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-weight: lighter;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-notification-log-danger-btn {
|
|
||||||
color: var(--white-500);
|
|
||||||
background-color: var(--button-danger-background);
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Logger } from "@utils/Logger";
|
|
||||||
|
|
||||||
const logger = new Logger("ServerListAPI");
|
|
||||||
|
|
||||||
export const enum ServerListRenderPosition {
|
|
||||||
Above,
|
|
||||||
In,
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderFunctionsAbove = new Set<Function>();
|
|
||||||
const renderFunctionsIn = new Set<Function>();
|
|
||||||
|
|
||||||
function getRenderFunctions(position: ServerListRenderPosition) {
|
|
||||||
return position === ServerListRenderPosition.Above ? renderFunctionsAbove : renderFunctionsIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addServerListElement(position: ServerListRenderPosition, renderFunction: Function) {
|
|
||||||
getRenderFunctions(position).add(renderFunction);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeServerListElement(position: ServerListRenderPosition, renderFunction: Function) {
|
|
||||||
getRenderFunctions(position).delete(renderFunction);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const renderAll = (position: ServerListRenderPosition) => {
|
|
||||||
const ret: Array<JSX.Element> = [];
|
|
||||||
|
|
||||||
for (const renderFunction of getRenderFunctions(position)) {
|
|
||||||
try {
|
|
||||||
ret.unshift(renderFunction());
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Failed to render server list element:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
};
|
|
@ -1,69 +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, wreq } from "@webpack";
|
|
||||||
|
|
||||||
import { Settings } from "./Settings";
|
|
||||||
|
|
||||||
interface Setting<T> {
|
|
||||||
/**
|
|
||||||
* Get the setting value
|
|
||||||
*/
|
|
||||||
getSetting(): T;
|
|
||||||
/**
|
|
||||||
* Update the setting value
|
|
||||||
* @param value The new value
|
|
||||||
*/
|
|
||||||
updateSetting(value: T | ((old: T) => T)): Promise<void>;
|
|
||||||
/**
|
|
||||||
* React hook for automatically updating components when the setting is updated
|
|
||||||
*/
|
|
||||||
useSetting(): T;
|
|
||||||
settingsStoreApiGroup: string;
|
|
||||||
settingsStoreApiName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsStores: Array<Setting<any>> | undefined = proxyLazy(() => {
|
|
||||||
const modId = findModuleId('"textAndImages","renderSpoilers"');
|
|
||||||
if (modId == null) return new Logger("SettingsStoreAPI").error("Didn't find stores module.");
|
|
||||||
|
|
||||||
const mod = wreq(modId);
|
|
||||||
if (mod == null) return;
|
|
||||||
|
|
||||||
return Object.values(mod).filter((s: any) => s?.settingsStoreApiGroup) as any;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the store for a setting
|
|
||||||
* @param group The setting group
|
|
||||||
* @param name The name of the setting
|
|
||||||
*/
|
|
||||||
export function getSettingStore<T = any>(group: string, name: string): Setting<T> | undefined {
|
|
||||||
if (!Settings.plugins.SettingsStoreAPI.enabled) throw new Error("Cannot use SettingsStoreAPI without setting as dependency.");
|
|
||||||
|
|
||||||
return SettingsStores?.find(s => s?.settingsStoreApiGroup === group && s?.settingsStoreApiName === name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getSettingStore but lazy
|
|
||||||
*/
|
|
||||||
export function getSettingStoreLazy<T = any>(group: string, name: string) {
|
|
||||||
return proxyLazy(() => getSettingStore<T>(group, name));
|
|
||||||
}
|
|
@ -1,162 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { MapValue } from "type-fest/source/entry";
|
|
||||||
|
|
||||||
export type Style = MapValue<typeof VencordStyles>;
|
|
||||||
|
|
||||||
export const styleMap = window.VencordStyles ??= new Map();
|
|
||||||
|
|
||||||
export function requireStyle(name: string) {
|
|
||||||
const style = styleMap.get(name);
|
|
||||||
if (!style) throw new Error(`Style "${name}" does not exist`);
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A style's name can be obtained from importing a stylesheet with `?managed` at the end of the import
|
|
||||||
* @param name The name of the style
|
|
||||||
* @returns `false` if the style was already enabled, `true` otherwise
|
|
||||||
* @example
|
|
||||||
* import pluginStyle from "./plugin.css?managed";
|
|
||||||
*
|
|
||||||
* // Inside some plugin method like "start()" or "[option].onChange()"
|
|
||||||
* enableStyle(pluginStyle);
|
|
||||||
*/
|
|
||||||
export function enableStyle(name: string) {
|
|
||||||
const style = requireStyle(name);
|
|
||||||
|
|
||||||
if (style.dom?.isConnected)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!style.dom) {
|
|
||||||
style.dom = document.createElement("style");
|
|
||||||
style.dom.dataset.vencordName = style.name;
|
|
||||||
}
|
|
||||||
compileStyle(style);
|
|
||||||
|
|
||||||
document.head.appendChild(style.dom);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param name The name of the style
|
|
||||||
* @returns `false` if the style was already disabled, `true` otherwise
|
|
||||||
* @see {@link enableStyle} for info on getting the name of an imported style
|
|
||||||
*/
|
|
||||||
export function disableStyle(name: string) {
|
|
||||||
const style = requireStyle(name);
|
|
||||||
if (!style.dom?.isConnected)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
style.dom.remove();
|
|
||||||
style.dom = null;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param name The name of the style
|
|
||||||
* @returns `true` in most cases, may return `false` in some edge cases
|
|
||||||
* @see {@link enableStyle} for info on getting the name of an imported style
|
|
||||||
*/
|
|
||||||
export const toggleStyle = (name: string) => isStyleEnabled(name) ? disableStyle(name) : enableStyle(name);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param name The name of the style
|
|
||||||
* @returns Whether the style is enabled
|
|
||||||
* @see {@link enableStyle} for info on getting the name of an imported style
|
|
||||||
*/
|
|
||||||
export const isStyleEnabled = (name: string) => requireStyle(name).dom?.isConnected ?? false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the variables of a style
|
|
||||||
* ```ts
|
|
||||||
* // -- plugin.ts --
|
|
||||||
* import pluginStyle from "./plugin.css?managed";
|
|
||||||
* import { setStyleVars } from "@api/Styles";
|
|
||||||
* import { findByPropsLazy } from "@webpack";
|
|
||||||
* const classNames = findByPropsLazy("thin", "scrollerBase"); // { thin: "thin-31rlnD scrollerBase-_bVAAt", ... }
|
|
||||||
*
|
|
||||||
* // Inside some plugin method like "start()"
|
|
||||||
* setStyleClassNames(pluginStyle, classNames);
|
|
||||||
* enableStyle(pluginStyle);
|
|
||||||
* ```
|
|
||||||
* ```scss
|
|
||||||
* // -- plugin.css --
|
|
||||||
* .plugin-root [--thin]::-webkit-scrollbar { ... }
|
|
||||||
* ```
|
|
||||||
* ```scss
|
|
||||||
* // -- final stylesheet --
|
|
||||||
* .plugin-root .thin-31rlnD.scrollerBase-_bVAAt::-webkit-scrollbar { ... }
|
|
||||||
* ```
|
|
||||||
* @param name The name of the style
|
|
||||||
* @param classNames An object where the keys are the variable names and the values are the variable values
|
|
||||||
* @param recompile Whether to recompile the style after setting the variables, defaults to `true`
|
|
||||||
* @see {@link enableStyle} for info on getting the name of an imported style
|
|
||||||
*/
|
|
||||||
export const setStyleClassNames = (name: string, classNames: Record<string, string>, recompile = true) => {
|
|
||||||
const style = requireStyle(name);
|
|
||||||
style.classNames = classNames;
|
|
||||||
if (recompile && isStyleEnabled(style.name))
|
|
||||||
compileStyle(style);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the stylesheet after doing the following to the sourcecode:
|
|
||||||
* - Interpolate style classnames
|
|
||||||
* @param style **_Must_ be a style with a DOM element**
|
|
||||||
* @see {@link setStyleClassNames} for more info on style classnames
|
|
||||||
*/
|
|
||||||
export const compileStyle = (style: Style) => {
|
|
||||||
if (!style.dom) throw new Error("Style has no DOM element");
|
|
||||||
|
|
||||||
style.dom.textContent = style.source
|
|
||||||
.replace(/\[--(\w+)\]/g, (match, name) => {
|
|
||||||
const className = style.classNames[name];
|
|
||||||
return className ? classNameToSelector(className) : match;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param name The classname
|
|
||||||
* @param prefix A prefix to add each class, defaults to `""`
|
|
||||||
* @return A css selector for the classname
|
|
||||||
* @example
|
|
||||||
* classNameToSelector("foo bar") // => ".foo.bar"
|
|
||||||
*/
|
|
||||||
export const classNameToSelector = (name: string, prefix = "") => name.split(" ").map(n => `.${prefix}${n}`).join("");
|
|
||||||
|
|
||||||
type ClassNameFactoryArg = string | string[] | Record<string, unknown> | false | null | undefined | 0 | "";
|
|
||||||
/**
|
|
||||||
* @param prefix The prefix to add to each class, defaults to `""`
|
|
||||||
* @returns A classname generator function
|
|
||||||
* @example
|
|
||||||
* const cl = classNameFactory("plugin-");
|
|
||||||
*
|
|
||||||
* cl("base", ["item", "editable"], { selected: null, disabled: true })
|
|
||||||
* // => "plugin-base plugin-item plugin-editable plugin-disabled"
|
|
||||||
*/
|
|
||||||
export const classNameFactory = (prefix: string = "") => (...args: ClassNameFactoryArg[]) => {
|
|
||||||
const classNames = new Set<string>();
|
|
||||||
for (const arg of args) {
|
|
||||||
if (arg && typeof arg === "string") classNames.add(arg);
|
|
||||||
else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name));
|
|
||||||
else if (arg && typeof arg === "object") Object.entries(arg).forEach(([name, value]) => value && classNames.add(name));
|
|
||||||
}
|
|
||||||
return Array.from(classNames, name => prefix + name).join(" ");
|
|
||||||
};
|
|
@ -16,21 +16,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as $Badges from "./Badges";
|
|
||||||
import * as $Commands from "./Commands";
|
import * as $Commands from "./Commands";
|
||||||
import * as $ContextMenu from "./ContextMenu";
|
|
||||||
import * as $DataStore from "./DataStore";
|
import * as $DataStore from "./DataStore";
|
||||||
import * as $MemberListDecorators from "./MemberListDecorators";
|
|
||||||
import * as $MessageAccessories from "./MessageAccessories";
|
import * as $MessageAccessories from "./MessageAccessories";
|
||||||
import * as $MessageDecorations from "./MessageDecorations";
|
|
||||||
import * as $MessageEventsAPI from "./MessageEvents";
|
import * as $MessageEventsAPI from "./MessageEvents";
|
||||||
import * as $MessagePopover from "./MessagePopover";
|
|
||||||
import * as $Notices from "./Notices";
|
import * as $Notices from "./Notices";
|
||||||
import * as $Notifications from "./Notifications";
|
|
||||||
import * as $ServerList from "./ServerList";
|
|
||||||
import * as $Settings from "./Settings";
|
|
||||||
import * as $SettingsStore from "./SettingsStore";
|
|
||||||
import * as $Styles from "./Styles";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An API allowing you to listen to Message Clicks or run your own logic
|
* An API allowing you to listen to Message Clicks or run your own logic
|
||||||
@ -38,16 +28,16 @@ import * as $Styles from "./Styles";
|
|||||||
*
|
*
|
||||||
* If your plugin uses this, you must add MessageEventsAPI to its dependencies
|
* If your plugin uses this, you must add MessageEventsAPI to its dependencies
|
||||||
*/
|
*/
|
||||||
export const MessageEvents = $MessageEventsAPI;
|
const MessageEvents = $MessageEventsAPI;
|
||||||
/**
|
/**
|
||||||
* An API allowing you to create custom notices
|
* An API allowing you to create custom notices
|
||||||
* (snackbars on the top, like the Update prompt)
|
* (snackbars on the top, like the Update prompt)
|
||||||
*/
|
*/
|
||||||
export const Notices = $Notices;
|
const Notices = $Notices;
|
||||||
/**
|
/**
|
||||||
* An API allowing you to register custom commands
|
* An API allowing you to register custom commands
|
||||||
*/
|
*/
|
||||||
export const Commands = $Commands;
|
const Commands = $Commands;
|
||||||
/**
|
/**
|
||||||
* A wrapper around IndexedDB. This can store arbitrarily
|
* A wrapper around IndexedDB. This can store arbitrarily
|
||||||
* large data and supports a lot of datatypes (Blob, Map, ...).
|
* large data and supports a lot of datatypes (Blob, Map, ...).
|
||||||
@ -62,50 +52,10 @@ export const Commands = $Commands;
|
|||||||
* This is actually just idb-keyval, so if you're familiar with that, you're golden!
|
* This is actually just idb-keyval, so if you're familiar with that, you're golden!
|
||||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types}
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types}
|
||||||
*/
|
*/
|
||||||
export const DataStore = $DataStore;
|
const DataStore = $DataStore;
|
||||||
/**
|
/**
|
||||||
* An API allowing you to add custom components as message accessories
|
* An API allowing you to add custom components as message accessories
|
||||||
*/
|
*/
|
||||||
export const MessageAccessories = $MessageAccessories;
|
const MessageAccessories = $MessageAccessories;
|
||||||
/**
|
|
||||||
* An API allowing you to add custom buttons in the message popover
|
|
||||||
*/
|
|
||||||
export const MessagePopover = $MessagePopover;
|
|
||||||
/**
|
|
||||||
* An API allowing you to add badges to user profiles
|
|
||||||
*/
|
|
||||||
export const Badges = $Badges;
|
|
||||||
/**
|
|
||||||
* An API allowing you to add custom elements to the server list
|
|
||||||
*/
|
|
||||||
export const ServerList = $ServerList;
|
|
||||||
/**
|
|
||||||
* An API allowing you to add components as message accessories
|
|
||||||
*/
|
|
||||||
export const MessageDecorations = $MessageDecorations;
|
|
||||||
/**
|
|
||||||
* An API allowing you to add components to member list users, in both DM's and servers
|
|
||||||
*/
|
|
||||||
export const MemberListDecorators = $MemberListDecorators;
|
|
||||||
/**
|
|
||||||
* An API allowing you to persist data
|
|
||||||
*/
|
|
||||||
export const Settings = $Settings;
|
|
||||||
/**
|
|
||||||
* An API allowing you to read, manipulate and automatically update components based on Discord settings
|
|
||||||
*/
|
|
||||||
export const SettingsStore = $SettingsStore;
|
|
||||||
/**
|
|
||||||
* An API allowing you to dynamically load styles
|
|
||||||
* a
|
|
||||||
*/
|
|
||||||
export const Styles = $Styles;
|
|
||||||
/**
|
|
||||||
* An API allowing you to display notifications
|
|
||||||
*/
|
|
||||||
export const Notifications = $Notifications;
|
|
||||||
|
|
||||||
/**
|
export { Commands,DataStore, MessageAccessories, MessageEvents, Notices };
|
||||||
* An api allowing you to patch and add/remove items to/from context menus
|
|
||||||
*/
|
|
||||||
export const ContextMenu = $ContextMenu;
|
|
||||||
|
@ -16,118 +16,57 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { debounce } from "@utils/debounce";
|
|
||||||
import { localStorage } from "@utils/localStorage";
|
|
||||||
import { Logger } from "@utils/Logger";
|
|
||||||
import { mergeDefaults } from "@utils/misc";
|
|
||||||
import { putCloudSettings } from "@utils/settingsSync";
|
|
||||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
|
||||||
import { React } from "@webpack/common";
|
|
||||||
|
|
||||||
import plugins from "~plugins";
|
import plugins from "~plugins";
|
||||||
|
|
||||||
const logger = new Logger("Settings");
|
import IpcEvents from "../utils/IpcEvents";
|
||||||
|
import { mergeDefaults } from "../utils/misc";
|
||||||
|
import { OptionType } from "../utils/types";
|
||||||
|
import { React } from "../webpack/common";
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
notifyAboutUpdates: boolean;
|
notifyAboutUpdates: boolean;
|
||||||
autoUpdate: boolean;
|
|
||||||
autoUpdateNotification: boolean,
|
|
||||||
useQuickCss: boolean;
|
useQuickCss: boolean;
|
||||||
enableReactDevtools: boolean;
|
enableReactDevtools: boolean;
|
||||||
themeLinks: string[];
|
|
||||||
enabledThemes: string[];
|
|
||||||
frameless: boolean;
|
|
||||||
transparent: boolean;
|
|
||||||
winCtrlQ: boolean;
|
|
||||||
macosTranslucency: boolean;
|
|
||||||
disableMinSize: boolean;
|
|
||||||
winNativeTitleBar: boolean;
|
|
||||||
plugins: {
|
plugins: {
|
||||||
[plugin: string]: {
|
[plugin: string]: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
[setting: string]: any;
|
[setting: string]: any;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
notifications: {
|
|
||||||
timeout: number;
|
|
||||||
position: "top-right" | "bottom-right";
|
|
||||||
useNative: "always" | "never" | "not-focused";
|
|
||||||
logLimit: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
cloud: {
|
|
||||||
authenticated: boolean;
|
|
||||||
url: string;
|
|
||||||
settingsSync: boolean;
|
|
||||||
settingsSyncVersion: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultSettings: Settings = {
|
const DefaultSettings: Settings = {
|
||||||
notifyAboutUpdates: true,
|
notifyAboutUpdates: true,
|
||||||
autoUpdate: false,
|
|
||||||
autoUpdateNotification: true,
|
|
||||||
useQuickCss: true,
|
useQuickCss: true,
|
||||||
themeLinks: [],
|
|
||||||
enabledThemes: [],
|
|
||||||
enableReactDevtools: false,
|
enableReactDevtools: false,
|
||||||
frameless: false,
|
plugins: {}
|
||||||
transparent: false,
|
|
||||||
winCtrlQ: false,
|
|
||||||
macosTranslucency: false,
|
|
||||||
disableMinSize: false,
|
|
||||||
winNativeTitleBar: false,
|
|
||||||
plugins: {},
|
|
||||||
|
|
||||||
notifications: {
|
|
||||||
timeout: 5000,
|
|
||||||
position: "bottom-right",
|
|
||||||
useNative: "not-focused",
|
|
||||||
logLimit: 50
|
|
||||||
},
|
|
||||||
|
|
||||||
cloud: {
|
|
||||||
authenticated: false,
|
|
||||||
url: "https://api.vencord.dev/",
|
|
||||||
settingsSync: false,
|
|
||||||
settingsSyncVersion: 0
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
for (const plugin in plugins) {
|
||||||
var settings = JSON.parse(VencordNative.settings.get()) as Settings;
|
DefaultSettings.plugins[plugin] = {
|
||||||
mergeDefaults(settings, DefaultSettings);
|
enabled: plugins[plugin].required ?? false
|
||||||
} 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 () => {
|
try {
|
||||||
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
|
var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings;
|
||||||
await putCloudSettings();
|
mergeDefaults(settings, DefaultSettings);
|
||||||
delete localStorage.Vencord_settingsDirty;
|
} catch (err) {
|
||||||
}
|
console.error("Corrupt settings file. ", err);
|
||||||
}, 60_000);
|
var settings = mergeDefaults({} as Settings, DefaultSettings);
|
||||||
|
}
|
||||||
|
|
||||||
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _paths?: Array<string>; };
|
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
|
||||||
const subscriptions = new Set<SubscriptionCallback>();
|
const subscriptions = new Set<SubscriptionCallback>();
|
||||||
|
|
||||||
const proxyCache = {} as Record<string, any>;
|
|
||||||
|
|
||||||
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
|
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
|
||||||
function makeProxy(settings: any, root = settings, path = ""): Settings {
|
function makeProxy(settings: Settings, root = settings, path = ""): Settings {
|
||||||
return proxyCache[path] ??= new Proxy(settings, {
|
return new Proxy(settings, {
|
||||||
get(target, p: string) {
|
get(target, p: string) {
|
||||||
const v = target[p];
|
const v = target[p];
|
||||||
|
|
||||||
// using "in" is important in the following cases to properly handle falsy or nullish values
|
// using "in" is important in the following cases to properly handle falsy or nullish values
|
||||||
if (!(p in target)) {
|
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}`);
|
|
||||||
|
|
||||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||||
// the default value.
|
// the default value.
|
||||||
if (path.startsWith("plugins.")) {
|
if (path.startsWith("plugins.")) {
|
||||||
@ -137,13 +76,9 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
|||||||
if (!setting) return v;
|
if (!setting) return v;
|
||||||
if ("default" in setting)
|
if ("default" in setting)
|
||||||
// normal setting with a default value
|
// normal setting with a default value
|
||||||
return (target[p] = setting.default);
|
return setting.default;
|
||||||
if (setting.type === OptionType.SELECT) {
|
if (setting.type === OptionType.SELECT)
|
||||||
const def = setting.options.find(o => o.default);
|
return setting.options.find(o => o.default)?.value;
|
||||||
if (def)
|
|
||||||
target[p] = def.value;
|
|
||||||
return def?.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
@ -164,17 +99,13 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
|||||||
target[p] = v;
|
target[p] = v;
|
||||||
// Call any listeners that are listening to a setting of this path
|
// Call any listeners that are listening to a setting of this path
|
||||||
const setPath = `${path}${path && "."}${p}`;
|
const setPath = `${path}${path && "."}${p}`;
|
||||||
delete proxyCache[setPath];
|
|
||||||
for (const subscription of subscriptions) {
|
for (const subscription of subscriptions) {
|
||||||
if (!subscription._paths || subscription._paths.includes(setPath)) {
|
if (!subscription._path || subscription._path === setPath) {
|
||||||
subscription(v, setPath);
|
subscription(v, setPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// And don't forget to persist the settings!
|
// And don't forget to persist the settings!
|
||||||
PlainSettings.cloud.settingsSyncVersion = Date.now();
|
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
|
||||||
localStorage.Vencord_settingsDirty = true;
|
|
||||||
saveSettingsOnFrequentAction();
|
|
||||||
VencordNative.settings.set(JSON.stringify(root, null, 4));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -200,20 +131,14 @@ export const Settings = makeProxy(settings);
|
|||||||
* Settings hook for React components. Returns a smart settings
|
* Settings hook for React components. Returns a smart settings
|
||||||
* object that automagically triggers a rerender if any properties
|
* object that automagically triggers a rerender if any properties
|
||||||
* are altered
|
* are altered
|
||||||
* @param paths An optional list of paths to whitelist for rerenders
|
|
||||||
* @returns Settings
|
* @returns Settings
|
||||||
*/
|
*/
|
||||||
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
|
export function useSettings() {
|
||||||
export function useSettings(paths?: UseSettings<Settings>[]) {
|
|
||||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||||
|
|
||||||
const onUpdate: SubscriptionCallback = paths
|
|
||||||
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
|
|
||||||
: forceUpdate;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
subscriptions.add(onUpdate);
|
subscriptions.add(forceUpdate);
|
||||||
return () => void subscriptions.delete(onUpdate);
|
return () => void subscriptions.delete(forceUpdate);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return Settings;
|
return Settings;
|
||||||
@ -237,58 +162,6 @@ type ResolvePropDeep<T, P> = P extends "" ? T :
|
|||||||
export function addSettingsListener<Path extends keyof Settings>(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void;
|
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 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) {
|
export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) {
|
||||||
((onUpdate as SubscriptionCallback)._paths ??= []).push(path);
|
(onUpdate as SubscriptionCallback)._path = path;
|
||||||
subscriptions.add(onUpdate);
|
subscriptions.add(onUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
|
||||||
const { plugins } = settings;
|
|
||||||
if (name in plugins) return;
|
|
||||||
|
|
||||||
for (const oldName of oldNames) {
|
|
||||||
if (oldName in plugins) {
|
|
||||||
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
|
|
||||||
plugins[name] = plugins[oldName];
|
|
||||||
delete plugins[oldName];
|
|
||||||
VencordNative.settings.set(JSON.stringify(settings, null, 4));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function definePluginSettings<
|
|
||||||
Def extends SettingsDefinition,
|
|
||||||
Checks extends SettingsChecks<Def>,
|
|
||||||
PrivateSettings extends object = {}
|
|
||||||
>(def: Def, checks?: Checks) {
|
|
||||||
const definedSettings: DefinedSettings<Def, Checks, PrivateSettings> = {
|
|
||||||
get store() {
|
|
||||||
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
|
|
||||||
return Settings.plugins[definedSettings.pluginName] as any;
|
|
||||||
},
|
|
||||||
use: settings => useSettings(
|
|
||||||
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
|
|
||||||
).plugins[definedSettings.pluginName] as any,
|
|
||||||
def,
|
|
||||||
checks: checks ?? {} as any,
|
|
||||||
pluginName: "",
|
|
||||||
|
|
||||||
withPrivateSettings<T extends object>() {
|
|
||||||
return this as DefinedSettings<Def, Checks, T>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return definedSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
type UseSettings<T extends object> = ResolveUseSettings<T>[keyof T];
|
|
||||||
|
|
||||||
type ResolveUseSettings<T extends object> = {
|
|
||||||
[Key in keyof T]:
|
|
||||||
Key extends string
|
|
||||||
? T[Key] extends Record<string, unknown>
|
|
||||||
// @ts-ignore "Type instantiation is excessively deep and possibly infinite"
|
|
||||||
? UseSettings<T[Key]> extends string ? `${Key}.${UseSettings<T[Key]>}` : never
|
|
||||||
: Key
|
|
||||||
: never;
|
|
||||||
};
|
|
@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function Badge({ text, color }): JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className="vc-plugins-badge" style={{
|
|
||||||
backgroundColor: color,
|
|
||||||
justifySelf: "flex-end",
|
|
||||||
marginLeft: "auto"
|
|
||||||
}}>
|
|
||||||
{text}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { React, TextInput } from "@webpack/common";
|
|
||||||
|
|
||||||
// TODO: Refactor settings to use this as well
|
|
||||||
interface TextInputProps {
|
|
||||||
/**
|
|
||||||
* WARNING: Changing this between renders will have no effect!
|
|
||||||
*/
|
|
||||||
value: string;
|
|
||||||
/**
|
|
||||||
* This will only be called if the new value passed validate()
|
|
||||||
*/
|
|
||||||
onChange(newValue: string): void;
|
|
||||||
/**
|
|
||||||
* Optionally validate the user input
|
|
||||||
* Return true if the input is valid
|
|
||||||
* Otherwise, return a string containing the reason for this input being invalid
|
|
||||||
*/
|
|
||||||
validate(v: string): true | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A very simple wrapper around Discord's TextInput that validates input and shows
|
|
||||||
* the user an error message and only calls your onChange when the input is valid
|
|
||||||
*/
|
|
||||||
export function CheckedTextInput({ value: initialValue, onChange, validate }: TextInputProps) {
|
|
||||||
const [value, setValue] = React.useState(initialValue);
|
|
||||||
const [error, setError] = React.useState<string>();
|
|
||||||
|
|
||||||
function handleChange(v: string) {
|
|
||||||
setValue(v);
|
|
||||||
const res = validate(v);
|
|
||||||
if (res === true) {
|
|
||||||
setError(void 0);
|
|
||||||
onChange(v);
|
|
||||||
} else {
|
|
||||||
setError(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Button } from "@webpack/common";
|
|
||||||
|
|
||||||
import { Heart } from "./Heart";
|
|
||||||
|
|
||||||
export default function DonateButton(props: any) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
{...props}
|
|
||||||
look={Button.Looks.LINK}
|
|
||||||
color={Button.Colors.TRANSPARENT}
|
|
||||||
onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/Vendicated")}
|
|
||||||
>
|
|
||||||
<Heart />
|
|
||||||
Donate
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
@ -16,25 +16,14 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Logger } from "@utils/Logger";
|
import Logger from "../utils/logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins, React } from "../webpack/common";
|
||||||
import { LazyComponent } from "@utils/react";
|
|
||||||
import { React } from "@webpack/common";
|
|
||||||
|
|
||||||
import { ErrorCard } from "./ErrorCard";
|
import { ErrorCard } from "./ErrorCard";
|
||||||
|
|
||||||
interface Props<T = any> {
|
interface Props {
|
||||||
/** Render nothing if an error occurs */
|
|
||||||
noop?: boolean;
|
|
||||||
/** Fallback component to render if an error occurs */
|
|
||||||
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
||||||
/** called when an error occurs. The props property is only available if using .wrap */
|
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
||||||
onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void;
|
|
||||||
/** Custom error message */
|
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|
||||||
/** The props passed to the wrapped component. Only used by wrap */
|
|
||||||
wrappedProps?: T;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = "#e78284";
|
const color = "#e78284";
|
||||||
@ -43,75 +32,68 @@ const logger = new Logger("React ErrorBoundary", color);
|
|||||||
|
|
||||||
const NO_ERROR = {};
|
const NO_ERROR = {};
|
||||||
|
|
||||||
// We might want to import this in a place where React isn't ready yet.
|
export default class ErrorBoundary extends React.Component<React.PropsWithChildren<Props>> {
|
||||||
// Thus, wrap in a LazyComponent
|
static wrap<T = any>(Component: React.ComponentType<T>): (props: T) => React.ReactElement {
|
||||||
const ErrorBoundary = LazyComponent(() => {
|
return props => (
|
||||||
return class ErrorBoundary extends React.PureComponent<React.PropsWithChildren<Props>> {
|
<ErrorBoundary>
|
||||||
state = {
|
<Component {...props as any/* I hate react typings ??? */} />
|
||||||
error: NO_ERROR as any,
|
</ErrorBoundary>
|
||||||
stack: "",
|
);
|
||||||
message: ""
|
}
|
||||||
};
|
|
||||||
|
|
||||||
static getDerivedStateFromError(error: any) {
|
state = {
|
||||||
let stack = error?.stack ?? "";
|
error: NO_ERROR as any,
|
||||||
let message = error?.message || String(error);
|
stack: "",
|
||||||
|
message: ""
|
||||||
|
};
|
||||||
|
|
||||||
if (error instanceof Error && stack) {
|
static getDerivedStateFromError(error: any) {
|
||||||
const eolIdx = stack.indexOf("\n");
|
let stack = error?.stack ?? "";
|
||||||
if (eolIdx !== -1) {
|
let message = error?.message || String(error);
|
||||||
message = stack.slice(0, eolIdx);
|
|
||||||
stack = stack.slice(eolIdx + 1).replace(/https:\/\/\S+\/assets\//g, "");
|
if (error instanceof Error && stack) {
|
||||||
}
|
const eolIdx = stack.indexOf("\n");
|
||||||
|
if (eolIdx !== -1) {
|
||||||
|
message = stack.slice(0, eolIdx);
|
||||||
|
stack = stack.slice(eolIdx + 1).replace(/https:\/\/\S+\/assets\//g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { error, stack, message };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
return { error, stack, message };
|
||||||
this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps });
|
}
|
||||||
logger.error("A component threw an Error\n", error);
|
|
||||||
logger.error("Component Stack", errorInfo.componentStack);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
if (this.state.error === NO_ERROR) return this.props.children;
|
this.props.onError?.(error, errorInfo);
|
||||||
|
logger.error("A component threw an Error\n", error);
|
||||||
|
logger.error("Component Stack", errorInfo.componentStack);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.noop) return null;
|
render() {
|
||||||
|
if (this.state.error === NO_ERROR) return this.props.children;
|
||||||
|
|
||||||
if (this.props.fallback)
|
if (this.props.fallback)
|
||||||
return <this.props.fallback
|
return <this.props.fallback
|
||||||
children={this.props.children}
|
children={this.props.children}
|
||||||
{...this.state}
|
{...this.state}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorCard style={{ overflow: "hidden" }}>
|
<ErrorCard style={{
|
||||||
<h1>Oh no!</h1>
|
overflow: "hidden",
|
||||||
<p>{msg}</p>
|
}}>
|
||||||
<code>
|
<h1>Oh no!</h1>
|
||||||
{this.state.message}
|
<p>{msg}</p>
|
||||||
{!!this.state.stack && (
|
<code>
|
||||||
<pre className={Margins.top8}>
|
{this.state.message}
|
||||||
{this.state.stack}
|
{!!this.state.stack && (
|
||||||
</pre>
|
<pre className={Margins.marginTop8}>
|
||||||
)}
|
{this.state.stack}
|
||||||
</code>
|
</pre>
|
||||||
</ErrorCard>
|
)}
|
||||||
);
|
</code>
|
||||||
}
|
</ErrorCard>
|
||||||
};
|
);
|
||||||
}) as
|
}
|
||||||
React.ComponentType<React.PropsWithChildren<Props>> & {
|
}
|
||||||
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, "wrappedProps">): React.FunctionComponent<T>;
|
|
||||||
};
|
|
||||||
|
|
||||||
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
|
||||||
<ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>
|
|
||||||
<Component {...props} />
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ErrorBoundary;
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
.vc-error-card {
|
|
||||||
padding: 2em;
|
|
||||||
background-color: #e7828430;
|
|
||||||
border: 1px solid #e78284;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: var(--text-normal, white);
|
|
||||||
}
|
|
@ -16,15 +16,24 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./ErrorCard.css";
|
import { Card } from "../webpack/common";
|
||||||
|
|
||||||
import { classes } from "@utils/misc";
|
interface Props {
|
||||||
import type { HTMLProps } from "react";
|
style?: React.CSSProperties;
|
||||||
|
className?: string;
|
||||||
export function ErrorCard(props: React.PropsWithChildren<HTMLProps<HTMLDivElement>>) {
|
}
|
||||||
|
export function ErrorCard(props: React.PropsWithChildren<Props>) {
|
||||||
return (
|
return (
|
||||||
<div {...props} className={classes(props.className, "vc-error-card")}>
|
<Card className={props.className} style={
|
||||||
|
{
|
||||||
|
padding: "2em",
|
||||||
|
backgroundColor: "#e7828430",
|
||||||
|
borderColor: "#e78284",
|
||||||
|
color: "var(--text-normal)",
|
||||||
|
...props.style
|
||||||
|
}
|
||||||
|
}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
.vc-expandableheader-center-flex {
|
|
||||||
display: flex;
|
|
||||||
justify-items: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-expandableheader-btn {
|
|
||||||
all: unset;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
@ -1,108 +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 { 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}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { React } from "@webpack/common";
|
import type { React } from "../webpack/common";
|
||||||
|
|
||||||
export function Flex(props: React.PropsWithChildren<{
|
export function Flex(props: React.PropsWithChildren<{
|
||||||
flexDirection?: React.CSSProperties["flexDirection"];
|
flexDirection?: React.CSSProperties["flexDirection"];
|
||||||
@ -24,11 +24,9 @@ export function Flex(props: React.PropsWithChildren<{
|
|||||||
className?: string;
|
className?: string;
|
||||||
} & React.HTMLProps<HTMLDivElement>>) {
|
} & React.HTMLProps<HTMLDivElement>>) {
|
||||||
props.style ??= {};
|
props.style ??= {};
|
||||||
props.style.display = "flex";
|
|
||||||
// TODO(ven): Remove me, what was I thinking??
|
|
||||||
props.style.gap ??= "1em";
|
|
||||||
props.style.flexDirection ||= props.flexDirection;
|
props.style.flexDirection ||= props.flexDirection;
|
||||||
delete props.flexDirection;
|
props.style.gap ??= "1em";
|
||||||
|
props.style.display = "flex";
|
||||||
return (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function Heart() {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
width="16"
|
|
||||||
style={{ marginRight: "0.5em", transform: "translateY(2px)" }}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="#db61a2"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,205 +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 "./iconStyles.css";
|
|
||||||
|
|
||||||
import { classes } from "@utils/misc";
|
|
||||||
import { i18n } from "@webpack/common";
|
|
||||||
import type { PropsWithChildren, SVGProps } from "react";
|
|
||||||
|
|
||||||
interface BaseIconProps extends IconProps {
|
|
||||||
viewBox: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IconProps extends SVGProps<SVGSVGElement> {
|
|
||||||
className?: string;
|
|
||||||
height?: number;
|
|
||||||
width?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
className={classes(className, "vc-icon")}
|
|
||||||
role="img"
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
viewBox={viewBox}
|
|
||||||
{...svgProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discord's link icon, as seen in the Message context menu "Copy Message Link" option
|
|
||||||
*/
|
|
||||||
export function LinkIcon({ height = 24, width = 24, className }: IconProps) {
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
height={height}
|
|
||||||
width={width}
|
|
||||||
className={classes(className, "vc-link-icon")}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<g fill="none" fill-rule="evenodd">
|
|
||||||
<path fill="currentColor" d="M10.59 13.41c.41.39.41 1.03 0 1.42-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0 5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24 2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0 4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0 5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24 2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24.973.973 0 0 1 0-1.42z" />
|
|
||||||
<rect width={width} height={height} />
|
|
||||||
</g>
|
|
||||||
</Icon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 (
|
|
||||||
<Icon
|
|
||||||
{...props}
|
|
||||||
className={classes(props.className, "vc-copy-icon")}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<g fill="currentColor">
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discord's open external icon, as seen in the user profile connections
|
|
||||||
*/
|
|
||||||
export function OpenExternalIcon(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
{...props}
|
|
||||||
className={classes(props.className, "vc-open-external-icon")}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<polygon
|
|
||||||
fill="currentColor"
|
|
||||||
fill-rule="nonzero"
|
|
||||||
points="13 20 11 20 11 8 5.5 13.5 4.08 12.08 12 4.16 19.92 12.08 18.5 13.5 13 8"
|
|
||||||
/>
|
|
||||||
</Icon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ImageIcon(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
{...props}
|
|
||||||
className={classes(props.className, "vc-image-icon")}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path fill="currentColor" d="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
|
|
||||||
</Icon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InfoIcon(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
{...props}
|
|
||||||
className={classes(props.className, "vc-info-icon")}
|
|
||||||
viewBox="0 0 12 12"
|
|
||||||
>
|
|
||||||
<path fill="currentColor" d="M6 1C3.243 1 1 3.244 1 6c0 2.758 2.243 5 5 5s5-2.242 5-5c0-2.756-2.243-5-5-5zm0 2.376a.625.625 0 110 1.25.625.625 0 010-1.25zM7.5 8.5h-3v-1h1V6H5V5h1a.5.5 0 01.5.5v2h1v1z" />
|
|
||||||
</Icon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OwnerCrownIcon(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
aria-label={i18n.Messages.GUILD_OWNER}
|
|
||||||
{...props}
|
|
||||||
className={classes(props.className, "vc-owner-crown-icon")}
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M13.6572 5.42868C13.8879 5.29002 14.1806 5.30402 14.3973 5.46468C14.6133 5.62602 14.7119 5.90068 14.6473 6.16202L13.3139 11.4954C13.2393 11.7927 12.9726 12.0007 12.6666 12.0007H3.33325C3.02725 12.0007 2.76058 11.792 2.68592 11.4954L1.35258 6.16202C1.28792 5.90068 1.38658 5.62602 1.60258 5.46468C1.81992 5.30468 2.11192 5.29068 2.34325 5.42868L5.13192 7.10202L7.44592 3.63068C7.46173 3.60697 7.48377 3.5913 7.50588 3.57559C7.5192 3.56612 7.53255 3.55663 7.54458 3.54535L6.90258 2.90268C6.77325 2.77335 6.77325 2.56068 6.90258 2.43135L7.76458 1.56935C7.89392 1.44002 8.10658 1.44002 8.23592 1.56935L9.09792 2.43135C9.22725 2.56068 9.22725 2.77335 9.09792 2.90268L8.45592 3.54535C8.46794 3.55686 8.48154 3.56651 8.49516 3.57618C8.51703 3.5917 8.53897 3.60727 8.55458 3.63068L10.8686 7.10202L13.6572 5.42868ZM2.66667 12.6673H13.3333V14.0007H2.66667V12.6673Z"
|
|
||||||
/>
|
|
||||||
</Icon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discord's screenshare icon, as seen in the connection panel
|
|
||||||
*/
|
|
||||||
export function ScreenshareIcon(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
{...props}
|
|
||||||
className={classes(props.className, "vc-screenshare-icon")}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M2 4.5C2 3.397 2.897 2.5 4 2.5H20C21.103 2.5 22 3.397 22 4.5V15.5C22 16.604 21.103 17.5 20 17.5H13V19.5H17V21.5H7V19.5H11V17.5H4C2.897 17.5 2 16.604 2 15.5V4.5ZM13.2 14.3375V11.6C9.864 11.6 7.668 12.6625 6 15C6.672 11.6625 8.532 8.3375 13.2 7.6625V5L18 9.6625L13.2 14.3375Z"
|
|
||||||
/>
|
|
||||||
</Icon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ImageVisible(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
{...props}
|
|
||||||
className={classes(props.className, "vc-image-visible")}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path fill="currentColor" d="M5 21q-.825 0-1.413-.587Q3 19.825 3 19V5q0-.825.587-1.413Q4.175 3 5 3h14q.825 0 1.413.587Q21 4.175 21 5v14q0 .825-.587 1.413Q19.825 21 19 21Zm0-2h14V5H5v14Zm1-2h12l-3.75-5-3 4L9 13Zm-1 2V5v14Z" />
|
|
||||||
</Icon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ImageInvisible(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
{...props}
|
|
||||||
className={classes(props.className, "vc-image-invisible")}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path fill="currentColor" d="m21 18.15-2-2V5H7.85l-2-2H19q.825 0 1.413.587Q21 4.175 21 5Zm-1.2 4.45L18.2 21H5q-.825 0-1.413-.587Q3 19.825 3 19V5.8L1.4 4.2l1.4-1.4 18.4 18.4ZM6 17l3-4 2.25 3 .825-1.1L5 7.825V19h11.175l-2-2Zm7.425-6.425ZM10.6 13.4Z" />
|
|
||||||
</Icon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Microphone(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
{...props}
|
|
||||||
className={classes(props.className, "vc-microphone")}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.99 11C14.99 12.66 13.66 14 12 14C10.34 14 9 12.66 9 11V5C9 3.34 10.34 2 12 2C13.66 2 15 3.34 15 5L14.99 11ZM12 16.1C14.76 16.1 17.3 14 17.3 11H19C19 14.42 16.28 17.24 13 17.72V21H11V17.72C7.72 17.23 5 14.41 5 11H6.7C6.7 14 9.24 16.1 12 16.1ZM12 4C11.2 4 11 4.66667 11 5V11C11 11.3333 11.2 12 12 12C12.8 12 13 11.3333 13 11V5C13 4.66667 12.8 4 12 4Z" fill="currentColor" />
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.99 11C14.99 12.66 13.66 14 12 14C10.34 14 9 12.66 9 11V5C9 3.34 10.34 2 12 2C13.66 2 15 3.34 15 5L14.99 11ZM12 16.1C14.76 16.1 17.3 14 17.3 11H19C19 14.42 16.28 17.24 13 17.72V22H11V17.72C7.72 17.23 5 14.41 5 11H6.7C6.7 14 9.24 16.1 12 16.1Z" fill="currentColor" />
|
|
||||||
</Icon >
|
|
||||||
);
|
|
||||||
}
|
|
@ -16,20 +16,21 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { React } from "@webpack/common";
|
import { React } from "../webpack/common";
|
||||||
|
|
||||||
interface Props extends React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> {
|
interface Props {
|
||||||
|
href: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Link(props: React.PropsWithChildren<Props>) {
|
export function Link(props: React.PropsWithChildren<Props>) {
|
||||||
if (props.disabled) {
|
if (props.disabled) {
|
||||||
props.style ??= {};
|
props.style ??= {};
|
||||||
props.style.pointerEvents = "none";
|
props.style.pointerEvents = "none";
|
||||||
props["aria-disabled"] = true;
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<a role="link" target="_blank" {...props}>
|
<a href={props.href} target="_blank" style={props.style}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
42
src/components/Monaco.ts
Normal file
42
src/components/Monaco.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import monacoHtml from "~fileContent/monacoWin.html";
|
||||||
|
|
||||||
|
import { IpcEvents } from "../utils";
|
||||||
|
import { debounce } from "../utils/debounce";
|
||||||
|
import { Queue } from "../utils/Queue";
|
||||||
|
import { find } from "../webpack/webpack";
|
||||||
|
|
||||||
|
const queue = new Queue();
|
||||||
|
const setCss = debounce((css: string) => {
|
||||||
|
queue.add(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css));
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function launchMonacoEditor() {
|
||||||
|
const win = open("about:blank", void 0, "popup,width=1000,height=1000")!;
|
||||||
|
|
||||||
|
win.setCss = setCss;
|
||||||
|
win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
|
||||||
|
win.getTheme = () => find(m => m.ProtoClass?.typeName.endsWith("PreloadedUserSettings"))
|
||||||
|
.getCurrentValue().appearance.theme === 1
|
||||||
|
? "vs-dark"
|
||||||
|
: "vs-light";
|
||||||
|
|
||||||
|
win.document.write(monacoHtml);
|
||||||
|
}
|
@ -16,35 +16,29 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { generateId } from "@api/Commands";
|
import { Forms } from "@components";
|
||||||
import { useSettings } from "@api/Settings";
|
|
||||||
import { disableStyle, enableStyle } from "@api/Styles";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Flex } from "@components/Flex";
|
|
||||||
import { proxyLazy } from "@utils/lazy";
|
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { classes } from "@utils/misc";
|
|
||||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
|
||||||
import { LazyComponent } from "@utils/react";
|
|
||||||
import { OptionType, Plugin } from "@utils/types";
|
|
||||||
import { findByCode, findByPropsLazy } from "@webpack";
|
|
||||||
import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
import { Constructor } from "type-fest";
|
import { Constructor } from "type-fest";
|
||||||
|
|
||||||
|
import { generateId } from "../../api/Commands";
|
||||||
|
import { useSettings } from "../../api/settings";
|
||||||
|
import { lazyWebpack, proxyLazy } from "../../utils";
|
||||||
|
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "../../utils/modal";
|
||||||
|
import { OptionType, Plugin } from "../../utils/types";
|
||||||
|
import { filters } from "../../webpack";
|
||||||
|
import { Button, FluxDispatcher, React, Text, Tooltip, UserStore, UserUtils } from "../../webpack/common";
|
||||||
|
import ErrorBoundary from "../ErrorBoundary";
|
||||||
|
import { Flex } from "../Flex";
|
||||||
import {
|
import {
|
||||||
ISettingElementProps,
|
|
||||||
SettingBooleanComponent,
|
SettingBooleanComponent,
|
||||||
SettingCustomComponent,
|
SettingInputComponent,
|
||||||
SettingNumericComponent,
|
SettingNumericComponent,
|
||||||
SettingSelectComponent,
|
SettingSelectComponent,
|
||||||
SettingSliderComponent,
|
SettingSliderComponent
|
||||||
SettingTextComponent
|
|
||||||
} from "./components";
|
} from "./components";
|
||||||
import hideBotTagStyle from "./userPopoutHideBotTag.css?managed";
|
|
||||||
|
|
||||||
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
const UserSummaryItem = lazyWebpack(filters.byCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
||||||
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
const AvatarStyles = lazyWebpack(filters.byProps(["moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"]));
|
||||||
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
||||||
|
|
||||||
interface PluginModalProps extends ModalProps {
|
interface PluginModalProps extends ModalProps {
|
||||||
@ -52,12 +46,11 @@ interface PluginModalProps extends ModalProps {
|
|||||||
onRestartNeeded(): void;
|
onRestartNeeded(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeDummyUser(user: { username: string; id?: string; avatar?: string; }) {
|
/** To stop discord making unwanted requests... */
|
||||||
|
function makeDummyUser(user: { name: string, id: BigInt; }) {
|
||||||
const newUser = new UserRecord({
|
const newUser = new UserRecord({
|
||||||
username: user.username,
|
username: user.name,
|
||||||
id: user.id ?? generateId(),
|
id: generateId(),
|
||||||
avatar: user.avatar,
|
|
||||||
/** To stop discord making unwanted requests... */
|
|
||||||
bot: true,
|
bot: true,
|
||||||
});
|
});
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
@ -67,16 +60,6 @@ function makeDummyUser(user: { username: string; id?: string; avatar?: string; }
|
|||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any>>> = {
|
|
||||||
[OptionType.STRING]: SettingTextComponent,
|
|
||||||
[OptionType.NUMBER]: SettingNumericComponent,
|
|
||||||
[OptionType.BIGINT]: SettingNumericComponent,
|
|
||||||
[OptionType.BOOLEAN]: SettingBooleanComponent,
|
|
||||||
[OptionType.SELECT]: SettingSelectComponent,
|
|
||||||
[OptionType.SLIDER]: SettingSliderComponent,
|
|
||||||
[OptionType.COMPONENT]: SettingCustomComponent
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
||||||
const [authors, setAuthors] = React.useState<Partial<User>[]>([]);
|
const [authors, setAuthors] = React.useState<Partial<User>[]>([]);
|
||||||
|
|
||||||
@ -85,50 +68,23 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
const [tempSettings, setTempSettings] = React.useState<Record<string, any>>({});
|
const [tempSettings, setTempSettings] = React.useState<Record<string, any>>({});
|
||||||
|
|
||||||
const [errors, setErrors] = React.useState<Record<string, boolean>>({});
|
const [errors, setErrors] = React.useState<Record<string, boolean>>({});
|
||||||
const [saveError, setSaveError] = React.useState<string | null>(null);
|
|
||||||
|
|
||||||
const canSubmit = () => Object.values(errors).every(e => !e);
|
const canSubmit = () => Object.values(errors).every(e => !e);
|
||||||
|
|
||||||
const hasSettings = Boolean(pluginSettings && plugin.options);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
enableStyle(hideBotTagStyle);
|
|
||||||
|
|
||||||
let originalUser: User;
|
|
||||||
(async () => {
|
(async () => {
|
||||||
for (const user of plugin.authors.slice(0, 6)) {
|
for (const user of plugin.authors.slice(0, 6)) {
|
||||||
const author = user.id
|
const author = user.id ? await UserUtils.fetchUser(`${user.id}`).catch(() => null) : makeDummyUser(user);
|
||||||
? await UserUtils.fetchUser(`${user.id}`)
|
setAuthors(a => [...a, author || makeDummyUser(user)]);
|
||||||
// only show name & pfp and no actions so users cannot harass plugin devs for support (send dms, add as friend, etc)
|
|
||||||
.then(u => (originalUser = u, makeDummyUser(u)))
|
|
||||||
.catch(() => makeDummyUser({ username: user.name }))
|
|
||||||
: makeDummyUser({ username: user.name });
|
|
||||||
|
|
||||||
setAuthors(a => [...a, author]);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
|
||||||
disableStyle(hideBotTagStyle);
|
|
||||||
if (originalUser)
|
|
||||||
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: originalUser });
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function saveAndClose() {
|
function saveAndClose() {
|
||||||
if (!plugin.options) {
|
if (!plugin.options) {
|
||||||
onClose();
|
onClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plugin.beforeSave) {
|
|
||||||
const result = await Promise.resolve(plugin.beforeSave(tempSettings));
|
|
||||||
if (result !== true) {
|
|
||||||
setSaveError(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let restartNeeded = false;
|
let restartNeeded = false;
|
||||||
for (const [key, value] of Object.entries(tempSettings)) {
|
for (const [key, value] of Object.entries(tempSettings)) {
|
||||||
const option = plugin.options[key];
|
const option = plugin.options[key];
|
||||||
@ -141,36 +97,46 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSettings() {
|
function renderSettings() {
|
||||||
if (!hasSettings || !plugin.options) {
|
if (!pluginSettings || !plugin.options) {
|
||||||
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
||||||
} else {
|
|
||||||
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
|
||||||
if (setting.hidden) return null;
|
|
||||||
|
|
||||||
function onChange(newValue: any) {
|
|
||||||
setTempSettings(s => ({ ...s, [key]: newValue }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError(hasError: boolean) {
|
|
||||||
setErrors(e => ({ ...e, [key]: hasError }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const Component = Components[setting.type];
|
|
||||||
return (
|
|
||||||
<Component
|
|
||||||
id={key}
|
|
||||||
key={key}
|
|
||||||
option={setting}
|
|
||||||
onChange={onChange}
|
|
||||||
onError={onError}
|
|
||||||
pluginSettings={pluginSettings}
|
|
||||||
definedSettings={plugin.settings}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return <Flex flexDirection="column" style={{ gap: 12, marginBottom: 16 }}>{options}</Flex>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const options: JSX.Element[] = [];
|
||||||
|
for (const [key, setting] of Object.entries(plugin.options)) {
|
||||||
|
function onChange(newValue) {
|
||||||
|
setTempSettings(s => ({ ...s, [key]: newValue }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(hasError: boolean) {
|
||||||
|
setErrors(e => ({ ...e, [key]: hasError }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = { onChange, pluginSettings, id: key, onError };
|
||||||
|
switch (setting.type) {
|
||||||
|
case OptionType.SELECT: {
|
||||||
|
options.push(<SettingSelectComponent key={key} option={setting} {...props} />);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OptionType.STRING: {
|
||||||
|
options.push(<SettingInputComponent key={key} option={setting} {...props} />);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OptionType.NUMBER:
|
||||||
|
case OptionType.BIGINT: {
|
||||||
|
options.push(<SettingNumericComponent key={key} option={setting} {...props} />);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OptionType.BOOLEAN: {
|
||||||
|
options.push(<SettingBooleanComponent key={key} option={setting} {...props} />);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OptionType.SLIDER: {
|
||||||
|
options.push(<SettingSliderComponent key={key} option={setting} {...props} />);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMoreUsers(_label: string, count: number) {
|
function renderMoreUsers(_label: string, count: number) {
|
||||||
@ -194,17 +160,15 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM} className="vc-text-selectable">
|
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
|
||||||
<ModalHeader separator={false}>
|
<ModalHeader>
|
||||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
|
<Text variant="heading-md/bold">{plugin.name}</Text>
|
||||||
<ModalCloseButton onClick={onClose} />
|
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalContent>
|
<ModalContent style={{ marginBottom: 8, marginTop: 8 }}>
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
|
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
|
||||||
<Forms.FormText>{plugin.description}</Forms.FormText>
|
<Forms.FormText>{plugin.description}</Forms.FormText>
|
||||||
<Forms.FormTitle tag="h3" style={{ marginTop: 8, marginBottom: 0 }}>Authors</Forms.FormTitle>
|
<div style={{ marginTop: 8, marginBottom: 8, width: "fit-content" }}>
|
||||||
<div style={{ width: "fit-content", marginBottom: 8 }}>
|
|
||||||
<UserSummaryItem
|
<UserSummaryItem
|
||||||
users={authors}
|
users={authors}
|
||||||
count={plugin.authors.length}
|
count={plugin.authors.length}
|
||||||
@ -218,48 +182,44 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
</div>
|
</div>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
{!!plugin.settingsAboutComponent && (
|
{!!plugin.settingsAboutComponent && (
|
||||||
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
|
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
|
||||||
<plugin.settingsAboutComponent tempSettings={tempSettings} />
|
<plugin.settingsAboutComponent />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Forms.FormSection className={Margins.bottom16}>
|
<Forms.FormSection>
|
||||||
<Forms.FormTitle tag="h3">Settings</Forms.FormTitle>
|
<Forms.FormTitle tag="h3">Settings</Forms.FormTitle>
|
||||||
{renderSettings()}
|
{renderSettings()}
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
{hasSettings && <ModalFooter>
|
<ModalFooter>
|
||||||
<Flex flexDirection="column" style={{ width: "100%" }}>
|
<Flex>
|
||||||
<Flex style={{ marginLeft: "auto" }}>
|
<Button
|
||||||
<Button
|
onClick={onClose}
|
||||||
onClick={onClose}
|
size={Button.Sizes.SMALL}
|
||||||
size={Button.Sizes.SMALL}
|
color={Button.Colors.RED}
|
||||||
color={Button.Colors.WHITE}
|
>
|
||||||
look={Button.Looks.LINK}
|
Exit Without Saving
|
||||||
>
|
</Button>
|
||||||
Cancel
|
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
|
||||||
</Button>
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
|
<Button
|
||||||
{({ onMouseEnter, onMouseLeave }) => (
|
size={Button.Sizes.SMALL}
|
||||||
<Button
|
color={Button.Colors.BRAND}
|
||||||
size={Button.Sizes.SMALL}
|
onClick={saveAndClose}
|
||||||
color={Button.Colors.BRAND}
|
onMouseEnter={onMouseEnter}
|
||||||
onClick={saveAndClose}
|
onMouseLeave={onMouseLeave}
|
||||||
onMouseEnter={onMouseEnter}
|
disabled={!canSubmit()}
|
||||||
onMouseLeave={onMouseLeave}
|
>
|
||||||
disabled={!canSubmit()}
|
Save & Exit
|
||||||
>
|
</Button>
|
||||||
Save & Close
|
)}
|
||||||
</Button>
|
</Tooltip>
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
</Flex>
|
|
||||||
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalFooter>}
|
</ModalFooter>
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,13 +16,13 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
import { Forms } from "@components";
|
||||||
import { PluginOptionBoolean } from "@utils/types";
|
|
||||||
import { Forms, React, Switch } from "@webpack/common";
|
|
||||||
|
|
||||||
|
import { PluginOptionBoolean } from "../../../utils/types";
|
||||||
|
import { React, Select } from "../../../webpack/common";
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function SettingBooleanComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
|
export function SettingBooleanComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
|
||||||
const def = pluginSettings[id] ?? option.default;
|
const def = pluginSettings[id] ?? option.default;
|
||||||
|
|
||||||
const [state, setState] = React.useState(def ?? false);
|
const [state, setState] = React.useState(def ?? false);
|
||||||
@ -32,8 +32,13 @@ export function SettingBooleanComponent({ option, pluginSettings, definedSetting
|
|||||||
onError(error !== null);
|
onError(error !== null);
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ label: "Enabled", value: true, default: def === true },
|
||||||
|
{ label: "Disabled", value: false, default: typeof def === "undefined" || def === false },
|
||||||
|
];
|
||||||
|
|
||||||
function handleChange(newValue: boolean): void {
|
function handleChange(newValue: boolean): void {
|
||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
@ -45,17 +50,18 @@ export function SettingBooleanComponent({ option, pluginSettings, definedSetting
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Switch
|
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||||
value={state}
|
<Select
|
||||||
onChange={handleChange}
|
isDisabled={option.disabled?.() ?? false}
|
||||||
note={option.description}
|
options={options}
|
||||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
placeholder={option.placeholder ?? "Select an option"}
|
||||||
|
maxVisibleItems={5}
|
||||||
|
closeOnSelect={true}
|
||||||
|
select={handleChange}
|
||||||
|
isSelected={v => v === state}
|
||||||
|
serialize={v => String(v)}
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
hideBorder
|
/>
|
||||||
style={{ marginBottom: "0.5em" }}
|
|
||||||
>
|
|
||||||
{wordsToTitle(wordsFromCamel(id))}
|
|
||||||
</Switch>
|
|
||||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
);
|
);
|
||||||
|
@ -16,14 +16,15 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { OptionType, PluginOptionNumber } from "@utils/types";
|
import { Forms } from "@components";
|
||||||
import { Forms, React, TextInput } from "@webpack/common";
|
|
||||||
|
|
||||||
|
import { OptionType, PluginOptionNumber } from "../../../utils/types";
|
||||||
|
import { React, TextInput } from "../../../webpack/common";
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
|
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||||
|
|
||||||
export function SettingNumericComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
|
export function SettingNumericComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
|
||||||
function serialize(value: any) {
|
function serialize(value: any) {
|
||||||
if (option.type === OptionType.BIGINT) return BigInt(value);
|
if (option.type === OptionType.BIGINT) return BigInt(value);
|
||||||
return Number(value);
|
return Number(value);
|
||||||
@ -37,13 +38,10 @@ export function SettingNumericComponent({ option, pluginSettings, definedSetting
|
|||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue) {
|
function handleChange(newValue) {
|
||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||||
|
|
||||||
setError(null);
|
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
|
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
||||||
if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
|
||||||
setState(`${Number.MAX_SAFE_INTEGER}`);
|
setState(`${Number.MAX_SAFE_INTEGER}`);
|
||||||
onChange(serialize(newValue));
|
onChange(serialize(newValue));
|
||||||
} else {
|
} else {
|
||||||
@ -61,7 +59,7 @@ export function SettingNumericComponent({ option, pluginSettings, definedSetting
|
|||||||
value={state}
|
value={state}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={option.placeholder ?? "Enter a number"}
|
placeholder={option.placeholder ?? "Enter a number"}
|
||||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
disabled={option.disabled?.() ?? false}
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||||
|
@ -16,12 +16,14 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PluginOptionSelect } from "@utils/types";
|
import { FormSection, FormText, FormTitle } from "@components/Forms";
|
||||||
import { Forms, React, Select } from "@webpack/common";
|
import Select from "@components/Select";
|
||||||
|
|
||||||
|
import { PluginOptionSelect } from "../../../utils/types";
|
||||||
|
import { React } from "../../../webpack/common";
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function SettingSelectComponent({ option, pluginSettings, definedSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
|
export function SettingSelectComponent({ option, pluginSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
|
||||||
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
|
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
|
||||||
|
|
||||||
const [state, setState] = React.useState<any>(def ?? null);
|
const [state, setState] = React.useState<any>(def ?? null);
|
||||||
@ -32,21 +34,20 @@ export function SettingSelectComponent({ option, pluginSettings, definedSettings
|
|||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue) {
|
function handleChange(newValue) {
|
||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
setError(null);
|
|
||||||
setState(newValue);
|
setState(newValue);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection>
|
<FormSection>
|
||||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
<FormTitle>{option.description}</FormTitle>
|
||||||
<Select
|
<Select
|
||||||
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
isDisabled={option.disabled?.() ?? false}
|
||||||
options={option.options}
|
options={option.options}
|
||||||
placeholder={option.placeholder ?? "Select an option"}
|
placeholder={option.placeholder ?? "Select an option"}
|
||||||
maxVisibleItems={5}
|
maxVisibleItems={5}
|
||||||
@ -56,7 +57,7 @@ export function SettingSelectComponent({ option, pluginSettings, definedSettings
|
|||||||
serialize={v => String(v)}
|
serialize={v => String(v)}
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
{error && <FormText style={{ color: "var(--text-danger)" }}>{error}</FormText>}
|
||||||
</Forms.FormSection>
|
</FormSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PluginOptionSlider } from "@utils/types";
|
import { Forms } from "@components";
|
||||||
import { Forms, React, Slider } from "@webpack/common";
|
|
||||||
|
|
||||||
|
import { PluginOptionSlider } from "../../../utils/types";
|
||||||
|
import { React, Slider } from "../../../webpack/common";
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function makeRange(start: number, end: number, step = 1) {
|
export function makeRange(start: number, end: number, step = 1) {
|
||||||
@ -29,7 +30,7 @@ export function makeRange(start: number, end: number, step = 1) {
|
|||||||
return ranges;
|
return ranges;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingSliderComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) {
|
export function SettingSliderComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) {
|
||||||
const def = pluginSettings[id] ?? option.default;
|
const def = pluginSettings[id] ?? option.default;
|
||||||
|
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
@ -39,7 +40,7 @@ export function SettingSliderComponent({ option, pluginSettings, definedSettings
|
|||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue: number): void {
|
function handleChange(newValue: number): void {
|
||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
@ -52,7 +53,7 @@ export function SettingSliderComponent({ option, pluginSettings, definedSettings
|
|||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||||
<Slider
|
<Slider
|
||||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
disabled={option.disabled?.() ?? false}
|
||||||
markers={option.markers}
|
markers={option.markers}
|
||||||
minValue={option.markers[0]}
|
minValue={option.markers[0]}
|
||||||
maxValue={option.markers[option.markers.length - 1]}
|
maxValue={option.markers[option.markers.length - 1]}
|
||||||
|
@ -16,12 +16,13 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PluginOptionString } from "@utils/types";
|
import { Forms } from "@components";
|
||||||
import { Forms, React, TextInput } from "@webpack/common";
|
|
||||||
|
|
||||||
|
import { PluginOptionString } from "../../../utils/types";
|
||||||
|
import { React, TextInput } from "../../../webpack/common";
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function SettingTextComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
|
export function SettingInputComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
|
||||||
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
|
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
@ -30,11 +31,10 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
|
|||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
function handleChange(newValue) {
|
function handleChange(newValue) {
|
||||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||||
if (typeof isValid === "string") setError(isValid);
|
if (typeof isValid === "string") setError(isValid);
|
||||||
else if (!isValid) setError("Invalid input provided.");
|
else if (!isValid) setError("Invalid input provided.");
|
||||||
else {
|
else {
|
||||||
setError(null);
|
|
||||||
setState(newValue);
|
setState(newValue);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
@ -48,7 +48,7 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
|
|||||||
value={state}
|
value={state}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={option.placeholder ?? "Enter a value"}
|
placeholder={option.placeholder ?? "Enter a value"}
|
||||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
disabled={option.disabled?.() ?? false}
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DefinedSettings, PluginOptionBase } from "@utils/types";
|
import { PluginOptionBase } from "../../../utils/types";
|
||||||
|
|
||||||
export interface ISettingElementProps<T extends PluginOptionBase> {
|
export interface ISettingElementProps<T extends PluginOptionBase> {
|
||||||
option: T;
|
option: T;
|
||||||
@ -27,14 +27,10 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
|
|||||||
};
|
};
|
||||||
id: string;
|
id: string;
|
||||||
onError(hasError: boolean): void;
|
onError(hasError: boolean): void;
|
||||||
definedSettings?: DefinedSettings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from "../../Badge";
|
|
||||||
export * from "./SettingBooleanComponent";
|
export * from "./SettingBooleanComponent";
|
||||||
export * from "./SettingCustomComponent";
|
|
||||||
export * from "./SettingNumericComponent";
|
export * from "./SettingNumericComponent";
|
||||||
export * from "./SettingSelectComponent";
|
export * from "./SettingSelectComponent";
|
||||||
export * from "./SettingSliderComponent";
|
export * from "./SettingSliderComponent";
|
||||||
export * from "./SettingTextComponent";
|
export * from "./SettingTextComponent";
|
||||||
|
|
||||||
|
@ -16,38 +16,32 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./styles.css";
|
import { FormDivider, FormSection, FormText, FormTitle } from "@components/Forms";
|
||||||
|
|
||||||
import * as DataStore from "@api/DataStore";
|
|
||||||
import { showNotice } from "@api/Notices";
|
|
||||||
import { Settings, useSettings } from "@api/Settings";
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
|
||||||
import PluginModal from "@components/PluginSettings/PluginModal";
|
|
||||||
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
|
||||||
import { SettingsTab } from "@components/VencordSettings/shared";
|
|
||||||
import { ChangeList } from "@utils/ChangeList";
|
|
||||||
import { Logger } from "@utils/Logger";
|
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { classes } from "@utils/misc";
|
|
||||||
import { openModalLazy } from "@utils/modal";
|
|
||||||
import { LazyComponent, useAwaiter } from "@utils/react";
|
|
||||||
import { Plugin } from "@utils/types";
|
|
||||||
import { findByCode, findByPropsLazy } from "@webpack";
|
|
||||||
import { Alerts, Button, Card, Forms, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
|
import { showNotice } from "../../api/Notices";
|
||||||
|
import { Settings, useSettings } from "../../api/settings";
|
||||||
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
||||||
|
import { Logger, Modals } from "../../utils";
|
||||||
|
import { ChangeList } from "../../utils/ChangeList";
|
||||||
|
import { classes, lazyWebpack } from "../../utils/misc";
|
||||||
|
import { Plugin } from "../../utils/types";
|
||||||
|
import { filters } from "../../webpack";
|
||||||
|
import { Alerts, Button, Margins, Parser, React, Switch, Text, TextInput, Toasts, Tooltip } from "../../webpack/common";
|
||||||
|
import ErrorBoundary from "../ErrorBoundary";
|
||||||
|
import { ErrorCard } from "../ErrorCard";
|
||||||
|
import { Flex } from "../Flex";
|
||||||
|
import PluginModal from "./PluginModal";
|
||||||
|
import * as styles from "./styles";
|
||||||
|
|
||||||
|
|
||||||
const cl = classNameFactory("vc-plugins-");
|
|
||||||
const logger = new Logger("PluginSettings", "#a6d189");
|
const logger = new Logger("PluginSettings", "#a6d189");
|
||||||
|
|
||||||
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
|
const Select = lazyWebpack(filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
|
||||||
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
|
const InputStyles = lazyWebpack(filters.byProps(["inputDefault", "inputWrapper"]));
|
||||||
|
|
||||||
const CogWheel = LazyComponent(() => findByCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
|
const CogWheel = lazyWebpack(filters.byCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
|
||||||
const InfoIcon = LazyComponent(() => findByCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
|
const InfoIcon = lazyWebpack(filters.byCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
|
||||||
|
|
||||||
function showErrorToast(message: string) {
|
function showErrorToast(message: string) {
|
||||||
Toasts.show({
|
Toasts.show({
|
||||||
@ -60,27 +54,23 @@ function showErrorToast(message: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReloadRequiredCard({ required }: { required: boolean; }) {
|
interface ReloadRequiredCardProps extends React.HTMLProps<HTMLDivElement> {
|
||||||
|
plugins: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReloadRequiredCard({ plugins, ...props }: ReloadRequiredCardProps) {
|
||||||
|
if (plugins.length === 0) return null;
|
||||||
|
|
||||||
|
const pluginPrefix = plugins.length === 1 ? "The plugin" : "The following plugins require a reload to apply changes:";
|
||||||
|
const pluginSuffix = plugins.length === 1 ? " requires a reload to apply changes." : ".";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={cl("info-card", { "restart-card": required })}>
|
<ErrorCard {...props} style={{ padding: "1em", display: "grid", gridTemplateColumns: "1fr auto", gap: 8, ...props.style }}>
|
||||||
{required ? (
|
<span style={{ margin: "auto 0" }}>
|
||||||
<>
|
{pluginPrefix} <code>{plugins.join(", ")}</code>{pluginSuffix}
|
||||||
<Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle>
|
</span>
|
||||||
<Forms.FormText className={cl("dep-text")}>
|
<Button look={Button.Looks.INVERTED} onClick={() => location.reload()}>Reload</Button>
|
||||||
Restart now to apply new plugins and their settings
|
</ErrorCard>
|
||||||
</Forms.FormText>
|
|
||||||
<Button color={Button.Colors.YELLOW} onClick={() => location.reload()}>
|
|
||||||
Restart
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle tag="h5">Plugin Management</Forms.FormTitle>
|
|
||||||
<Forms.FormText>Press the cog wheel or info icon to get more info on a plugin</Forms.FormText>
|
|
||||||
<Forms.FormText>Plugins with a cog wheel have settings you can modify!</Forms.FormText>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,16 +78,20 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
|||||||
plugin: Plugin;
|
plugin: Plugin;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onRestartNeeded(name: string): void;
|
onRestartNeeded(name: string): void;
|
||||||
isNew?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave }: PluginCardProps) {
|
||||||
const settings = Settings.plugins[plugin.name];
|
const settings = useSettings();
|
||||||
|
const pluginSettings = settings.plugins[plugin.name];
|
||||||
|
|
||||||
const isEnabled = () => settings.enabled ?? false;
|
const [iconHover, setIconHover] = React.useState(false);
|
||||||
|
|
||||||
|
function isEnabled() {
|
||||||
|
return pluginSettings?.enabled || plugin.started;
|
||||||
|
}
|
||||||
|
|
||||||
function openModal() {
|
function openModal() {
|
||||||
openModalLazy(async () => {
|
Modals.openModalLazy(async () => {
|
||||||
return modalProps => {
|
return modalProps => {
|
||||||
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
|
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
|
||||||
};
|
};
|
||||||
@ -116,68 +110,69 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||||||
return;
|
return;
|
||||||
} else if (restartNeeded) {
|
} else if (restartNeeded) {
|
||||||
// If any dependencies have patches, don't start the plugin yet.
|
// If any dependencies have patches, don't start the plugin yet.
|
||||||
settings.enabled = true;
|
pluginSettings.enabled = true;
|
||||||
onRestartNeeded(plugin.name);
|
onRestartNeeded(plugin.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
|
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
|
||||||
if (plugin.patches?.length) {
|
if (plugin.patches) {
|
||||||
settings.enabled = !wasEnabled;
|
pluginSettings.enabled = !wasEnabled;
|
||||||
onRestartNeeded(plugin.name);
|
onRestartNeeded(plugin.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the plugin is enabled, but hasn't been started, then we can just toggle it off.
|
// If the plugin is enabled, but hasn't been started, then we can just toggle it off.
|
||||||
if (wasEnabled && !plugin.started) {
|
if (wasEnabled && !plugin.started) {
|
||||||
settings.enabled = !wasEnabled;
|
pluginSettings.enabled = !wasEnabled;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = wasEnabled ? stopPlugin(plugin) : startPlugin(plugin);
|
const result = wasEnabled ? stopPlugin(plugin) : startPlugin(plugin);
|
||||||
|
const action = wasEnabled ? "stop" : "start";
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
settings.enabled = false;
|
logger.error(`Failed to ${action} plugin ${plugin.name}`);
|
||||||
|
showErrorToast(`Failed to ${action} plugin: ${plugin.name}`);
|
||||||
const msg = `Error while ${wasEnabled ? "stopping" : "starting"} plugin ${plugin.name}`;
|
|
||||||
logger.error(msg);
|
|
||||||
showErrorToast(msg);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.enabled = !wasEnabled;
|
pluginSettings.enabled = !wasEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AddonCard
|
<Flex style={styles.PluginsGridItem} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||||
name={plugin.name}
|
<Switch
|
||||||
description={plugin.description}
|
onChange={toggleEnabled}
|
||||||
isNew={isNew}
|
disabled={disabled}
|
||||||
enabled={isEnabled()}
|
value={isEnabled()}
|
||||||
setEnabled={toggleEnabled}
|
note={<Text variant="text-md/normal" style={{ height: 40, overflow: "hidden" }}>{plugin.description}</Text>}
|
||||||
disabled={disabled}
|
hideBorder={true}
|
||||||
onMouseEnter={onMouseEnter}
|
>
|
||||||
onMouseLeave={onMouseLeave}
|
<Flex style={{ marginTop: "auto", width: "100%", height: "100%", alignItems: "center" }}>
|
||||||
infoButton={
|
<Text variant="text-md/bold" style={{ flexGrow: "1" }}>{plugin.name}</Text>
|
||||||
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
|
<button role="switch" onClick={() => openModal()} style={styles.SettingsIcon} className="button-12Fmur">
|
||||||
{plugin.options
|
{plugin.options
|
||||||
? <CogWheel />
|
? <CogWheel
|
||||||
: <InfoIcon width="24" height="24" />}
|
style={{ color: iconHover ? "" : "var(--text-muted)" }}
|
||||||
</button>
|
onMouseEnter={() => setIconHover(true)}
|
||||||
}
|
onMouseLeave={() => setIconHover(false)}
|
||||||
/>
|
/>
|
||||||
|
: <InfoIcon
|
||||||
|
width="24" height="24"
|
||||||
|
style={{ color: iconHover ? "" : "var(--text-muted)" }}
|
||||||
|
onMouseEnter={() => setIconHover(true)}
|
||||||
|
onMouseLeave={() => setIconHover(false)}
|
||||||
|
/>}
|
||||||
|
</button>
|
||||||
|
</Flex>
|
||||||
|
</Switch>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const enum SearchStatus {
|
export default ErrorBoundary.wrap(function Settings() {
|
||||||
ALL,
|
|
||||||
ENABLED,
|
|
||||||
DISABLED,
|
|
||||||
NEW
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PluginSettings() {
|
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
||||||
|
|
||||||
@ -215,115 +210,51 @@ export default function PluginSettings() {
|
|||||||
return o;
|
return o;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
function hasDependents(plugin: Plugin) {
|
||||||
|
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
||||||
|
return !!enabledDependants?.length;
|
||||||
|
}
|
||||||
|
|
||||||
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
|
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
|
||||||
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL });
|
const [searchValue, setSearchValue] = React.useState({ value: "", status: "all" });
|
||||||
|
|
||||||
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
||||||
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
|
const onStatusChange = (status: string) => setSearchValue(prev => ({ ...prev, status }));
|
||||||
|
|
||||||
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
|
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
|
||||||
const enabled = settings.plugins[plugin.name]?.enabled;
|
const showEnabled = searchValue.status === "enabled" || searchValue.status === "all";
|
||||||
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
|
const showDisabled = searchValue.status === "disabled" || searchValue.status === "all";
|
||||||
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
|
const enabled = settings.plugins[plugin.name]?.enabled || plugin.started;
|
||||||
if (searchValue.status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false;
|
|
||||||
if (!searchValue.value.length) return true;
|
|
||||||
|
|
||||||
const v = searchValue.value.toLowerCase();
|
|
||||||
return (
|
return (
|
||||||
plugin.name.toLowerCase().includes(v) ||
|
((showEnabled && enabled) || (showDisabled && !enabled)) &&
|
||||||
plugin.description.toLowerCase().includes(v) ||
|
(
|
||||||
plugin.tags?.some(t => t.toLowerCase().includes(v))
|
plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) ||
|
||||||
|
plugin.description.toLowerCase().includes(searchValue.value.toLowerCase())
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [newPlugins] = useAwaiter(() => DataStore.get("Vencord_existingPlugins").then((cachedPlugins: Record<string, number> | undefined) => {
|
|
||||||
const now = Date.now() / 1000;
|
|
||||||
const existingTimestamps: Record<string, number> = {};
|
|
||||||
const sortedPluginNames = Object.values(sortedPlugins).map(plugin => plugin.name);
|
|
||||||
|
|
||||||
const newPlugins: string[] = [];
|
|
||||||
for (const { name: p } of sortedPlugins) {
|
|
||||||
const time = existingTimestamps[p] = cachedPlugins?.[p] ?? now;
|
|
||||||
if ((time + 60 * 60 * 24 * 2) > now) {
|
|
||||||
newPlugins.push(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DataStore.set("Vencord_existingPlugins", existingTimestamps);
|
|
||||||
|
|
||||||
return window._.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
|
|
||||||
}));
|
|
||||||
|
|
||||||
type P = JSX.Element | JSX.Element[];
|
|
||||||
let plugins: P, requiredPlugins: P;
|
|
||||||
if (sortedPlugins?.length) {
|
|
||||||
plugins = [];
|
|
||||||
requiredPlugins = [];
|
|
||||||
|
|
||||||
for (const p of sortedPlugins) {
|
|
||||||
if (!p.options && p.name.endsWith("API") && searchValue.value !== "API")
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!pluginFilter(p)) continue;
|
|
||||||
|
|
||||||
const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled);
|
|
||||||
|
|
||||||
if (isRequired) {
|
|
||||||
const tooltipText = p.required
|
|
||||||
? "This plugin is required for Vencord to function."
|
|
||||||
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
|
|
||||||
|
|
||||||
requiredPlugins.push(
|
|
||||||
<Tooltip text={tooltipText} key={p.name}>
|
|
||||||
{({ onMouseLeave, onMouseEnter }) => (
|
|
||||||
<PluginCard
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onRestartNeeded={name => changes.handleChange(name)}
|
|
||||||
disabled={true}
|
|
||||||
plugin={p}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
plugins.push(
|
|
||||||
<PluginCard
|
|
||||||
onRestartNeeded={name => changes.handleChange(name)}
|
|
||||||
disabled={false}
|
|
||||||
plugin={p}
|
|
||||||
isNew={newPlugins?.includes(p.name)}
|
|
||||||
key={p.name}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
plugins = requiredPlugins = <Text variant="text-md/normal">No plugins meet search criteria.</Text>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsTab title="Plugins">
|
<FormSection tag="h1" title="Vencord">
|
||||||
<ReloadRequiredCard required={changes.hasChanges} />
|
<FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
|
Plugins
|
||||||
|
</FormTitle>
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
<ReloadRequiredCard plugins={[...changes.getChanges()]} style={{ marginBottom: 16 }} />
|
||||||
Filters
|
|
||||||
</Forms.FormTitle>
|
|
||||||
|
|
||||||
<div className={cl("filter-controls")}>
|
<div style={styles.FiltersBar}>
|
||||||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
|
<TextInput value={searchValue.value} placeholder={"Search for a plugin..."} onChange={onSearch} style={{ marginBottom: 24 }} />
|
||||||
<div className={InputStyles.inputWrapper}>
|
<div className={InputStyles.inputWrapper}>
|
||||||
<Select
|
<Select
|
||||||
className={InputStyles.inputDefault}
|
className={InputStyles.inputDefault}
|
||||||
options={[
|
options={[
|
||||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
{ label: "Show All", value: "all", default: true },
|
||||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
{ label: "Show Enabled", value: "enabled" },
|
||||||
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
{ label: "Show Disabled", value: "disabled" }
|
||||||
{ label: "Show New", value: SearchStatus.NEW }
|
|
||||||
]}
|
]}
|
||||||
serialize={String}
|
serialize={v => String(v)}
|
||||||
select={onStatusChange}
|
select={onStatusChange}
|
||||||
isSelected={v => v === searchValue.status}
|
isSelected={v => v === searchValue.status}
|
||||||
closeOnSelect={true}
|
closeOnSelect={true}
|
||||||
@ -331,29 +262,62 @@ export default function PluginSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
|
<div style={styles.PluginsGrid}>
|
||||||
|
{sortedPlugins?.length ? sortedPlugins
|
||||||
<div className={cl("grid")}>
|
.filter(a => !a.required && !dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
||||||
{plugins}
|
.map(plugin => {
|
||||||
|
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
||||||
|
const dependency = enabledDependants?.length;
|
||||||
|
return <PluginCard
|
||||||
|
onRestartNeeded={name => changes.add(name)}
|
||||||
|
disabled={plugin.required || !!dependency}
|
||||||
|
plugin={plugin}
|
||||||
|
/>;
|
||||||
|
})
|
||||||
|
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<FormDivider />
|
||||||
<Forms.FormDivider className={Margins.top20} />
|
<FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
|
||||||
Required Plugins
|
Required Plugins
|
||||||
</Forms.FormTitle>
|
</FormTitle>
|
||||||
<div className={cl("grid")}>
|
<div style={styles.PluginsGrid}>
|
||||||
{requiredPlugins}
|
{sortedPlugins?.length ? sortedPlugins
|
||||||
|
.filter(a => a.required || dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
||||||
|
.map(plugin => {
|
||||||
|
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
||||||
|
const dependency = enabledDependants?.length;
|
||||||
|
const tooltipText = plugin.required
|
||||||
|
? "This plugin is required for Vencord to function."
|
||||||
|
: makeDependencyList(dependencyCheck(plugin.name, depMap));
|
||||||
|
return <Tooltip text={tooltipText}>
|
||||||
|
{({ onMouseLeave, onMouseEnter }) => (
|
||||||
|
<PluginCard
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onRestartNeeded={name => changes.add(name)}
|
||||||
|
disabled={plugin.required || !!dependency}
|
||||||
|
plugin={plugin}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tooltip>;
|
||||||
|
})
|
||||||
|
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</SettingsTab >
|
</FormSection>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
function makeDependencyList(deps: string[]) {
|
function makeDependencyList(deps: string[]) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
<FormText>This plugin is required by:</FormText>
|
||||||
{deps.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)}
|
{deps.map((dep: string) => <FormText style={{ margin: "0 auto" }}>{dep}</FormText>)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dependencyCheck(pluginName: string, depMap: Record<string, string[]>): string[] {
|
||||||
|
return depMap[pluginName]?.filter(d => Settings.plugins[d].enabled) || [];
|
||||||
|
}
|
||||||
|
@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.vc-plugins-grid {
|
|
||||||
margin-top: 16px;
|
|
||||||
display: grid;
|
|
||||||
grid-gap: 16px;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-info-button {
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
padding: 0;
|
|
||||||
background: transparent;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-settings-button:hover {
|
|
||||||
color: var(--interactive-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-filter-controls {
|
|
||||||
display: grid;
|
|
||||||
height: 40px;
|
|
||||||
gap: 10px;
|
|
||||||
grid-template-columns: 1fr 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-badge {
|
|
||||||
padding: 0 6px;
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 8px;
|
|
||||||
height: 16px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
color: var(--white-500);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-dep-name {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-info-card {
|
|
||||||
padding: 1em;
|
|
||||||
height: 8em;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-info-card div {
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-restart-card {
|
|
||||||
padding: 1em;
|
|
||||||
background: var(--info-warning-background);
|
|
||||||
border: 1px solid var(--info-warning-foreground);
|
|
||||||
color: var(--info-warning-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-restart-card button {
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-plugins-info-button svg:not(:hover, :focus) {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
@ -16,21 +16,35 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const styles = new Map<string, HTMLStyleElement>();
|
export const PluginsGrid: React.CSSProperties = {
|
||||||
|
marginTop: 16,
|
||||||
export function setStyle(css: string, id: string) {
|
display: "grid",
|
||||||
const style = document.createElement("style");
|
gridGap: 16,
|
||||||
style.innerText = css;
|
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
|
||||||
document.head.appendChild(style);
|
};
|
||||||
styles.set(id, style);
|
|
||||||
}
|
export const PluginsGridItem: React.CSSProperties = {
|
||||||
|
backgroundColor: "var(--background-modifier-selected)",
|
||||||
export function removeStyle(id: string) {
|
color: "var(--interactive-active)",
|
||||||
styles.get(id)?.remove();
|
borderRadius: 3,
|
||||||
return styles.delete(id);
|
cursor: "pointer",
|
||||||
}
|
display: "block",
|
||||||
|
height: "min-content",
|
||||||
export const clearStyles = () => {
|
padding: 10,
|
||||||
styles.forEach(style => style.remove());
|
width: "100%",
|
||||||
styles.clear();
|
};
|
||||||
|
|
||||||
|
export const FiltersBar: React.CSSProperties = {
|
||||||
|
gap: 10,
|
||||||
|
height: 40,
|
||||||
|
gridTemplateColumns: "1fr 150px",
|
||||||
|
display: "grid"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsIcon: React.CSSProperties = {
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
padding: "0",
|
||||||
|
background: "transparent",
|
||||||
|
marginRight: 8
|
||||||
};
|
};
|
@ -1,3 +0,0 @@
|
|||||||
[class|="userPopoutOuter"] [class*="botTag"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
121
src/components/Settings.tsx
Normal file
121
src/components/Settings.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FormDivider, FormSection, FormText, FormTitle } from "@components/Forms";
|
||||||
|
|
||||||
|
import { useSettings } from "../api/settings";
|
||||||
|
import { ChangeList } from "../utils/ChangeList";
|
||||||
|
import IpcEvents from "../utils/IpcEvents";
|
||||||
|
import { useAwaiter } from "../utils/misc";
|
||||||
|
import { Alerts, Button, Margins, Parser, React, Switch } from "../webpack/common";
|
||||||
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
|
import { Flex } from "./Flex";
|
||||||
|
import { launchMonacoEditor } from "./Monaco";
|
||||||
|
|
||||||
|
export default ErrorBoundary.wrap(function Settings() {
|
||||||
|
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading...");
|
||||||
|
const settings = useSettings();
|
||||||
|
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => void (changes.hasChanges && Alerts.show({
|
||||||
|
title: "Restart required",
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<p>The following plugins require a restart:</p>
|
||||||
|
<div>{changes.map((s, i) => (
|
||||||
|
<>
|
||||||
|
{i > 0 && ", "}
|
||||||
|
{Parser.parse("`" + s + "`")}
|
||||||
|
</>
|
||||||
|
))}</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
confirmText: "Restart now",
|
||||||
|
cancelText: "Later!",
|
||||||
|
onConfirm: () => location.reload()
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormSection tag="h1" title="Vencord">
|
||||||
|
<FormTitle tag="h5">
|
||||||
|
Settings
|
||||||
|
</FormTitle>
|
||||||
|
|
||||||
|
<FormText>
|
||||||
|
Settings Directory: <code style={{ userSelect: "text", cursor: "text" }}>{settingsDir}</code>
|
||||||
|
</FormText>
|
||||||
|
|
||||||
|
{!IS_WEB && <Flex className={Margins.marginBottom20} style={{ marginTop: 8 }}>
|
||||||
|
<Button
|
||||||
|
onClick={() => window.DiscordNative.app.relaunch()}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
color={Button.Colors.GREEN}
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={settingsDirPending}
|
||||||
|
>
|
||||||
|
Launch Directory
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_MONACO_EDITOR)}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={settingsDir === "Loading..."}
|
||||||
|
>
|
||||||
|
Open QuickCSS File
|
||||||
|
</Button>
|
||||||
|
</Flex>}
|
||||||
|
|
||||||
|
{IS_WEB && <Button
|
||||||
|
onClick={launchMonacoEditor}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={settingsDir === "Loading..."}
|
||||||
|
>
|
||||||
|
Open QuickCSS File
|
||||||
|
</Button>}
|
||||||
|
|
||||||
|
<FormDivider />
|
||||||
|
<Switch
|
||||||
|
value={settings.useQuickCss}
|
||||||
|
onChange={(v: boolean) => settings.useQuickCss = v}
|
||||||
|
note="Loads styles from your QuickCss file"
|
||||||
|
>
|
||||||
|
Use QuickCss
|
||||||
|
</Switch>
|
||||||
|
{!IS_WEB && <Switch
|
||||||
|
value={settings.enableReactDevtools}
|
||||||
|
onChange={(v: boolean) => settings.enableReactDevtools = v}
|
||||||
|
note="Requires a full restart"
|
||||||
|
>
|
||||||
|
Enable React Developer Tools
|
||||||
|
</Switch>}
|
||||||
|
{!IS_WEB && <Switch
|
||||||
|
value={settings.notifyAboutUpdates}
|
||||||
|
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||||
|
note="Shows a Toast on StartUp"
|
||||||
|
>
|
||||||
|
Get notified about new Updates
|
||||||
|
</Switch>}
|
||||||
|
</FormSection>
|
||||||
|
);
|
||||||
|
});
|
@ -1,3 +0,0 @@
|
|||||||
.vc-switch-slider {
|
|
||||||
transition: 100ms transform ease-in-out;
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user