Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
79b35d5797 | ||
|
62194674eb | ||
|
04da98498f | ||
|
6fa0fb017b | ||
|
11ecc45b71 | ||
|
82cd8d98f6 | ||
|
c815f1c5f3 | ||
|
e248f58d9f | ||
|
3171b78a36 | ||
|
525aa3af33 | ||
|
b7299ea2cc |
@ -4,7 +4,7 @@
|
||||
"ignorePatterns": ["dist", "browser"],
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"simple-header",
|
||||
"header",
|
||||
"simple-import-sort",
|
||||
"unused-imports",
|
||||
"path-alias"
|
||||
@ -26,12 +26,35 @@
|
||||
// Since it's only been a month and Vencord has already been stolen
|
||||
// by random skids who rebranded it to "AlphaCord" and erased all license
|
||||
// information
|
||||
"simple-header/header": [
|
||||
"error",
|
||||
{
|
||||
"files": ["scripts/header-new.txt", "scripts/header-old.txt"],
|
||||
"templates": { "author": [".*", "Vendicated and contributors"] }
|
||||
}
|
||||
"header/header": [
|
||||
2,
|
||||
"block",
|
||||
[
|
||||
{
|
||||
"pattern": "!?",
|
||||
"template": " "
|
||||
},
|
||||
" * Vencord, a modification for Discord's desktop app",
|
||||
{
|
||||
"pattern": " \\* Copyright \\(c\\) \\d{4}",
|
||||
"template": " * Copyright (c) 2023 Vendicated and contributors"
|
||||
},
|
||||
" *",
|
||||
" * This program is free software: you can redistribute it and/or modify",
|
||||
" * it under the terms of the GNU General Public License as published by",
|
||||
" * the Free Software Foundation, either version 3 of the License, or",
|
||||
" * (at your option) any later version.",
|
||||
" *",
|
||||
" * This program is distributed in the hope that it will be useful,",
|
||||
" * but WITHOUT ANY WARRANTY; without even the implied warranty of",
|
||||
" * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the",
|
||||
" * GNU General Public License for more details.",
|
||||
" *",
|
||||
" * You should have received a copy of the GNU General Public License",
|
||||
" * along with this program. If not, see <https://www.gnu.org/licenses/>.",
|
||||
""
|
||||
],
|
||||
2
|
||||
],
|
||||
"quotes": ["error", "double", { "avoidEscape": true }],
|
||||
"jsx-quotes": ["error", "prefer-double"],
|
||||
@ -39,7 +62,7 @@
|
||||
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||
"arrow-parens": ["error", "as-needed"],
|
||||
"eol-last": ["error", "always"],
|
||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
||||
"func-call-spacing": ["error", "never"],
|
||||
"no-multi-spaces": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-whitespace-before-property": "error",
|
||||
|
22
.github/ISSUE_TEMPLATE/blank.yml
vendored
Normal file
22
.github/ISSUE_TEMPLATE/blank.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: Blank Template
|
||||
description: Use this only if your issue does not fit into another template. **DO NOT ASK FOR SUPPORT OR REQUEST PLUGINS**
|
||||
labels: []
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: info-sec
|
||||
attributes:
|
||||
label: Tell us all about it.
|
||||
description: Go nuts, let us know what you're wanting to bring attention to.
|
||||
placeholder: ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: agreement-check
|
||||
attributes:
|
||||
label: Request Agreement
|
||||
description: DO NOT USE THIS TEMPLATE FOR SUPPORT OR PLUGIN REQUESTS!!! For Support, **join our Discord**. For plugin requests, **use discussions**
|
||||
options:
|
||||
- label: This is not a support or plugin request
|
||||
required: true
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: true
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Vencord Support Server
|
||||
url: https://discord.gg/D9uwnFnqmd
|
||||
|
40
.github/workflows/build.yml
vendored
40
.github/workflows/build.yml
vendored
@ -42,39 +42,37 @@ jobs:
|
||||
|
||||
- name: Clean up obsolete files
|
||||
run: |
|
||||
rm -rf dist/*-unpacked Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
|
||||
rm -rf dist/*-unpacked Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
|
||||
|
||||
- name: Get some values needed for the release
|
||||
id: release_values
|
||||
run: |
|
||||
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload DevBuild as release
|
||||
if: github.repository == 'Vendicated/Vencord'
|
||||
run: |
|
||||
gh release upload devbuild --clobber dist/*
|
||||
gh release edit devbuild --title "DevBuild $RELEASE_TAG"
|
||||
gh release upload devbuild --clobber dist/*
|
||||
gh release edit devbuild --title "DevBuild $RELEASE_TAG"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ env.release_tag }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ env.release_tag }}
|
||||
|
||||
- name: Upload DevBuild to builds repo
|
||||
if: github.repository == 'Vendicated/Vencord'
|
||||
run: |
|
||||
git config --global user.name "$USERNAME"
|
||||
git config --global user.email actions@github.com
|
||||
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
|
||||
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/* .
|
||||
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
|
||||
git add -A
|
||||
git commit -m "Builds for https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA"
|
||||
git push --force https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git
|
||||
env:
|
||||
API_TOKEN: ${{ secrets.BUILDS_TOKEN }}
|
||||
GH_REPO: Vencord/builds
|
||||
USERNAME: GitHub-Actions
|
||||
API_TOKEN: ${{ secrets.BUILDS_TOKEN }}
|
||||
GH_REPO: Vencord/builds
|
||||
USERNAME: GitHub-Actions
|
||||
|
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 }}
|
47
.github/workflows/publish.yml
vendored
47
.github/workflows/publish.yml
vendored
@ -6,7 +6,6 @@ on:
|
||||
|
||||
jobs:
|
||||
Publish:
|
||||
if: github.repository == 'Vendicated/Vencord'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@ -14,11 +13,11 @@ jobs:
|
||||
|
||||
- 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
|
||||
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
|
||||
|
||||
@ -36,26 +35,26 @@ jobs:
|
||||
|
||||
- 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
|
||||
# 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=$?
|
||||
# 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=$?
|
||||
# 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
|
||||
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 }}
|
||||
# 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 }}
|
||||
# Firefox
|
||||
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
|
||||
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}
|
||||
|
37
.github/workflows/reportBrokenPlugins.yml
vendored
37
.github/workflows/reportBrokenPlugins.yml
vendored
@ -2,12 +2,11 @@ name: Test Patches
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Every day at midnight
|
||||
- cron: 0 0 * * *
|
||||
# Every day at midnight
|
||||
- cron: 0 0 * * *
|
||||
|
||||
jobs:
|
||||
TestPlugins:
|
||||
if: github.repository == 'Vendicated/Vencord'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@ -23,10 +22,10 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm add puppeteer
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm add puppeteer
|
||||
|
||||
sudo apt-get install -y chromium-browser
|
||||
sudo apt-get install -y chromium-browser
|
||||
|
||||
- name: Build web
|
||||
run: pnpm buildWeb --standalone
|
||||
@ -34,25 +33,25 @@ jobs:
|
||||
- name: Create Report
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||
export CHROMIUM_BIN=$(which chromium-browser)
|
||||
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
|
||||
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 }}
|
||||
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
|
||||
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
|
||||
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 }}
|
||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
7
.github/workflows/test.yml
vendored
7
.github/workflows/test.yml
vendored
@ -26,8 +26,5 @@ jobs:
|
||||
- name: Lint & Test if desktop version compiles
|
||||
run: pnpm test
|
||||
|
||||
- name: Test if web version compiles
|
||||
run: pnpm buildWeb
|
||||
|
||||
- name: Test if plugin structure is valid
|
||||
run: pnpm generatePluginJson
|
||||
- name: Lint & Test if web version compiles
|
||||
run: pnpm testWeb
|
||||
|
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@ -12,12 +12,5 @@
|
||||
"javascript.format.semicolons": "insert",
|
||||
"typescript.format.semicolons": "insert",
|
||||
"typescript.preferences.quoteStyle": "double",
|
||||
"javascript.preferences.quoteStyle": "double",
|
||||
|
||||
"gitlens.remotes": [
|
||||
{
|
||||
"domain": "codeberg.org",
|
||||
"type": "Gitea"
|
||||
}
|
||||
]
|
||||
"javascript.preferences.quoteStyle": "double"
|
||||
}
|
||||
|
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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
56
README.md
56
README.md
@ -1,31 +1,22 @@
|
||||
# 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)
|
||||
|
||||
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
|
||||
|
||||
- Super easy to install (Download Installer, open, click install button, done)
|
||||
- 100+ plugins built in: [See a list](https://vencord.dev/plugins)
|
||||
- Some highlights: SpotifyControls, MessageLogger, Experiments, GameActivityToggle, Translate, NoTrack, QuickReply, Free Emotes/Stickers, PermissionsViewer, CustomCommands, ShowHiddenChannels, PronounDB
|
||||
- 100+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
||||
- Some highlights: SpotifyControls, GameActivityToggle, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
|
||||
- Fairly lightweight despite the many inbuilt plugins
|
||||
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
||||
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)
|
||||
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
||||
- 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
|
||||
|
||||
Click the below button to install Vencord to the Discord Desktop app
|
||||
|
||||
[![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)
|
||||
[![Download and run the Installer ](https://img.shields.io/github/v/release/Vencord/Installer?label=Download%20Vencord%20Installer&style=for-the-badge)](https://github.com/Vencord/Installer#usage)
|
||||
|
||||
## Installing on Browser
|
||||
|
||||
@ -33,48 +24,25 @@ Click the below button to install Vencord to the Discord Desktop app
|
||||
|
||||
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
|
||||
|
||||
<details>
|
||||
<summary>Alternative Downloads</summary>
|
||||
## Building from Source
|
||||
|
||||
## Vencord Desktop
|
||||
See the docs folder
|
||||
|
||||
> **Warning**
|
||||
> 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
|
||||
## Contributing
|
||||
|
||||
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
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) and [Megu's Plugin Guide!](docs/2_PLUGINS.md)
|
||||
|
||||
[![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)
|
||||
[contribute]: CONTRIBUTING.md
|
||||
|
||||
</details>
|
||||
[contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute]
|
||||
|
||||
## Join our Support/Community Server
|
||||
## Join
|
||||
|
||||
https://discord.gg/D9uwnFnqmd
|
||||
[join]: https://discord.gg/D9uwnFnqmd
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#Vendicated/Vencord&Timeline">
|
||||
<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>
|
||||
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
|
||||
|
||||
## Disclaimer
|
||||
|
||||
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>
|
||||
|
@ -16,6 +16,20 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function fetchOptions(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const opt = {
|
||||
method: "OPTIONS",
|
||||
url: url,
|
||||
};
|
||||
opt.onload = resp => resolve(resp.responseHeaders);
|
||||
opt.ontimeout = () => reject("fetch timeout");
|
||||
opt.onerror = () => reject("fetch error");
|
||||
opt.onabort = () => reject("fetch abort");
|
||||
GM_xmlhttpRequest(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function parseHeaders(headers) {
|
||||
if (!headers)
|
||||
return {};
|
||||
@ -38,6 +52,19 @@ function parseHeaders(headers) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// returns true if CORS permits request
|
||||
async function checkCors(url, method) {
|
||||
const headers = parseHeaders(await fetchOptions(url));
|
||||
|
||||
const origin = headers["access-control-allow-origin"];
|
||||
if (origin !== "*" && origin !== window.location.origin) return false;
|
||||
|
||||
const methods = headers["access-control-allow-methods"]?.toLowerCase().split(/,\s/g);
|
||||
if (methods && !methods.includes(method.toLowerCase())) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function blobTo(to, blob) {
|
||||
if (to === "arrayBuffer" && blob.arrayBuffer) return blob.arrayBuffer();
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -51,25 +78,31 @@ function blobTo(to, blob) {
|
||||
|
||||
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);
|
||||
checkCors(url, opt?.method || "GET")
|
||||
.then(can => {
|
||||
if (can) {
|
||||
// https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest
|
||||
const options = opt || {};
|
||||
options.url = url;
|
||||
options.data = options.body;
|
||||
options.responseType = "blob";
|
||||
options.onload = resp => {
|
||||
var blob = resp.response;
|
||||
resp.blob = () => Promise.resolve(blob);
|
||||
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
|
||||
resp.text = () => blobTo("text", blob);
|
||||
resp.json = async () => JSON.parse(await blobTo("text", blob));
|
||||
resp.headers = new Headers(parseHeaders(resp.responseHeaders));
|
||||
resolve(resp);
|
||||
};
|
||||
options.ontimeout = () => reject("fetch timeout");
|
||||
options.onerror = () => reject("fetch error");
|
||||
options.onabort = () => reject("fetch abort");
|
||||
GM_xmlhttpRequest(options);
|
||||
} else {
|
||||
reject("CORS issue");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
export const fetch = GM_fetch;
|
||||
|
@ -16,86 +16,51 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// <reference path="../src/modules.d.ts" />
|
||||
/// <reference path="../src/globals.d.ts" />
|
||||
|
||||
import monacoHtml from "~fileContent/../src/components/monacoWin.html";
|
||||
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";
|
||||
import IpcEvents from "../src/utils/IpcEvents";
|
||||
|
||||
// Discord deletes this so need to store in variable
|
||||
const { localStorage } = window;
|
||||
|
||||
// listeners for ipc.on
|
||||
const cssListeners = new Set<(css: string) => void>();
|
||||
const NOOP = () => { };
|
||||
const NOOP_ASYNC = async () => { };
|
||||
const listeners = {} as Record<string, Set<Function>>;
|
||||
|
||||
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
|
||||
window.VencordNative = {
|
||||
themes: {
|
||||
uploadTheme: (fileName: string, fileData: string) => DataStore.set(fileName, fileData, themeStore),
|
||||
deleteTheme: (fileName: string) => DataStore.del(fileName, themeStore),
|
||||
getThemesDir: async () => "",
|
||||
getThemesList: () => DataStore.entries(themeStore).then(entries =>
|
||||
entries.map(([name, css]) => getThemeInfo(css, name.toString()))
|
||||
),
|
||||
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));
|
||||
getVersions: () => ({}),
|
||||
ipc: {
|
||||
send: (event: string, ...args: any[]) => void onEvent(event, ...args),
|
||||
sendSync: onEvent,
|
||||
on(event: string, listener: () => {}) {
|
||||
(listeners[event] ??= new Set()).add(listener);
|
||||
},
|
||||
addChangeListener(cb) {
|
||||
cssListeners.add(cb);
|
||||
},
|
||||
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);
|
||||
off(event: string, listener: () => {}) {
|
||||
return listeners[event]?.delete(listener);
|
||||
},
|
||||
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,
|
||||
};
|
||||
|
@ -16,7 +16,7 @@ chrome.webRequest.onHeadersReceived.addListener(
|
||||
// In main frame requests, the CSP needs to be removed to enable fetching of custom css
|
||||
// as desired by the user
|
||||
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-security-policy");
|
||||
} else if (type === "stylesheet" && url.startsWith("https://raw.githubusercontent.com/")) {
|
||||
} else if (type === "stylesheet" && url.startsWith("https://raw.githubusercontent.com")) {
|
||||
// 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");
|
||||
|
@ -41,5 +41,12 @@
|
||||
"path": "modifyResponseHeaders.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "vencord-firefox@vendicated.dev",
|
||||
"strict_min_version": "109.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
> [!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.
|
||||
> **Warning**
|
||||
> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead.
|
||||
|
||||
# Installation Guide
|
||||
|
||||
|
@ -26,6 +26,10 @@ export default definePlugin({
|
||||
name: "Your Name",
|
||||
},
|
||||
],
|
||||
// Delete `patches` if you are not using code patches, as it will make
|
||||
// your plugin require restarts, and your stop() method will not be
|
||||
// invoked at all. The presence of the key in the object alone is
|
||||
// enough to trigger this behavior, even if the value is an empty array.
|
||||
patches: [],
|
||||
// Delete these two below if you are only using code patches
|
||||
start() {},
|
||||
|
47
package.json
47
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.4.3",
|
||||
"version": "1.1.9",
|
||||
"description": "The cutest Discord client mod",
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
@ -11,7 +11,7 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Vendicated/Vencord.git"
|
||||
},
|
||||
"license": "GPL-3.0-or-later",
|
||||
"license": "GPL-3.0",
|
||||
"author": "Vendicated",
|
||||
"directories": {
|
||||
"doc": "docs"
|
||||
@ -24,52 +24,55 @@
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
|
||||
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
|
||||
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc",
|
||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
||||
"testTsc": "tsc --noEmit",
|
||||
"uninject": "node scripts/runInstaller.mjs",
|
||||
"watch": "node scripts/build/build.mjs --watch"
|
||||
"watch": "node scripts/build/build.mjs --watch",
|
||||
"buildTypes": "ttsc --emitDeclarationOnly --declaration --outDir packages/vencord-types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vap/core": "0.0.12",
|
||||
"@vap/shiki": "0.10.5",
|
||||
"eslint-plugin-simple-header": "^1.0.2",
|
||||
"@vap/shiki": "0.10.3",
|
||||
"fflate": "^0.7.4",
|
||||
"nanoid": "^4.0.2",
|
||||
"virtual-merge": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.0.3",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^18.16.3",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@types/diff": "^5.0.2",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/yazl": "^2.4.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||
"@typescript-eslint/parser": "^5.59.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.49.0",
|
||||
"diff": "^5.1.0",
|
||||
"discord-types": "^1.3.26",
|
||||
"esbuild": "^0.15.18",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-path-alias": "^1.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"highlight.js": "10.6.0",
|
||||
"moment": "^2.29.4",
|
||||
"puppeteer-core": "^19.11.1",
|
||||
"puppeteer-core": "^19.6.0",
|
||||
"standalone-electron-types": "^1.0.0",
|
||||
"stylelint": "^15.6.0",
|
||||
"stylelint-config-standard": "^33.0.0",
|
||||
"tsx": "^3.12.7",
|
||||
"type-fest": "^3.9.0",
|
||||
"typescript": "^5.0.4"
|
||||
"stylelint": "^14.16.1",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"tsx": "^3.12.6",
|
||||
"ttypescript": "^1.5.15",
|
||||
"type-fest": "^3.5.3",
|
||||
"typescript": "^4.9.4",
|
||||
"typescript-transform-paths": "^3.4.6"
|
||||
},
|
||||
"packageManager": "pnpm@8.1.1",
|
||||
"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"
|
||||
"eslint@8.28.0": "patches/eslint@8.28.0.patch"
|
||||
},
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
|
7
packages/vencord-types/.gitignore
vendored
Normal file
7
packages/vencord-types/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
*
|
||||
!.*ignore
|
||||
!package.json
|
||||
!README.md
|
||||
!prepare.ts
|
||||
!index.d.ts
|
||||
!globals.d.ts
|
3
packages/vencord-types/.npmignore
Normal file
3
packages/vencord-types/.npmignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
prepare.ts
|
||||
.gitignore
|
11
packages/vencord-types/README.md
Normal file
11
packages/vencord-types/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Vencord Types
|
||||
|
||||
Typings for Vencord's api, published to npm
|
||||
|
||||
```sh
|
||||
npm i @vencord/types
|
||||
|
||||
yarn add @vencord/types
|
||||
|
||||
pnpm add @vencord/types
|
||||
```
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) {year} {author}
|
||||
* 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
|
||||
@ -15,3 +15,10 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare global {
|
||||
export var VencordNative: typeof import("./VencordNative").default;
|
||||
export var Vencord: typeof import("./Vencord");
|
||||
}
|
||||
|
||||
export { };
|
5
packages/vencord-types/index.d.ts
vendored
Normal file
5
packages/vencord-types/index.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/* eslint-disable */
|
||||
|
||||
/// <reference path="Vencord.d.ts" />
|
||||
/// <reference path="globals.d.ts" />
|
||||
/// <reference path="modules.d.ts" />
|
26
packages/vencord-types/package.json
Normal file
26
packages/vencord-types/package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@vencord/types",
|
||||
"private": false,
|
||||
"version": "0.1.3",
|
||||
"description": "",
|
||||
"types": "index.d.ts",
|
||||
"scripts": {
|
||||
"prepublishOnly": "tsx ./prepare.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Vencord",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"tsx": "^3.12.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"discord-types": "^1.3.26",
|
||||
"standalone-electron-types": "^1.0.0",
|
||||
"type-fest": "^3.5.3"
|
||||
}
|
||||
}
|
44
packages/vencord-types/prepare.ts
Normal file
44
packages/vencord-types/prepare.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { cpSync, readdirSync, rmSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
const SRC = join(__dirname, "..", "..", "src");
|
||||
|
||||
for (const file of ["preload.d.ts", "userplugins", "main", "debug"]) {
|
||||
rmSync(join(__dirname, file), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function copyDtsFiles(from: string, to: string) {
|
||||
for (const file of readdirSync(from, { withFileTypes: true })) {
|
||||
// bad
|
||||
if (from === SRC && file.name === "globals.d.ts") continue;
|
||||
|
||||
const fullFrom = join(from, file.name);
|
||||
const fullTo = join(to, file.name);
|
||||
|
||||
if (file.isDirectory()) {
|
||||
copyDtsFiles(fullFrom, fullTo);
|
||||
} else if (file.name.endsWith(".d.ts")) {
|
||||
cpSync(fullFrom, fullTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
copyDtsFiles(SRC, __dirname);
|
@ -1,11 +1,11 @@
|
||||
diff --git a/lib/rules/no-useless-escape.js b/lib/rules/no-useless-escape.js
|
||||
index 0e0f6f09f2c35f3276173c08f832cde9f2cf56a0..7dc22851715f3574d935f513c1b5e35552985711 100644
|
||||
index 2046a148a17fd1d5f3a4bbc9f45f7700259d11fa..f4898c6b57355a4fd72c43a9f32bf1a36a6ccf4a 100644
|
||||
--- a/lib/rules/no-useless-escape.js
|
||||
+++ b/lib/rules/no-useless-escape.js
|
||||
@@ -65,13 +65,31 @@ module.exports = {
|
||||
@@ -97,12 +97,30 @@ module.exports = {
|
||||
escapeBackslash: "Replace the `\\` with `\\\\` to include the actual backslash character."
|
||||
},
|
||||
|
||||
|
||||
- schema: []
|
||||
+ schema: [{
|
||||
+ type: "object",
|
||||
@ -22,28 +22,24 @@ index 0e0f6f09f2c35f3276173c08f832cde9f2cf56a0..7dc22851715f3574d935f513c1b5e355
|
||||
+ 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));
|
||||
+ const { extra, extraCharClass } = options || ''
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
+ const NON_CHARCLASS_ESCAPES = union(REGEX_NON_CHARCLASS_ESCAPES, new Set(extra))
|
||||
+ const CHARCLASS_ESCAPES = union(REGEX_GENERAL_ESCAPES, new Set(extraCharClass))
|
||||
+
|
||||
/**
|
||||
* Reports a node
|
||||
* @param {ASTNode} node The node to report
|
||||
@@ -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;
|
||||
@@ -238,7 +256,7 @@ module.exports = {
|
||||
.filter(charInfo => charInfo.escaped)
|
||||
|
||||
// Filter out characters that are valid to escape, based on their position in the regular expression.
|
||||
- .filter(charInfo => !(charInfo.inCharClass ? REGEX_GENERAL_ESCAPES : REGEX_NON_CHARCLASS_ESCAPES).has(charInfo.text))
|
||||
+ .filter(charInfo => !(charInfo.inCharClass ? CHARCLASS_ESCAPES : NON_CHARCLASS_ESCAPES).has(charInfo.text))
|
||||
|
||||
// Report all the remaining characters.
|
||||
.forEach(charInfo => report(node, charInfo.index, charInfo.text));
|
1206
pnpm-lock.yaml
generated
1206
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- packages/*
|
@ -19,13 +19,11 @@
|
||||
|
||||
import esbuild from "esbuild";
|
||||
|
||||
import { commonOpts, globPlugins, isStandalone, VERSION, watch } from "./common.mjs";
|
||||
import { commonOpts, globPlugins, isStandalone, watch } from "./common.mjs";
|
||||
|
||||
const defines = {
|
||||
IS_STANDALONE: isStandalone,
|
||||
IS_DEV: JSON.stringify(watch),
|
||||
VERSION: JSON.stringify(VERSION),
|
||||
BUILD_TIMESTAMP: Date.now(),
|
||||
IS_DEV: JSON.stringify(watch)
|
||||
};
|
||||
if (defines.IS_STANDALONE === "false")
|
||||
// If this is a local build (not standalone), optimise
|
||||
@ -40,6 +38,8 @@ const nodeCommonOpts = {
|
||||
format: "cjs",
|
||||
platform: "node",
|
||||
target: ["esnext"],
|
||||
minify: true,
|
||||
bundle: true,
|
||||
external: ["electron", ...commonOpts.external],
|
||||
define: defines,
|
||||
};
|
||||
@ -48,7 +48,16 @@ const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.j
|
||||
const sourcemap = watch ? "inline" : "external";
|
||||
|
||||
await Promise.all([
|
||||
// Discord Desktop main & renderer & preload
|
||||
// common preload
|
||||
esbuild.build({
|
||||
...nodeCommonOpts,
|
||||
entryPoints: ["src/preload.ts"],
|
||||
outfile: "dist/preload.js",
|
||||
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
|
||||
sourcemap,
|
||||
}),
|
||||
|
||||
// Discord Desktop main & renderer
|
||||
esbuild.build({
|
||||
...nodeCommonOpts,
|
||||
entryPoints: ["src/main/index.ts"],
|
||||
@ -67,7 +76,7 @@ await Promise.all([
|
||||
outfile: "dist/renderer.js",
|
||||
format: "iife",
|
||||
target: ["esnext"],
|
||||
footer: { js: "//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
|
||||
footer: { js: "Vencord.Plugins.loadExternalPlugins();\n//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
|
||||
globalName: "Vencord",
|
||||
sourcemap,
|
||||
plugins: [
|
||||
@ -81,20 +90,8 @@ await Promise.all([
|
||||
IS_VENCORD_DESKTOP: 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_VENCORD_DESKTOP: false
|
||||
}
|
||||
}),
|
||||
|
||||
// Vencord Desktop main & renderer & preload
|
||||
// Vencord Desktop main & renderer
|
||||
esbuild.build({
|
||||
...nodeCommonOpts,
|
||||
entryPoints: ["src/main/index.ts"],
|
||||
@ -113,7 +110,7 @@ await Promise.all([
|
||||
outfile: "dist/vencordDesktopRenderer.js",
|
||||
format: "iife",
|
||||
target: ["esnext"],
|
||||
footer: { js: "//# sourceURL=VencordDesktopRenderer\n" + sourceMapFooter("vencordDesktopRenderer") },
|
||||
footer: { js: "Vencord.Plugins.loadExternalPlugins();\n//# sourceURL=VencordDesktopRenderer\n" + sourceMapFooter("vencordDesktopRenderer") },
|
||||
globalName: "Vencord",
|
||||
sourcemap,
|
||||
plugins: [
|
||||
@ -127,18 +124,6 @@ await Promise.all([
|
||||
IS_VENCORD_DESKTOP: 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_VENCORD_DESKTOP: true
|
||||
}
|
||||
}),
|
||||
]).catch(err => {
|
||||
console.error("Build failed");
|
||||
console.error(err.message);
|
||||
|
@ -24,7 +24,9 @@ import { readFileSync } from "fs";
|
||||
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
||||
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, globPlugins, watch } from "./common.mjs";
|
||||
|
||||
/**
|
||||
* @type {esbuild.BuildOptions}
|
||||
@ -45,9 +47,7 @@ const commonOptions = {
|
||||
IS_STANDALONE: "true",
|
||||
IS_DEV: JSON.stringify(watch),
|
||||
IS_DISCORD_DESKTOP: "false",
|
||||
IS_VENCORD_DESKTOP: "false",
|
||||
VERSION: JSON.stringify(VERSION),
|
||||
BUILD_TIMESTAMP: Date.now(),
|
||||
IS_VENCORD_DESKTOP: "false"
|
||||
}
|
||||
};
|
||||
|
||||
@ -67,7 +67,7 @@ await Promise.all(
|
||||
},
|
||||
outfile: "dist/Vencord.user.js",
|
||||
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}.${new Date().getTime()}`)
|
||||
},
|
||||
footer: {
|
||||
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
|
||||
@ -88,7 +88,7 @@ async function buildPluginZip(target, files, shouldZip) {
|
||||
let content = await readFile(join("browser", f));
|
||||
if (f.startsWith("manifest")) {
|
||||
const json = JSON.parse(content.toString("utf-8"));
|
||||
json.version = VERSION;
|
||||
json.version = PackageJSON.version;
|
||||
content = new TextEncoder().encode(JSON.stringify(json));
|
||||
}
|
||||
|
||||
|
@ -16,20 +16,12 @@
|
||||
* 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 { existsSync, readFileSync } from "fs";
|
||||
import { readdir, readFile } from "fs/promises";
|
||||
import { join, relative } from "path";
|
||||
import { promisify } from "util";
|
||||
|
||||
// wtf is this assert syntax
|
||||
import PackageJSON from "../../package.json" assert { type: "json" };
|
||||
|
||||
export const VERSION = PackageJSON.version;
|
||||
export const BUILD_TIMESTAMP = Date.now();
|
||||
export const watch = process.argv.includes("--watch");
|
||||
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
||||
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
||||
@ -70,7 +62,7 @@ export const globPlugins = kind => ({
|
||||
});
|
||||
|
||||
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
|
||||
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins"];
|
||||
const pluginDirs = ["plugins", "userplugins"];
|
||||
let code = "";
|
||||
let plugins = "\n";
|
||||
let i = 0;
|
||||
@ -78,9 +70,8 @@ export const globPlugins = kind => ({
|
||||
if (!existsSync(`./src/${dir}`)) continue;
|
||||
const files = await readdir(`./src/${dir}`);
|
||||
for (const file of files) {
|
||||
if (file.startsWith("_") || file.startsWith(".")) continue;
|
||||
if (file.startsWith(".")) continue;
|
||||
if (file === "index.ts") continue;
|
||||
|
||||
const fileBits = file.split(".");
|
||||
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1))) {
|
||||
const mod = fileBits.at(-2);
|
||||
|
62
scripts/genPluginList.js
Normal file
62
scripts/genPluginList.js
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// A script to automatically generate a list of all plugins.
|
||||
// Just copy paste the entire file into a running Vencord install and it will prompt you
|
||||
// to save the file
|
||||
|
||||
// eslint-disable-next-line spaced-comment
|
||||
/// <reference types="../src/modules"/>
|
||||
|
||||
(() => {
|
||||
/**
|
||||
* @type {typeof import("~plugins").default}
|
||||
*/
|
||||
const Plugins = Vencord.Plugins.plugins;
|
||||
|
||||
const header = `
|
||||
<!-- This file is auto generated, do not edit -->
|
||||
|
||||
# Vencord Plugins
|
||||
`;
|
||||
|
||||
let tableOfContents = "\n\n";
|
||||
|
||||
let list = "\n\n";
|
||||
|
||||
for (const p of Object.values(Plugins).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
tableOfContents += `- [${p.name}](#${p.name.replaceAll(" ", "-")})\n`;
|
||||
|
||||
list += `## ${p.name}
|
||||
|
||||
${p.description}
|
||||
|
||||
**Authors**: ${p.authors.map(a => a.name).join(", ")}
|
||||
`;
|
||||
|
||||
if (p.commands?.length) {
|
||||
list += "\n\n#### Commands\n";
|
||||
for (const cmd of p.commands) {
|
||||
list += `${cmd.name} - ${cmd.description}\n\n`;
|
||||
}
|
||||
}
|
||||
list += "\n\n";
|
||||
}
|
||||
|
||||
copy(header + tableOfContents + list);
|
||||
})();
|
@ -19,7 +19,7 @@
|
||||
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 { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
|
||||
|
||||
interface Dev {
|
||||
name: string;
|
||||
@ -29,7 +29,6 @@ interface Dev {
|
||||
interface PluginData {
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
authors: Dev[];
|
||||
dependencies: string[];
|
||||
hasPatches: boolean;
|
||||
@ -66,9 +65,9 @@ function parseDevs() {
|
||||
|
||||
const value = devsDeclaration.initializer.arguments[0];
|
||||
|
||||
if (!isSatisfiesExpression(value) || !isObjectLiteralExpression(value.expression)) throw new Error("Failed to parse devs: not an object literal");
|
||||
if (!isObjectLiteralExpression(value)) return;
|
||||
|
||||
for (const prop of value.expression.properties) {
|
||||
for (const prop of value.properties) {
|
||||
const name = (prop.name as Identifier).text;
|
||||
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
|
||||
|
||||
@ -107,7 +106,6 @@ async function parseFile(fileName: string) {
|
||||
hasCommands: false,
|
||||
enabledByDefault: false,
|
||||
required: false,
|
||||
tags: [] as string[]
|
||||
} as PluginData;
|
||||
|
||||
for (const prop of pluginObj.properties) {
|
||||
@ -130,16 +128,7 @@ async function parseFile(fileName: string) {
|
||||
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;
|
||||
return devs[getName(e)!];
|
||||
});
|
||||
break;
|
||||
case "dependencies":
|
||||
@ -151,6 +140,7 @@ async function parseFile(fileName: string) {
|
||||
case "required":
|
||||
case "enabledByDefault":
|
||||
data[key] = value.kind === SyntaxKind.TrueKeyword;
|
||||
if (!data[key] && value.kind !== SyntaxKind.FalseKeyword) throw fail(`${key} is not a boolean literal`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -170,8 +160,8 @@ async function parseFile(fileName: string) {
|
||||
throw fail("no default export called 'definePlugin' found");
|
||||
}
|
||||
|
||||
async function getEntryPoint(dir: string, dirent: Dirent) {
|
||||
const base = join(dir, dirent.name);
|
||||
async function getEntryPoint(dirent: Dirent) {
|
||||
const base = join("./src/plugins", dirent.name);
|
||||
if (!dirent.isDirectory()) return base;
|
||||
|
||||
for (const name of ["index.ts", "index.tsx"]) {
|
||||
@ -185,23 +175,13 @@ async function getEntryPoint(dir: string, dirent: Dirent) {
|
||||
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 = readdirSync("./src/plugins", { withFileTypes: true }).filter(d => d.name !== "index.ts");
|
||||
|
||||
const plugins = ["src/plugins", "src/plugins/_core"].flatMap(dir =>
|
||||
readdirSync(dir, { withFileTypes: true })
|
||||
.filter(isPluginFile)
|
||||
.map(async dirent =>
|
||||
parseFile(await getEntryPoint(dir, dirent))
|
||||
)
|
||||
);
|
||||
const promises = plugins.map(async dirent => parseFile(await getEntryPoint(dirent)));
|
||||
|
||||
const data = JSON.stringify(await Promise.all(plugins));
|
||||
const data = JSON.stringify(await Promise.all(promises));
|
||||
|
||||
if (process.argv.length > 2) {
|
||||
writeFileSync(process.argv[2], data);
|
||||
|
@ -130,7 +130,7 @@ async function printReport() {
|
||||
},
|
||||
{
|
||||
title: "Discord Errors",
|
||||
description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n")) : "None",
|
||||
description: toCodeBlock(report.otherErrors.join("\n")),
|
||||
color: report.otherErrors.length ? 0xff0000 : 0x00ff00
|
||||
}
|
||||
]
|
||||
@ -194,10 +194,9 @@ page.on("console", async e => {
|
||||
return a.toString();
|
||||
}
|
||||
})
|
||||
).then(a => a.join(" ").trim());
|
||||
).then(a => a.join(" "));
|
||||
|
||||
|
||||
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of")) {
|
||||
if (!text.startsWith("Failed to load resource: the server responded with a status of")) {
|
||||
console.error("Got unexpected error", text);
|
||||
report.otherErrors.push(text);
|
||||
}
|
||||
@ -234,7 +233,7 @@ function runTime(token: string) {
|
||||
// Needs native server to run
|
||||
if (p.name === "WebRichPresence (arRPC)") return;
|
||||
|
||||
Vencord.Settings.plugins[p.name].enabled = true;
|
||||
p.required = true;
|
||||
p.patches?.forEach(patch => {
|
||||
patch.plugin = p.name;
|
||||
delete patch.predicate;
|
||||
@ -263,12 +262,12 @@ function runTime(token: string) {
|
||||
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"));
|
||||
.then(t => t.includes(".module.wasm"));
|
||||
|
||||
if (!isWasm)
|
||||
await wreq.e(id as any);
|
||||
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
console.error("[PUP_DEBUG]", "Finished loading chunks!");
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
Vencord, a Discord client mod
|
||||
Copyright (c) {year} {author}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
@ -27,34 +27,19 @@ export { PlainSettings, Settings };
|
||||
import "./utils/quickCss";
|
||||
import "./webpack/patchWebpack";
|
||||
|
||||
import { get as dsGet } from "./api/DataStore";
|
||||
import { showNotification } from "./api/Notifications";
|
||||
import { PlainSettings, Settings } from "./api/Settings";
|
||||
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 { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
|
||||
import { onceReady } from "./webpack";
|
||||
import { SettingsRouter } from "./webpack/common";
|
||||
|
||||
async function syncSettings() {
|
||||
// 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;
|
||||
}
|
||||
export let Components: any;
|
||||
|
||||
async function syncSettings() {
|
||||
if (
|
||||
Settings.cloud.settingsSync && // if it's enabled
|
||||
Settings.cloud.authenticated // if cloud integrations are enabled
|
||||
@ -80,6 +65,7 @@ async function syncSettings() {
|
||||
async function init() {
|
||||
await onceReady;
|
||||
startAllPlugins();
|
||||
Components = await import("./components");
|
||||
|
||||
syncSettings();
|
||||
|
||||
@ -90,6 +76,7 @@ async function init() {
|
||||
|
||||
if (Settings.autoUpdate) {
|
||||
await update();
|
||||
await rebuild();
|
||||
if (Settings.autoUpdateNotification)
|
||||
setTimeout(() => showNotification({
|
||||
title: "Vencord has been updated!",
|
||||
|
@ -1,71 +1,49 @@
|
||||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { IpcEvents } from "@utils/IpcEvents";
|
||||
import { IpcRes } from "@utils/types";
|
||||
import { ipcRenderer } from "electron";
|
||||
import type { UserThemeHeader } from "main/themes";
|
||||
import IPC_EVENTS from "@utils/IpcEvents";
|
||||
import { IpcRenderer, ipcRenderer } from "electron";
|
||||
|
||||
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
|
||||
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 {
|
||||
themes: {
|
||||
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData),
|
||||
deleteTheme: (fileName: string) => invoke<void>(IpcEvents.DELETE_THEME, fileName),
|
||||
getThemesDir: () => invoke<string>(IpcEvents.GET_THEMES_DIR),
|
||||
getThemesList: () => invoke<Array<UserThemeHeader>>(IpcEvents.GET_THEMES_LIST),
|
||||
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));
|
||||
getVersions: () => process.versions,
|
||||
ipc: {
|
||||
send(event: string, ...args: any[]) {
|
||||
assertEventAllowed(event);
|
||||
ipcRenderer.send(event, ...args);
|
||||
},
|
||||
|
||||
addThemeChangeListener(cb: () => void) {
|
||||
ipcRenderer.on(IpcEvents.THEME_UPDATE, cb);
|
||||
sendSync<T = any>(event: string, ...args: any[]): T {
|
||||
assertEventAllowed(event);
|
||||
return ipcRenderer.sendSync(event, ...args);
|
||||
},
|
||||
|
||||
openFile: () => invoke<void>(IpcEvents.OPEN_QUICKCSS),
|
||||
openEditor: () => invoke<void>(IpcEvents.OPEN_MONACO_EDITOR),
|
||||
},
|
||||
|
||||
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),
|
||||
on(event: string, listener: Parameters<IpcRenderer["on"]>[1]) {
|
||||
assertEventAllowed(event);
|
||||
ipcRenderer.on(event, listener);
|
||||
},
|
||||
VoiceMessages: {
|
||||
readRecording: (path: string) => invoke<Uint8Array | null>(IpcEvents.VOICE_MESSAGES_READ_RECORDING, path),
|
||||
off(event: string, listener: Parameters<IpcRenderer["off"]>[1]) {
|
||||
assertEventAllowed(event);
|
||||
ipcRenderer.off(event, listener);
|
||||
},
|
||||
invoke<T = any>(event: string, ...args: any[]): Promise<T> {
|
||||
assertEventAllowed(event);
|
||||
return ipcRenderer.invoke(event, ...args);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ import { ComponentType, HTMLProps } from "react";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
|
||||
export const enum BadgePosition {
|
||||
export enum BadgePosition {
|
||||
START,
|
||||
END
|
||||
}
|
||||
@ -79,8 +79,8 @@ export function _getBadges(args: BadgeUserArgs) {
|
||||
: 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);
|
||||
const donorBadge = (Plugins.BadgeAPI as any).getDonorBadge(args.user.id);
|
||||
if (donorBadge) badges.unshift(donorBadge);
|
||||
|
||||
return badges;
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { makeCodeblock } from "@utils/text";
|
||||
import { makeCodeblock } from "@utils/misc";
|
||||
|
||||
import { sendBotMessage } from "./commandHelpers";
|
||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
|
||||
|
@ -24,7 +24,7 @@ export interface CommandContext {
|
||||
guild?: Guild;
|
||||
}
|
||||
|
||||
export const enum ApplicationCommandOptionType {
|
||||
export enum ApplicationCommandOptionType {
|
||||
SUB_COMMAND = 1,
|
||||
SUB_COMMAND_GROUP = 2,
|
||||
STRING = 3,
|
||||
@ -38,7 +38,7 @@ export const enum ApplicationCommandOptionType {
|
||||
ATTACHMENT = 11,
|
||||
}
|
||||
|
||||
export const enum ApplicationCommandInputType {
|
||||
export enum ApplicationCommandInputType {
|
||||
BUILT_IN = 0,
|
||||
BUILT_IN_TEXT = 1,
|
||||
BUILT_IN_INTEGRATION = 2,
|
||||
@ -64,7 +64,7 @@ export interface ChoicesOption {
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
export const enum ApplicationCommandType {
|
||||
export enum ApplicationCommandType {
|
||||
CHAT_INPUT = 1,
|
||||
USER = 2,
|
||||
MESSAGE = 3,
|
||||
|
@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import Logger from "@utils/Logger";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
type ContextMenuPatchCallbackReturn = (() => void) | void;
|
||||
@ -25,14 +25,14 @@ type ContextMenuPatchCallbackReturn = (() => void) | void;
|
||||
* @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;
|
||||
export type NavContextMenuPatchCallback = (children: Array<React.ReactElement>, ...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;
|
||||
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<React.ReactElement>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
|
||||
|
||||
const ContextMenuLogger = new Logger("ContextMenu");
|
||||
|
||||
@ -89,18 +89,15 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function for finding the children array of a group nested inside a context menu based on the id(s) of its children
|
||||
* @param id The id of the child. If an array is specified, all ids will be tried
|
||||
* A helper function for finding the children array of a group nested inside a context menu based on the id of one of its childs
|
||||
* @param id The id of the child
|
||||
* @param children The context menu children
|
||||
*/
|
||||
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null>, _itemsArray?: Array<ReactElement | null>): Array<ReactElement | null> | null {
|
||||
export function findGroupChildrenByChildId(id: string, children: Array<React.ReactElement>, _itemsArray?: Array<React.ReactElement>): Array<React.ReactElement> | 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;
|
||||
if (child.props?.id === id) return _itemsArray ?? null;
|
||||
|
||||
let nextChildren = child.props?.children;
|
||||
if (nextChildren) {
|
||||
@ -120,7 +117,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
|
||||
interface ContextMenuProps {
|
||||
contextMenuApiArguments?: Array<any>;
|
||||
navId: string;
|
||||
children: Array<ReactElement | null>;
|
||||
children: Array<ReactElement>;
|
||||
"aria-label": string;
|
||||
onSelect: (() => void) | undefined;
|
||||
onClose: (callback: (...args: Array<any>) => any) => void;
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* eslint-disable simple-header/header */
|
||||
/* eslint-disable header/header */
|
||||
|
||||
/*!
|
||||
* idb-keyval v6.2.0
|
||||
|
@ -16,61 +16,30 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import Logger from "@utils/Logger";
|
||||
import { MessageStore } from "@webpack/common";
|
||||
import { CustomEmoji } from "@webpack/types";
|
||||
import type { Channel, Message } from "discord-types/general";
|
||||
import type { Promisable } from "type-fest";
|
||||
|
||||
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 {
|
||||
content: string,
|
||||
validNonShortcutEmojis: CustomEmoji[];
|
||||
invalidEmojis: any[];
|
||||
tts: boolean;
|
||||
}
|
||||
|
||||
export interface Upload {
|
||||
classification: string;
|
||||
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;
|
||||
};
|
||||
validNonShortcutEmojis: Emoji[];
|
||||
}
|
||||
|
||||
export interface MessageExtra {
|
||||
stickers?: string[];
|
||||
uploads?: Upload[];
|
||||
replyOptions: MessageReplyOptions;
|
||||
content: string;
|
||||
channel: Channel;
|
||||
type?: any;
|
||||
openWarningPopout: (props: any) => any;
|
||||
stickerIds?: string[];
|
||||
}
|
||||
|
||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
|
||||
@ -79,8 +48,7 @@ export type EditListener = (channelId: string, messageId: string, messageObj: Me
|
||||
const sendListeners = new Set<SendListener>();
|
||||
const editListeners = new Set<EditListener>();
|
||||
|
||||
export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra, replyOptions: MessageReplyOptions) {
|
||||
extra.replyOptions = replyOptions;
|
||||
export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
|
||||
for (const listener of sendListeners) {
|
||||
try {
|
||||
const result = await listener(channelId, messageObj, extra);
|
||||
|
@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import Logger from "@utils/Logger";
|
||||
import { Channel, Message } from "discord-types/general";
|
||||
import type { MouseEventHandler } from "react";
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { useSettings } from "@api/Settings";
|
||||
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";
|
||||
|
@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { Settings } from "@api/settings";
|
||||
import { Queue } from "@utils/Queue";
|
||||
import { ReactDOM } from "@webpack/common";
|
||||
import type { ReactNode } from "react";
|
||||
|
@ -17,10 +17,10 @@
|
||||
*/
|
||||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { Settings } from "@api/settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { useAwaiter } from "@utils/misc";
|
||||
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";
|
||||
|
@ -16,11 +16,11 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import Logger from "@utils/Logger";
|
||||
|
||||
const logger = new Logger("ServerListAPI");
|
||||
|
||||
export const enum ServerListRenderPosition {
|
||||
export enum ServerListRenderPosition {
|
||||
Above,
|
||||
In,
|
||||
}
|
||||
|
@ -16,11 +16,11 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import Logger from "@utils/Logger";
|
||||
import { proxyLazy } from "@utils/proxyLazy";
|
||||
import { findModuleId, wreq } from "@webpack";
|
||||
|
||||
import { Settings } from "./Settings";
|
||||
import { Settings } from "./settings";
|
||||
|
||||
interface Setting<T> {
|
||||
/**
|
||||
|
@ -141,7 +141,7 @@ export const compileStyle = (style: Style) => {
|
||||
*/
|
||||
export const classNameToSelector = (name: string, prefix = "") => name.split(" ").map(n => `.${prefix}${n}`).join("");
|
||||
|
||||
type ClassNameFactoryArg = string | string[] | Record<string, unknown> | false | null | undefined | 0 | "";
|
||||
type ClassNameFactoryArg = string | string[] | Record<string, unknown>;
|
||||
/**
|
||||
* @param prefix The prefix to add to each class, defaults to `""`
|
||||
* @returns A classname generator function
|
||||
@ -154,9 +154,9 @@ type ClassNameFactoryArg = string | string[] | Record<string, unknown> | false |
|
||||
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);
|
||||
if (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));
|
||||
else if (typeof arg === "object") Object.entries(arg).forEach(([name, value]) => value && classNames.add(name));
|
||||
}
|
||||
return Array.from(classNames, name => prefix + name).join(" ");
|
||||
};
|
||||
|
@ -28,7 +28,7 @@ import * as $MessagePopover from "./MessagePopover";
|
||||
import * as $Notices from "./Notices";
|
||||
import * as $Notifications from "./Notifications";
|
||||
import * as $ServerList from "./ServerList";
|
||||
import * as $Settings from "./Settings";
|
||||
import * as $Settings from "./settings";
|
||||
import * as $SettingsStore from "./SettingsStore";
|
||||
import * as $Styles from "./Styles";
|
||||
|
||||
@ -87,10 +87,6 @@ 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
|
||||
*/
|
||||
@ -109,3 +105,9 @@ export const Notifications = $Notifications;
|
||||
* An api allowing you to patch and add/remove items to/from context menus
|
||||
*/
|
||||
export const ContextMenu = $ContextMenu;
|
||||
|
||||
/**
|
||||
* Settings lol
|
||||
*/
|
||||
export const Settings = $Settings;
|
||||
export const settings = $Settings;
|
||||
|
@ -17,8 +17,9 @@
|
||||
*/
|
||||
|
||||
import { debounce } from "@utils/debounce";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { localStorage } from "@utils/localStorage";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import Logger from "@utils/Logger";
|
||||
import { mergeDefaults } from "@utils/misc";
|
||||
import { putCloudSettings } from "@utils/settingsSync";
|
||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||
@ -34,7 +35,6 @@ export interface Settings {
|
||||
useQuickCss: boolean;
|
||||
enableReactDevtools: boolean;
|
||||
themeLinks: string[];
|
||||
enabledThemes: string[];
|
||||
frameless: boolean;
|
||||
transparent: boolean;
|
||||
winCtrlQ: boolean;
|
||||
@ -69,7 +69,6 @@ const DefaultSettings: Settings = {
|
||||
autoUpdateNotification: true,
|
||||
useQuickCss: true,
|
||||
themeLinks: [],
|
||||
enabledThemes: [],
|
||||
enableReactDevtools: false,
|
||||
frameless: false,
|
||||
transparent: false,
|
||||
@ -95,7 +94,7 @@ const DefaultSettings: Settings = {
|
||||
};
|
||||
|
||||
try {
|
||||
var settings = JSON.parse(VencordNative.settings.get()) as Settings;
|
||||
var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings;
|
||||
mergeDefaults(settings, DefaultSettings);
|
||||
} catch (err) {
|
||||
var settings = mergeDefaults({} as Settings, DefaultSettings);
|
||||
@ -109,7 +108,7 @@ const saveSettingsOnFrequentAction = debounce(async () => {
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
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 proxyCache = {} as Record<string, any>;
|
||||
@ -166,7 +165,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
||||
const setPath = `${path}${path && "."}${p}`;
|
||||
delete proxyCache[setPath];
|
||||
for (const subscription of subscriptions) {
|
||||
if (!subscription._paths || subscription._paths.includes(setPath)) {
|
||||
if (!subscription._path || subscription._path === setPath) {
|
||||
subscription(v, setPath);
|
||||
}
|
||||
}
|
||||
@ -174,7 +173,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
||||
PlainSettings.cloud.settingsSyncVersion = Date.now();
|
||||
localStorage.Vencord_settingsDirty = true;
|
||||
saveSettingsOnFrequentAction();
|
||||
VencordNative.settings.set(JSON.stringify(root, null, 4));
|
||||
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@ -237,7 +236,7 @@ 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 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) {
|
||||
((onUpdate as SubscriptionCallback)._paths ??= []).push(path);
|
||||
(onUpdate as SubscriptionCallback)._path = path;
|
||||
subscriptions.add(onUpdate);
|
||||
}
|
||||
|
||||
@ -250,18 +249,17 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
||||
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));
|
||||
VencordNative.ipc.invoke(
|
||||
IpcEvents.SET_SETTINGS,
|
||||
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> = {
|
||||
export function definePluginSettings<D extends SettingsDefinition, C extends SettingsChecks<D>>(def: D, checks?: C) {
|
||||
const definedSettings: DefinedSettings<D> = {
|
||||
get store() {
|
||||
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
|
||||
return Settings.plugins[definedSettings.pluginName] as any;
|
||||
@ -270,14 +268,9 @@ export function definePluginSettings<
|
||||
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
|
||||
).plugins[definedSettings.pluginName] as any,
|
||||
def,
|
||||
checks: checks ?? {} as any,
|
||||
checks: checks ?? {},
|
||||
pluginName: "",
|
||||
|
||||
withPrivateSettings<T extends object>() {
|
||||
return this as DefinedSettings<Def, Checks, T>;
|
||||
}
|
||||
};
|
||||
|
||||
return definedSettings;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { Button } from "@webpack/common";
|
||||
|
||||
import { Heart } from "./Heart";
|
||||
@ -26,7 +27,9 @@ export default function DonateButton(props: any) {
|
||||
{...props}
|
||||
look={Button.Looks.LINK}
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/Vendicated")}
|
||||
onClick={() =>
|
||||
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/sponsors/Vendicated")
|
||||
}
|
||||
>
|
||||
<Heart />
|
||||
Donate
|
||||
|
@ -16,9 +16,9 @@
|
||||
* 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 { LazyComponent } from "@utils/react";
|
||||
import { LazyComponent } from "@utils/misc";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
import { ErrorCard } from "./ErrorCard";
|
||||
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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 >
|
||||
);
|
||||
}
|
51
src/components/Monaco.ts
Normal file
51
src/components/Monaco.ts
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { debounce } from "@utils/debounce";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { Queue } from "@utils/Queue";
|
||||
import { find } from "@webpack";
|
||||
|
||||
import monacoHtml from "~fileContent/monacoWin.html";
|
||||
|
||||
const queue = new Queue();
|
||||
const setCss = debounce((css: string) => {
|
||||
queue.push(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css));
|
||||
});
|
||||
|
||||
export async function launchMonacoEditor() {
|
||||
const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
|
||||
const win = open("about:blank", "VencordQuickCss", features);
|
||||
if (!win) {
|
||||
alert("Failed to open QuickCSS popup. Make sure to allow popups!");
|
||||
return;
|
||||
}
|
||||
|
||||
win.setCss = setCss;
|
||||
win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
|
||||
win.getTheme = () =>
|
||||
find(m =>
|
||||
m.ProtoClass?.typeName.endsWith("PreloadedUserSettings")
|
||||
)?.getCurrentValue()?.appearance?.theme === 2
|
||||
? "vs-light"
|
||||
: "vs-dark";
|
||||
|
||||
win.document.write(monacoHtml);
|
||||
|
||||
window.__VENCORD_MONACO_WIN__ = new WeakRef(win);
|
||||
}
|
@ -16,16 +16,16 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { makeCodeblock } from "@utils/misc";
|
||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||
import { makeCodeblock } from "@utils/text";
|
||||
import { ReplaceFn } from "@utils/types";
|
||||
import { search } from "@webpack";
|
||||
import { Button, Clipboard, Forms, Parser, React, Switch, TextInput } from "@webpack/common";
|
||||
import { Button, Clipboard, Forms, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
import { CheckedTextInput } from "./CheckedTextInput";
|
||||
import ErrorBoundary from "./ErrorBoundary";
|
||||
|
||||
// Do not include diff in non dev builds (side effects import)
|
||||
if (IS_DEV) {
|
||||
@ -258,7 +258,8 @@ function PatchHelper() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsTab title="Patch Helper">
|
||||
<Forms.FormSection>
|
||||
<Text variant="heading-md/normal" tag="h2" className={Margins.bottom8}>Patch Helper</Text>
|
||||
<Forms.FormTitle>find</Forms.FormTitle>
|
||||
<TextInput
|
||||
type="text"
|
||||
@ -303,8 +304,8 @@ function PatchHelper() {
|
||||
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
||||
</>
|
||||
)}
|
||||
</SettingsTab>
|
||||
</Forms.FormSection>
|
||||
);
|
||||
}
|
||||
|
||||
export default IS_DEV ? wrapTab(PatchHelper, "PatchHelper") : null;
|
||||
export default IS_DEV ? ErrorBoundary.wrap(PatchHelper) : null;
|
@ -17,15 +17,13 @@
|
||||
*/
|
||||
|
||||
import { generateId } from "@api/Commands";
|
||||
import { useSettings } from "@api/Settings";
|
||||
import { disableStyle, enableStyle } from "@api/Styles";
|
||||
import { useSettings } from "@api/settings";
|
||||
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 { classes, LazyComponent } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { proxyLazy } from "@utils/proxyLazy";
|
||||
import { OptionType, Plugin } from "@utils/types";
|
||||
import { findByCode, findByPropsLazy } from "@webpack";
|
||||
import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
||||
@ -41,7 +39,6 @@ import {
|
||||
SettingSliderComponent,
|
||||
SettingTextComponent
|
||||
} from "./components";
|
||||
import hideBotTagStyle from "./userPopoutHideBotTag.css?managed";
|
||||
|
||||
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
||||
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
||||
@ -52,12 +49,11 @@ interface PluginModalProps extends ModalProps {
|
||||
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({
|
||||
username: user.username,
|
||||
id: user.id ?? generateId(),
|
||||
avatar: user.avatar,
|
||||
/** To stop discord making unwanted requests... */
|
||||
username: user.name,
|
||||
id: generateId(),
|
||||
bot: true,
|
||||
});
|
||||
FluxDispatcher.dispatch({
|
||||
@ -92,27 +88,14 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||
const hasSettings = Boolean(pluginSettings && plugin.options);
|
||||
|
||||
React.useEffect(() => {
|
||||
enableStyle(hideBotTagStyle);
|
||||
|
||||
let originalUser: User;
|
||||
(async () => {
|
||||
for (const user of plugin.authors.slice(0, 6)) {
|
||||
const author = user.id
|
||||
? await UserUtils.fetchUser(`${user.id}`)
|
||||
// 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 });
|
||||
|
||||
? await UserUtils.fetchUser(`${user.id}`).catch(() => makeDummyUser(user))
|
||||
: makeDummyUser(user);
|
||||
setAuthors(a => [...a, author]);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
disableStyle(hideBotTagStyle);
|
||||
if (originalUser)
|
||||
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: originalUser });
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function saveAndClose() {
|
||||
@ -145,8 +128,6 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||
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 }));
|
||||
}
|
||||
@ -169,7 +150,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||
);
|
||||
});
|
||||
|
||||
return <Flex flexDirection="column" style={{ gap: 12, marginBottom: 16 }}>{options}</Flex>;
|
||||
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,7 +180,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<ModalContent style={{ marginBottom: 8, marginTop: 8 }}>
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
|
||||
<Forms.FormText>{plugin.description}</Forms.FormText>
|
||||
@ -226,7 +207,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||
</Forms.FormSection>
|
||||
</div>
|
||||
)}
|
||||
<Forms.FormSection className={Margins.bottom16}>
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">Settings</Forms.FormTitle>
|
||||
{renderSettings()}
|
||||
</Forms.FormSection>
|
||||
|
@ -16,9 +16,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
||||
import { PluginOptionBoolean } from "@utils/types";
|
||||
import { Forms, React, Switch } from "@webpack/common";
|
||||
import { Forms, React, Select } from "@webpack/common";
|
||||
|
||||
import { ISettingElementProps } from ".";
|
||||
|
||||
@ -32,6 +31,11 @@ export function SettingBooleanComponent({ option, pluginSettings, definedSetting
|
||||
onError(error !== null);
|
||||
}, [error]);
|
||||
|
||||
const options = [
|
||||
{ label: "Enabled", value: true, default: def === true },
|
||||
{ label: "Disabled", value: false, default: typeof def === "undefined" || def === false },
|
||||
];
|
||||
|
||||
function handleChange(newValue: boolean): void {
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
if (typeof isValid === "string") setError(isValid);
|
||||
@ -45,17 +49,18 @@ export function SettingBooleanComponent({ option, pluginSettings, definedSetting
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Switch
|
||||
value={state}
|
||||
onChange={handleChange}
|
||||
note={option.description}
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<Select
|
||||
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
||||
options={options}
|
||||
placeholder={option.placeholder ?? "Select an option"}
|
||||
maxVisibleItems={5}
|
||||
closeOnSelect={true}
|
||||
select={handleChange}
|
||||
isSelected={v => v === state}
|
||||
serialize={v => String(v)}
|
||||
{...option.componentProps}
|
||||
hideBorder
|
||||
style={{ marginBottom: "0.5em" }}
|
||||
>
|
||||
{wordsToTitle(wordsFromCamel(id))}
|
||||
</Switch>
|
||||
/>
|
||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||
</Forms.FormSection>
|
||||
);
|
||||
|
@ -20,17 +20,19 @@ import "./styles.css";
|
||||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { showNotice } from "@api/Notices";
|
||||
import { Settings, useSettings } from "@api/Settings";
|
||||
import { useSettings } from "@api/settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { Badge } from "@components/PluginSettings/components";
|
||||
import PluginModal from "@components/PluginSettings/PluginModal";
|
||||
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
||||
import { SettingsTab } from "@components/VencordSettings/shared";
|
||||
import { Switch } from "@components/Switch";
|
||||
import { ChangeList } from "@utils/ChangeList";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import Logger from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { classes, LazyComponent, useAwaiter } 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";
|
||||
@ -92,7 +94,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
||||
}
|
||||
|
||||
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
||||
const settings = Settings.plugins[plugin.name];
|
||||
const settings = useSettings([`plugins.${plugin.name}.enabled`]).plugins[plugin.name];
|
||||
|
||||
const isEnabled = () => settings.enabled ?? false;
|
||||
|
||||
@ -123,7 +125,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
||||
}
|
||||
|
||||
// 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;
|
||||
onRestartNeeded(plugin.name);
|
||||
return;
|
||||
@ -136,13 +138,11 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
||||
}
|
||||
|
||||
const result = wasEnabled ? stopPlugin(plugin) : startPlugin(plugin);
|
||||
const action = wasEnabled ? "stop" : "start";
|
||||
|
||||
if (!result) {
|
||||
settings.enabled = false;
|
||||
|
||||
const msg = `Error while ${wasEnabled ? "stopping" : "starting"} plugin ${plugin.name}`;
|
||||
logger.error(msg);
|
||||
showErrorToast(msg);
|
||||
logger.error(`Failed to ${action} plugin ${plugin.name}`);
|
||||
showErrorToast(`Failed to ${action} plugin: ${plugin.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -150,34 +150,34 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
||||
}
|
||||
|
||||
return (
|
||||
<AddonCard
|
||||
name={plugin.name}
|
||||
description={plugin.description}
|
||||
isNew={isNew}
|
||||
enabled={isEnabled()}
|
||||
setEnabled={toggleEnabled}
|
||||
disabled={disabled}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
infoButton={
|
||||
<Flex className={cl("card", { "card-disabled": disabled })} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<div className={cl("card-header")}>
|
||||
<Text variant="text-md/bold" className={cl("name")}>
|
||||
{plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||
</Text>
|
||||
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
|
||||
{plugin.options
|
||||
? <CogWheel />
|
||||
: <InfoIcon width="24" height="24" />}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<Switch
|
||||
checked={isEnabled()}
|
||||
onChange={toggleEnabled}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<Text className={cl("note")} variant="text-sm/normal">{plugin.description}</Text>
|
||||
</Flex >
|
||||
);
|
||||
}
|
||||
|
||||
const enum SearchStatus {
|
||||
enum SearchStatus {
|
||||
ALL,
|
||||
ENABLED,
|
||||
DISABLED,
|
||||
NEW
|
||||
DISABLED
|
||||
}
|
||||
|
||||
export default function PluginSettings() {
|
||||
export default ErrorBoundary.wrap(function PluginSettings() {
|
||||
const settings = useSettings();
|
||||
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
||||
|
||||
@ -227,14 +227,10 @@ export default function PluginSettings() {
|
||||
const enabled = settings.plugins[plugin.name]?.enabled;
|
||||
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
|
||||
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
|
||||
if (searchValue.status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false;
|
||||
if (!searchValue.value.length) return true;
|
||||
|
||||
const v = searchValue.value.toLowerCase();
|
||||
return (
|
||||
plugin.name.toLowerCase().includes(v) ||
|
||||
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())
|
||||
);
|
||||
};
|
||||
|
||||
@ -262,9 +258,6 @@ export default function PluginSettings() {
|
||||
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);
|
||||
@ -305,7 +298,7 @@ export default function PluginSettings() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsTab title="Plugins">
|
||||
<Forms.FormSection className={Margins.top16}>
|
||||
<ReloadRequiredCard required={changes.hasChanges} />
|
||||
|
||||
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
||||
@ -320,8 +313,7 @@ export default function PluginSettings() {
|
||||
options={[
|
||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
||||
{ label: "Show New", value: SearchStatus.NEW }
|
||||
{ label: "Show Disabled", value: SearchStatus.DISABLED }
|
||||
]}
|
||||
serialize={String}
|
||||
select={onStatusChange}
|
||||
@ -345,9 +337,12 @@ export default function PluginSettings() {
|
||||
<div className={cl("grid")}>
|
||||
{requiredPlugins}
|
||||
</div>
|
||||
</SettingsTab >
|
||||
</Forms.FormSection >
|
||||
);
|
||||
}
|
||||
}, {
|
||||
message: "Failed to render the Plugin Settings. If this persists, try using the installer to reinstall!",
|
||||
onError: handleComponentFailed,
|
||||
});
|
||||
|
||||
function makeDependencyList(deps: string[]) {
|
||||
return (
|
||||
|
@ -23,6 +23,38 @@
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.vc-plugins-card {
|
||||
background-color: var(--background-secondary-alt);
|
||||
color: var(--interactive-active);
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
width: 100%;
|
||||
transition: 0.1s ease-out;
|
||||
transition-property: box-shadow, transform, background, opacity;
|
||||
}
|
||||
|
||||
.vc-plugins-card-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.vc-plugins-card:hover {
|
||||
background-color: var(--background-tertiary);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--elevation-high);
|
||||
}
|
||||
|
||||
.vc-plugins-card-header {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
height: 1.5rem;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vc-plugins-info-button {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
@ -54,6 +86,27 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vc-plugins-note {
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
/* stylelint-disable-next-line property-no-unknown */
|
||||
box-orient: vertical;
|
||||
}
|
||||
|
||||
.vc-plugins-name {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
gap: 8px;
|
||||
cursor: "default";
|
||||
}
|
||||
|
||||
.vc-plugins-dep-name {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
[class|="userPopoutOuter"] [class*="botTag"] {
|
||||
display: none;
|
||||
}
|
@ -1,77 +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 "./addonCard.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Badge } from "@components/Badge";
|
||||
import { Switch } from "@components/Switch";
|
||||
import { Text } from "@webpack/common";
|
||||
import type { MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
const cl = classNameFactory("vc-addon-");
|
||||
|
||||
interface Props {
|
||||
name: ReactNode;
|
||||
description: ReactNode;
|
||||
enabled: boolean;
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
disabled?: boolean;
|
||||
isNew?: boolean;
|
||||
onMouseEnter?: MouseEventHandler<HTMLDivElement>;
|
||||
onMouseLeave?: MouseEventHandler<HTMLDivElement>;
|
||||
|
||||
infoButton?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
author?: ReactNode;
|
||||
}
|
||||
|
||||
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={cl("card", { "card-disabled": disabled })}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<div className={cl("header")}>
|
||||
<div className={cl("name-author")}>
|
||||
<Text variant="text-md/bold" className={cl("name")}>
|
||||
{name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||
</Text>
|
||||
{!!author && (
|
||||
<Text variant="text-md/normal" className={cl("author")}>
|
||||
{author}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{infoButton}
|
||||
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={setEnabled}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Text className={cl("note")} variant="text-sm/normal">{description}</Text>
|
||||
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -16,17 +16,16 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
||||
import { Button, Card, Text } from "@webpack/common";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
import { Button, Card, Forms, Text } from "@webpack/common";
|
||||
|
||||
function BackupRestoreTab() {
|
||||
return (
|
||||
<SettingsTab title="Backup & Restore">
|
||||
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
|
||||
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
||||
<Flex flexDirection="column">
|
||||
<strong>Warning</strong>
|
||||
@ -60,8 +59,8 @@ function BackupRestoreTab() {
|
||||
Export Settings
|
||||
</Button>
|
||||
</Flex>
|
||||
</SettingsTab>
|
||||
</Forms.FormSection>
|
||||
);
|
||||
}
|
||||
|
||||
export default wrapTab(BackupRestoreTab, "Backup & Restore");
|
||||
export default ErrorBoundary.wrap(BackupRestoreTab);
|
@ -17,16 +17,15 @@
|
||||
*/
|
||||
|
||||
import { showNotification } from "@api/Notifications";
|
||||
import { Settings, useSettings } from "@api/Settings";
|
||||
import { Settings, useSettings } from "@api/settings";
|
||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Link } from "@components/Link";
|
||||
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
|
||||
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
function validateUrl(url: string) {
|
||||
try {
|
||||
new URL(url);
|
||||
@ -86,7 +85,7 @@ function SettingsSyncSection() {
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={!sectionEnabled}
|
||||
onClick={() => putCloudSettings(true)}
|
||||
onClick={() => putCloudSettings()}
|
||||
>Sync to Cloud</Button>
|
||||
<Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!">
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
@ -115,7 +114,7 @@ function CloudTab() {
|
||||
const settings = useSettings(["cloud.authenticated", "cloud.url"]);
|
||||
|
||||
return (
|
||||
<SettingsTab title="Vencord Cloud">
|
||||
<>
|
||||
<Forms.FormSection title="Cloud Settings" className={Margins.top16}>
|
||||
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
|
||||
Vencord comes with a cloud integration that adds goodies like settings sync across devices.
|
||||
@ -158,8 +157,8 @@ function CloudTab() {
|
||||
<Forms.FormDivider className={Margins.top16} />
|
||||
</Forms.FormSection >
|
||||
<SettingsSyncSection />
|
||||
</SettingsTab>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default wrapTab(CloudTab, "Cloud");
|
||||
export default ErrorBoundary.wrap(CloudTab);
|
||||
|
@ -16,8 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import PluginSettings from "@components/PluginSettings";
|
||||
|
||||
import { wrapTab } from "./shared";
|
||||
|
||||
export default wrapTab(PluginSettings, "Plugins");
|
||||
export default ErrorBoundary.wrap(PluginSettings);
|
||||
|
@ -16,36 +16,16 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { useSettings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Link } from "@components/Link";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { showItemInFolder } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
|
||||
import { Button, Card, FluxDispatcher, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
||||
import { UserThemeHeader } from "main/themes";
|
||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||
import { useAwaiter } from "@utils/misc";
|
||||
import { findLazy } from "@webpack";
|
||||
import { Card, Forms, React, TextArea } from "@webpack/common";
|
||||
|
||||
import { AddonCard } from "./AddonCard";
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
type FileInput = ComponentType<{
|
||||
ref: Ref<HTMLInputElement>;
|
||||
onChange: (e: SyntheticEvent<HTMLInputElement>) => void;
|
||||
multiple?: boolean;
|
||||
filters?: { name?: string; extensions: string[]; }[];
|
||||
}>;
|
||||
|
||||
const InviteActions = findByPropsLazy("resolveInvite");
|
||||
const TrashIcon = findByCodeLazy("M5 6.99902V18.999C5 20.101 5.897 20.999");
|
||||
const FileInput: FileInput = findByCodeLazy("activateUploadDialogue=");
|
||||
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
||||
|
||||
const cl = classNameFactory("vc-settings-theme-");
|
||||
|
||||
function Validator({ link }: { link: string; }) {
|
||||
const [res, err, pending] = useAwaiter(() => fetch(link).then(res => {
|
||||
if (res.status > 300) throw `${res.status} ${res.statusText}`;
|
||||
@ -94,191 +74,10 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
||||
);
|
||||
}
|
||||
|
||||
interface ThemeCardProps {
|
||||
theme: UserThemeHeader;
|
||||
enabled: boolean;
|
||||
onChange: (enabled: boolean) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
export default ErrorBoundary.wrap(function () {
|
||||
const settings = useSettings();
|
||||
const [themeText, setThemeText] = React.useState(settings.themeLinks.join("\n"));
|
||||
|
||||
function ThemeCard({ theme, enabled, onChange, onDelete }: ThemeCardProps) {
|
||||
return (
|
||||
<AddonCard
|
||||
name={theme.name}
|
||||
description={theme.description}
|
||||
author={theme.author}
|
||||
enabled={enabled}
|
||||
setEnabled={onChange}
|
||||
infoButton={
|
||||
IS_WEB && (
|
||||
<div style={{ cursor: "pointer", color: "var(--status-danger" }} onClick={onDelete}>
|
||||
<TrashIcon />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
footer={
|
||||
<Flex flexDirection="row" style={{ gap: "0.2em" }}>
|
||||
{!!theme.website && <Link href={theme.website}>Website</Link>}
|
||||
{!!(theme.website && theme.invite) && " • "}
|
||||
{!!theme.invite && (
|
||||
<Link
|
||||
href={`https://discord.gg/${theme.invite}`}
|
||||
onClick={async e => {
|
||||
e.preventDefault();
|
||||
const { invite } = await InviteActions.resolveInvite(theme.invite, "Desktop Modal");
|
||||
if (!invite) return showToast("Invalid or expired invite");
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
type: "INVITE_MODAL_OPEN",
|
||||
invite,
|
||||
code: theme.invite,
|
||||
context: "APP"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Discord Server
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
enum ThemeTab {
|
||||
LOCAL,
|
||||
ONLINE
|
||||
}
|
||||
|
||||
function ThemesTab() {
|
||||
const settings = useSettings(["themeLinks", "enabledThemes"]);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
|
||||
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n"));
|
||||
const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null);
|
||||
const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir);
|
||||
|
||||
useEffect(() => {
|
||||
refreshLocalThemes();
|
||||
}, []);
|
||||
|
||||
async function refreshLocalThemes() {
|
||||
const themes = await VencordNative.themes.getThemesList();
|
||||
setUserThemes(themes);
|
||||
}
|
||||
|
||||
// When a local theme is enabled/disabled, update the settings
|
||||
function onLocalThemeChange(fileName: string, value: boolean) {
|
||||
if (value) {
|
||||
if (settings.enabledThemes.includes(fileName)) return;
|
||||
settings.enabledThemes = [...settings.enabledThemes, fileName];
|
||||
} else {
|
||||
settings.enabledThemes = settings.enabledThemes.filter(f => f !== fileName);
|
||||
}
|
||||
}
|
||||
|
||||
async function onFileUpload(e: SyntheticEvent<HTMLInputElement>) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!e.currentTarget?.files?.length) return;
|
||||
const { files } = e.currentTarget;
|
||||
|
||||
const uploads = Array.from(files, file => {
|
||||
const { name } = file;
|
||||
if (!name.endsWith(".css")) return;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
VencordNative.themes.uploadTheme(name, reader.result as string)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(uploads);
|
||||
refreshLocalThemes();
|
||||
}
|
||||
|
||||
function renderLocalThemes() {
|
||||
return (
|
||||
<>
|
||||
<Card className="vc-settings-card">
|
||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
||||
<div style={{ marginBottom: ".5em", display: "flex", flexDirection: "column" }}>
|
||||
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
||||
BetterDiscord Themes
|
||||
</Link>
|
||||
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
|
||||
</div>
|
||||
<Forms.FormText>If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.</Forms.FormText>
|
||||
</Card>
|
||||
|
||||
<Forms.FormSection title="Local Themes">
|
||||
<Card className="vc-settings-quick-actions-card">
|
||||
<>
|
||||
{IS_WEB ?
|
||||
(
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={themeDirPending}
|
||||
>
|
||||
Upload Theme
|
||||
<FileInput
|
||||
ref={fileInputRef}
|
||||
onChange={onFileUpload}
|
||||
multiple={true}
|
||||
filters={[{ extensions: ["*.css"] }]}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => showItemInFolder(themeDir!)}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={themeDirPending}
|
||||
>
|
||||
Open Themes Folder
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={refreshLocalThemes}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Load missing Themes
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => VencordNative.quickCss.openEditor()}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Edit QuickCSS
|
||||
</Button>
|
||||
</>
|
||||
</Card>
|
||||
|
||||
<div className={cl("grid")}>
|
||||
{userThemes?.map(theme => (
|
||||
<ThemeCard
|
||||
key={theme.fileName}
|
||||
enabled={settings.enabledThemes.includes(theme.fileName)}
|
||||
onChange={enabled => onLocalThemeChange(theme.fileName, enabled)}
|
||||
onDelete={async () => {
|
||||
onLocalThemeChange(theme.fileName, false);
|
||||
await VencordNative.themes.deleteTheme(theme.fileName);
|
||||
refreshLocalThemes();
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Forms.FormSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// When the user leaves the online theme textbox, update the settings
|
||||
function onBlur() {
|
||||
settings.themeLinks = [...new Set(
|
||||
themeText
|
||||
@ -289,58 +88,42 @@ function ThemesTab() {
|
||||
)];
|
||||
}
|
||||
|
||||
function renderOnlineThemes() {
|
||||
return (
|
||||
<>
|
||||
<Card className="vc-settings-card vc-text-selectable">
|
||||
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
|
||||
<Forms.FormText>One link per line</Forms.FormText>
|
||||
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
|
||||
</Card>
|
||||
|
||||
<Forms.FormSection title="Online Themes" tag="h5">
|
||||
<TextArea
|
||||
value={themeText}
|
||||
onChange={setThemeText}
|
||||
className={classes(TextAreaProps.textarea, "vc-settings-theme-links")}
|
||||
placeholder="Theme Links"
|
||||
spellCheck={false}
|
||||
onBlur={onBlur}
|
||||
rows={10}
|
||||
/>
|
||||
<Validators themeLinks={settings.themeLinks} />
|
||||
</Forms.FormSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsTab title="Themes">
|
||||
<TabBar
|
||||
type="top"
|
||||
look="brand"
|
||||
className="vc-settings-tab-bar"
|
||||
selectedItem={currentTab}
|
||||
onItemSelect={setCurrentTab}
|
||||
>
|
||||
<TabBar.Item
|
||||
className="vc-settings-tab-bar-item"
|
||||
id={ThemeTab.LOCAL}
|
||||
>
|
||||
Local Themes
|
||||
</TabBar.Item>
|
||||
<TabBar.Item
|
||||
className="vc-settings-tab-bar-item"
|
||||
id={ThemeTab.ONLINE}
|
||||
>
|
||||
Online Themes
|
||||
</TabBar.Item>
|
||||
</TabBar>
|
||||
|
||||
{currentTab === ThemeTab.LOCAL && renderLocalThemes()}
|
||||
{currentTab === ThemeTab.ONLINE && renderOnlineThemes()}
|
||||
</SettingsTab>
|
||||
<>
|
||||
<Card className="vc-settings-card vc-text-selectable">
|
||||
<Forms.FormTitle tag="h5">Paste links to .theme.css files here</Forms.FormTitle>
|
||||
<Forms.FormText>One link per line</Forms.FormText>
|
||||
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
|
||||
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
|
||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
||||
<div style={{ marginBottom: ".5em" }}>
|
||||
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
||||
BetterDiscord Themes
|
||||
</Link>
|
||||
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
|
||||
</div>
|
||||
<Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText>
|
||||
<Forms.FormText>In the GitHub repository of your theme, find X.theme.css, click on it, then click the "Raw" button</Forms.FormText>
|
||||
<Forms.FormText>
|
||||
If the theme has configuration that requires you to edit the file:
|
||||
<ul>
|
||||
<li>• Make a <Link href="https://github.com/signup">GitHub</Link> account</li>
|
||||
<li>• Click the fork button on the top right</li>
|
||||
<li>• Edit the file</li>
|
||||
<li>• Use the link to your own repository instead</li>
|
||||
</ul>
|
||||
</Forms.FormText>
|
||||
</Card>
|
||||
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
|
||||
<TextArea
|
||||
value={themeText}
|
||||
onChange={setThemeText}
|
||||
className={`${TextAreaProps.textarea} vc-settings-theme-links`}
|
||||
placeholder="Theme Links"
|
||||
spellCheck={false}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
<Validators themeLinks={settings.themeLinks} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default wrapTab(ThemesTab, "Themes");
|
||||
});
|
||||
|
@ -16,21 +16,20 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useSettings } from "@api/Settings";
|
||||
import { useSettings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { Link } from "@components/Link";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { classes, useAwaiter } from "@utils/misc";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
|
||||
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
||||
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
||||
return async () => {
|
||||
dispatcher(true);
|
||||
@ -126,6 +125,7 @@ function Updatable(props: CommonProps) {
|
||||
onClick={withDispatcher(setIsUpdating, async () => {
|
||||
if (await update()) {
|
||||
setUpdates([]);
|
||||
await rebuild();
|
||||
await new Promise<void>(r => {
|
||||
Alerts.show({
|
||||
title: "Update Success!",
|
||||
@ -198,7 +198,7 @@ function Updater() {
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsTab title="Vencord Updater">
|
||||
<Forms.FormSection className={Margins.top16}>
|
||||
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||
<Switch
|
||||
value={settings.notifyAboutUpdates}
|
||||
@ -245,8 +245,11 @@ function Updater() {
|
||||
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
||||
|
||||
{isNewer ? <Newer {...commonProps} /> : <Updatable {...commonProps} />}
|
||||
</SettingsTab>
|
||||
</Forms.FormSection >
|
||||
);
|
||||
}
|
||||
|
||||
export default IS_WEB ? null : wrapTab(Updater, "Updater");
|
||||
export default IS_WEB ? null : ErrorBoundary.wrap(Updater, {
|
||||
message: "Failed to render the Updater. If this persists, try using the installer to reinstall!",
|
||||
onError: handleComponentFailed,
|
||||
});
|
@ -16,19 +16,19 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
||||
import { Settings, useSettings } from "@api/Settings";
|
||||
import { Settings, useSettings } from "@api/settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import DonateButton from "@components/DonateButton";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity } from "@utils/misc";
|
||||
import { identity, useAwaiter } from "@utils/misc";
|
||||
import { relaunch, showItemInFolder } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
const cl = classNameFactory("vc-settings-");
|
||||
|
||||
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
|
||||
@ -39,7 +39,7 @@ type KeysOfType<Object, Type> = {
|
||||
}[keyof Object];
|
||||
|
||||
function VencordSettings() {
|
||||
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
|
||||
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
|
||||
fallbackValue: "Loading..."
|
||||
});
|
||||
const settings = useSettings();
|
||||
@ -97,39 +97,44 @@ function VencordSettings() {
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsTab title="Vencord Settings">
|
||||
<React.Fragment>
|
||||
<DonateCard image={donateImage} />
|
||||
<Forms.FormSection title="Quick Actions">
|
||||
<Card className={cl("quick-actions-card")}>
|
||||
<React.Fragment>
|
||||
{!IS_WEB && (
|
||||
{IS_WEB ? (
|
||||
<Button
|
||||
onClick={() => require("../Monaco").launchMonacoEditor()}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDir === "Loading..."}>
|
||||
Open QuickCSS File
|
||||
</Button>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
onClick={relaunch}
|
||||
size={Button.Sizes.SMALL}>
|
||||
Restart Client
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => VencordNative.quickCss.openEditor()}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDir === "Loading..."}>
|
||||
Open QuickCSS File
|
||||
</Button>
|
||||
{!IS_WEB && (
|
||||
<Button
|
||||
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_MONACO_EDITOR)}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDir === "Loading..."}>
|
||||
Open QuickCSS File
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => showItemInFolder(settingsDir)}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDirPending}>
|
||||
Open Settings Folder
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => VencordNative.native.openExternal("https://github.com/Vendicated/Vencord")}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDirPending}>
|
||||
Open in GitHub
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
<Button
|
||||
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/Vendicated/Vencord")}
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={settingsDirPending}>
|
||||
Open in GitHub
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Card>
|
||||
</Forms.FormSection>
|
||||
|
||||
@ -153,7 +158,7 @@ function VencordSettings() {
|
||||
|
||||
|
||||
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />}
|
||||
</SettingsTab>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@ -263,4 +268,4 @@ function DonateCard({ image }: DonateCardProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default wrapTab(VencordSettings, "Vencord Settings");
|
||||
export default ErrorBoundary.wrap(VencordSettings);
|
||||
|
@ -1,63 +0,0 @@
|
||||
.vc-addon-card {
|
||||
background-color: var(--background-secondary-alt);
|
||||
color: var(--interactive-active);
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
width: 100%;
|
||||
transition: 0.1s ease-out;
|
||||
transition-property: box-shadow, transform, background, opacity;
|
||||
}
|
||||
|
||||
.vc-addon-card-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.vc-addon-card:hover {
|
||||
background-color: var(--background-tertiary);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--elevation-high);
|
||||
}
|
||||
|
||||
.vc-addon-header {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.vc-addon-note {
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
/* stylelint-disable-next-line property-no-unknown */
|
||||
box-orient: vertical;
|
||||
}
|
||||
|
||||
.vc-addon-name-author {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vc-addon-name {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vc-addon-author {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.vc-addon-author::before {
|
||||
content: "by ";
|
||||
}
|
93
src/components/VencordSettings/index.tsx
Normal file
93
src/components/VencordSettings/index.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./settingsStyles.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { isMobile } from "@utils/misc";
|
||||
import { Forms, SettingsRouter, TabBar, Text } from "@webpack/common";
|
||||
|
||||
import BackupRestoreTab from "./BackupRestoreTab";
|
||||
import CloudTab from "./CloudTab";
|
||||
import PluginsTab from "./PluginsTab";
|
||||
import ThemesTab from "./ThemesTab";
|
||||
import Updater from "./Updater";
|
||||
import VencordSettings from "./VencordTab";
|
||||
|
||||
const cl = classNameFactory("vc-settings-");
|
||||
|
||||
interface SettingsProps {
|
||||
tab: string;
|
||||
}
|
||||
|
||||
interface SettingsTab {
|
||||
name: string;
|
||||
component?: React.ComponentType;
|
||||
}
|
||||
|
||||
const SettingsTabs: Record<string, SettingsTab> = {
|
||||
VencordSettings: { name: "Vencord", component: () => <VencordSettings /> },
|
||||
VencordPlugins: { name: "Plugins", component: () => <PluginsTab /> },
|
||||
VencordThemes: { name: "Themes", component: () => <ThemesTab /> },
|
||||
VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false
|
||||
VencordCloud: { name: "Cloud", component: () => <CloudTab /> },
|
||||
VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> }
|
||||
};
|
||||
|
||||
if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater />;
|
||||
|
||||
function Settings(props: SettingsProps) {
|
||||
const { tab = "VencordSettings" } = props;
|
||||
|
||||
const CurrentTab = SettingsTabs[tab]?.component ?? null;
|
||||
if (isMobile) {
|
||||
return CurrentTab && <CurrentTab />;
|
||||
}
|
||||
|
||||
return <Forms.FormSection>
|
||||
<Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">Vencord Settings</Text>
|
||||
|
||||
<TabBar
|
||||
type="top"
|
||||
look="brand"
|
||||
className={cl("tab-bar")}
|
||||
selectedItem={tab}
|
||||
onItemSelect={SettingsRouter.open}
|
||||
>
|
||||
{Object.entries(SettingsTabs).map(([key, { name, component }]) => {
|
||||
if (!component) return null;
|
||||
return <TabBar.Item
|
||||
id={key}
|
||||
className={cl("tab-bar-item")}
|
||||
key={key}>
|
||||
{name}
|
||||
</TabBar.Item>;
|
||||
})}
|
||||
</TabBar>
|
||||
<Forms.FormDivider />
|
||||
{CurrentTab && <CurrentTab />}
|
||||
</Forms.FormSection >;
|
||||
}
|
||||
|
||||
export default function (props: SettingsProps) {
|
||||
return <ErrorBoundary onError={handleComponentFailed}>
|
||||
<Settings tab={props.tab} />
|
||||
</ErrorBoundary>;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
.vc-settings-tab-bar {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: -2px;
|
||||
border-bottom: 2px solid var(--background-modifier-accent);
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
flex-flow: row wrap;
|
||||
margin-bottom: 1em;
|
||||
@ -29,12 +29,14 @@
|
||||
.vc-settings-card {
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.vc-backup-restore-card {
|
||||
background-color: var(--info-warning-background);
|
||||
border-color: var(--info-warning-foreground);
|
||||
color: var(--info-warning-text);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.vc-settings-theme-links {
|
||||
@ -43,7 +45,6 @@
|
||||
color: var(--text-normal) !important;
|
||||
padding: 0.5em;
|
||||
border: 1px solid var(--background-modifier-accent);
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
.vc-cloud-settings-sync-grid {
|
||||
@ -58,7 +59,7 @@
|
||||
}
|
||||
|
||||
.vc-text-selectable,
|
||||
.vc-text-selectable :where([class*="text" i], [class*="title" i]) {
|
||||
.vc-text-selectable :not(a, button) {
|
||||
/* make text selectable, silly discord makes the entirety of settings not selectable */
|
||||
user-select: text;
|
||||
|
||||
|
@ -1,52 +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 "./settingsStyles.css";
|
||||
import "./themesStyles.css";
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { onlyOnce } from "@utils/onlyOnce";
|
||||
import { Forms, Text } from "@webpack/common";
|
||||
import type { ComponentType, PropsWithChildren } from "react";
|
||||
|
||||
export function SettingsTab({ title, children }: PropsWithChildren<{ title: string; }>) {
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Text
|
||||
variant="heading-lg/semibold"
|
||||
tag="h2"
|
||||
className={Margins.bottom16}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{children}
|
||||
</Forms.FormSection>
|
||||
);
|
||||
}
|
||||
|
||||
const onError = onlyOnce(handleComponentFailed);
|
||||
|
||||
export function wrapTab(component: ComponentType, tab: string) {
|
||||
return ErrorBoundary.wrap(component, {
|
||||
message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`,
|
||||
onError,
|
||||
});
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
.vc-settings-theme-grid {
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.vc-settings-theme-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--background-secondary-alt);
|
||||
color: var(--interactive-active);
|
||||
border-radius: 8px;
|
||||
padding: 1em;
|
||||
width: 100%;
|
||||
transition: 0.1s ease-out;
|
||||
transition-property: box-shadow, transform, background, opacity;
|
||||
}
|
||||
|
||||
.vc-settings-theme-card-text {
|
||||
text-overflow: ellipsis;
|
||||
height: 1.2em;
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vc-settings-theme-author::before {
|
||||
content: "by ";
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
.vc-open-external-icon {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.vc-owner-crown-icon {
|
||||
color: var(--text-warning);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* 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
|
||||
@ -16,14 +16,13 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export function onlyOnce<F extends Function>(f: F): F {
|
||||
let called = false;
|
||||
let result: any;
|
||||
return function onlyOnceWrapper(this: unknown) {
|
||||
if (called) return result;
|
||||
export { default as ErrorBoundary } from "./ErrorBoundary";
|
||||
export * from "./ErrorCard";
|
||||
export * from "./Flex";
|
||||
export * from "./Heart";
|
||||
export * from "./Link";
|
||||
export { default as PatchHelper } from "./PatchHelper";
|
||||
export { default as PluginSettings } from "./PluginSettings";
|
||||
export * from "./Switch";
|
||||
export { default as VencordSettings } from "./VencordSettings";
|
||||
|
||||
called = true;
|
||||
|
||||
return (result = f.apply(this, arguments));
|
||||
} as unknown as F;
|
||||
}
|
@ -1,66 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Vencord QuickCSS Editor</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.40.0/min/vs/editor/editor.main.min.css"
|
||||
integrity="sha512-MOoQ02h80hklccfLrXFYkCzG+WVjORflOp9Zp8dltiaRP+35LYnO4LKOklR64oMGfGgJDLO8WJpkM1o5gZXYZQ=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.40.0/min/vs/loader.min.js"
|
||||
integrity="sha512-QzMpXeCPciAHP4wbYlV2PYgrQcaEkDQUjzkPU4xnjyVSD9T36/udamxtNBqb4qK4/bMQMPZ8ayrBe9hrGdBFjQ=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
></script>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Vencord QuickCSS Editor</title>
|
||||
<link rel="stylesheet" data-name="vs/editor/editor.main"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.min.css">
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<script>
|
||||
require.config({
|
||||
paths: {
|
||||
vs: "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.40.0/min/vs",
|
||||
},
|
||||
});
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/loader.min.js"></script>
|
||||
|
||||
require(["vs/editor/editor.main"], () => {
|
||||
getCurrentCss().then((css) => {
|
||||
var editor = monaco.editor.create(
|
||||
document.getElementById("container"),
|
||||
{
|
||||
value: css,
|
||||
language: "css",
|
||||
theme: getTheme(),
|
||||
}
|
||||
);
|
||||
editor.onDidChangeModelContent(() =>
|
||||
setCss(editor.getValue())
|
||||
);
|
||||
window.addEventListener("resize", () => {
|
||||
// make monaco re-layout
|
||||
editor.layout();
|
||||
});
|
||||
<script>
|
||||
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs' } });
|
||||
require(["vs/editor/editor.main"], () => {
|
||||
getCurrentCss().then(css => {
|
||||
var editor = monaco.editor.create(document.getElementById('container'), {
|
||||
value: css,
|
||||
language: 'css',
|
||||
theme: getTheme(),
|
||||
});
|
||||
editor.onDidChangeModelContent(() =>
|
||||
setCss(editor.getValue())
|
||||
);
|
||||
window.addEventListener("resize", () => {
|
||||
// make monaco re-layout
|
||||
editor.layout();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import Logger from "@utils/Logger";
|
||||
|
||||
if (IS_DEV) {
|
||||
var traces = {} as Record<string, [number, any[]]>;
|
||||
|
2
src/globals.d.ts
vendored
2
src/globals.d.ts
vendored
@ -37,8 +37,6 @@ declare global {
|
||||
export var IS_STANDALONE: boolean;
|
||||
export var IS_DISCORD_DESKTOP: boolean;
|
||||
export var IS_VENCORD_DESKTOP: boolean;
|
||||
export var VERSION: string;
|
||||
export var BUILD_TIMESTAMP: number;
|
||||
|
||||
export var VencordNative: typeof import("./VencordNative").default;
|
||||
export var Vencord: typeof import("./Vencord");
|
||||
|
@ -19,8 +19,8 @@
|
||||
import { app, protocol, session } from "electron";
|
||||
import { join } from "path";
|
||||
|
||||
import { ensureSafePath, getSettings } from "./ipcMain";
|
||||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||
import { getSettings } from "./ipcMain";
|
||||
import { IS_VANILLA } from "./utils/constants";
|
||||
import { installExt } from "./utils/extensions";
|
||||
|
||||
if (IS_VENCORD_DESKTOP || !IS_VANILLA) {
|
||||
@ -30,21 +30,10 @@ if (IS_VENCORD_DESKTOP || !IS_VANILLA) {
|
||||
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
||||
let url = unsafeUrl.slice("vencord://".length);
|
||||
if (url.endsWith("/")) url = url.slice(0, -1);
|
||||
if (url.startsWith("/themes/")) {
|
||||
const theme = url.slice("/themes/".length);
|
||||
const safeUrl = ensureSafePath(THEMES_DIR, theme);
|
||||
if (!safeUrl) {
|
||||
cb({ statusCode: 403 });
|
||||
return;
|
||||
}
|
||||
cb(safeUrl.replace(/\?v=\d+$/, ""));
|
||||
return;
|
||||
}
|
||||
switch (url) {
|
||||
case "renderer.js.map":
|
||||
case "vencordDesktopRenderer.js.map":
|
||||
case "preload.js.map":
|
||||
case "vencordDesktopPreload.js.map":
|
||||
case "patcher.js.map":
|
||||
case "vencordDesktopMain.js.map":
|
||||
cb(join(__dirname, url));
|
||||
@ -86,7 +75,7 @@ if (IS_VENCORD_DESKTOP || !IS_VANILLA) {
|
||||
const csp = parsePolicy(headers[header][0]);
|
||||
|
||||
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
|
||||
csp[directive] = ["*", "blob:", "data:", "vencord:", "'unsafe-inline'"];
|
||||
csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
|
||||
}
|
||||
// TODO: Restrict this to only imported packages with fixed version.
|
||||
// Perhaps auto generate with esbuild
|
||||
|
@ -17,58 +17,25 @@
|
||||
*/
|
||||
|
||||
import "./updater";
|
||||
import "./ipcPlugins";
|
||||
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { IpcEvents } from "@utils/IpcEvents";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { Queue } from "@utils/Queue";
|
||||
import { BrowserWindow, ipcMain, shell } from "electron";
|
||||
import { mkdirSync, readFileSync, watch } from "fs";
|
||||
import { open, readdir, readFile, writeFile } from "fs/promises";
|
||||
import { join, normalize } from "path";
|
||||
import { mkdirSync, readdirSync, readFileSync, watch } from "fs";
|
||||
import { open, readFile, writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
|
||||
|
||||
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants";
|
||||
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
||||
import { ALLOWED_PROTOCOLS, PLUGINS_DIR, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants";
|
||||
|
||||
mkdirSync(SETTINGS_DIR, { recursive: true });
|
||||
mkdirSync(THEMES_DIR, { recursive: true });
|
||||
|
||||
export function ensureSafePath(basePath: string, path: string) {
|
||||
const normalizedBasePath = normalize(basePath);
|
||||
const newPath = join(basePath, path);
|
||||
const normalizedPath = normalize(newPath);
|
||||
return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null;
|
||||
}
|
||||
|
||||
function readCss() {
|
||||
return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
|
||||
}
|
||||
|
||||
async function listThemes(): Promise<UserThemeHeader[]> {
|
||||
const files = await readdir(THEMES_DIR).catch(() => []);
|
||||
|
||||
const themeInfo: UserThemeHeader[] = [];
|
||||
|
||||
for (const fileName of files) {
|
||||
const data = await getThemeData(fileName).then(stripBOM).catch(() => null);
|
||||
if (!data) continue;
|
||||
const parsed = getThemeInfo(data, fileName);
|
||||
themeInfo.push(parsed);
|
||||
}
|
||||
|
||||
return themeInfo;
|
||||
}
|
||||
|
||||
function getThemeData(fileName: string) {
|
||||
fileName = fileName.replace(/\?v=\d+$/, "");
|
||||
const safePath = ensureSafePath(THEMES_DIR, fileName);
|
||||
if (!safePath) return Promise.reject(`Unsafe path ${fileName}`);
|
||||
return readFile(safePath, "utf-8");
|
||||
}
|
||||
|
||||
export function readSettings() {
|
||||
try {
|
||||
return readFileSync(SETTINGS_FILE, "utf-8");
|
||||
@ -77,7 +44,7 @@ export function readSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
export function getSettings(): typeof import("@api/Settings").Settings {
|
||||
export function getSettings(): typeof import("@api/settings").Settings {
|
||||
try {
|
||||
return JSON.parse(readSettings());
|
||||
} catch {
|
||||
@ -107,10 +74,6 @@ ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
|
||||
cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css))
|
||||
);
|
||||
|
||||
ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR);
|
||||
ipcMain.handle(IpcEvents.GET_THEMES_LIST, () => listThemes());
|
||||
ipcMain.handle(IpcEvents.GET_THEME_DATA, (_, fileName) => getThemeData(fileName));
|
||||
|
||||
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
||||
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
|
||||
|
||||
@ -126,10 +89,6 @@ export function initIpc(mainWindow: BrowserWindow) {
|
||||
mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
|
||||
}, 50));
|
||||
});
|
||||
|
||||
watch(THEMES_DIR, { persistent: false }, debounce(() => {
|
||||
mainWindow.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0);
|
||||
}));
|
||||
}
|
||||
|
||||
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
||||
@ -138,14 +97,23 @@ ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
||||
autoHideMenuBar: true,
|
||||
darkTheme: true,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js"),
|
||||
preload: join(__dirname, "preload.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: false
|
||||
}
|
||||
});
|
||||
|
||||
makeLinksOpenExternally(win);
|
||||
|
||||
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
|
||||
});
|
||||
|
||||
ipcMain.on(IpcEvents.GET_PLUGINS, e => {
|
||||
try {
|
||||
const files = readdirSync(PLUGINS_DIR).filter(f => f.endsWith(".js"));
|
||||
console.log(files);
|
||||
|
||||
e.returnValue = files.map(f => [f, readFileSync(join(PLUGINS_DIR, f), "utf-8")]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
e.returnValue = [];
|
||||
}
|
||||
});
|
||||
|
@ -1,67 +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 { IpcEvents } from "@utils/IpcEvents";
|
||||
import { app, ipcMain } from "electron";
|
||||
import { readFile } from "fs/promises";
|
||||
import { request } from "https";
|
||||
import { basename, normalize } from "path";
|
||||
|
||||
// #region OpenInApp
|
||||
// These links don't support CORS, so this has to be native
|
||||
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
||||
|
||||
function getRedirect(url: string) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const req = request(new URL(url), { method: "HEAD" }, res => {
|
||||
resolve(
|
||||
res.headers.location
|
||||
? getRedirect(res.headers.location)
|
||||
: url
|
||||
);
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.handle(IpcEvents.OPEN_IN_APP__RESOLVE_REDIRECT, async (_, url: string) => {
|
||||
if (!validRedirectUrls.test(url)) return url;
|
||||
|
||||
return getRedirect(url);
|
||||
});
|
||||
// #endregion
|
||||
|
||||
|
||||
// #region VoiceMessages
|
||||
ipcMain.handle(IpcEvents.VOICE_MESSAGES_READ_RECORDING, async (_, filePath: string) => {
|
||||
filePath = normalize(filePath);
|
||||
const filename = basename(filePath);
|
||||
const discordBaseDirWithTrailingSlash = normalize(app.getPath("userData") + "/");
|
||||
console.log(filename, discordBaseDirWithTrailingSlash, filePath);
|
||||
if (filename !== "recording.ogg" || !filePath.startsWith(discordBaseDirWithTrailingSlash)) return null;
|
||||
|
||||
try {
|
||||
const buf = await readFile(filePath);
|
||||
return new Uint8Array(buf.buffer);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// #endregion
|
@ -71,7 +71,7 @@ if (!IS_VANILLA) {
|
||||
constructor(options: BrowserWindowConstructorOptions) {
|
||||
if (options?.webPreferences?.preload && options.title) {
|
||||
const original = options.webPreferences.preload;
|
||||
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
|
||||
options.webPreferences.preload = join(__dirname, "preload.js");
|
||||
options.webPreferences.sandbox = false;
|
||||
if (settings.frameless) {
|
||||
options.frame = false;
|
||||
|
@ -1,177 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
@ -1,81 +0,0 @@
|
||||
/* eslint-disable simple-header/header */
|
||||
|
||||
/*!
|
||||
* BetterDiscord addon meta parser
|
||||
* Copyright 2023 BetterDiscord contributors
|
||||
* Copyright 2023 Vendicated and Vencord contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const splitRegex = /[^\S\r\n]*?\r?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/;
|
||||
const escapedAtRegex = /^\\@/;
|
||||
|
||||
export interface UserThemeHeader {
|
||||
fileName: string;
|
||||
name: string;
|
||||
author: string;
|
||||
description: string;
|
||||
version?: string;
|
||||
license?: string;
|
||||
source?: string;
|
||||
website?: string;
|
||||
invite?: string;
|
||||
}
|
||||
|
||||
function makeHeader(fileName: string, opts: Partial<UserThemeHeader> = {}): UserThemeHeader {
|
||||
return {
|
||||
fileName,
|
||||
name: opts.name ?? fileName.replace(/\.css$/i, ""),
|
||||
author: opts.author ?? "Unknown Author",
|
||||
description: opts.description ?? "A Discord Theme.",
|
||||
version: opts.version,
|
||||
license: opts.license,
|
||||
source: opts.source,
|
||||
website: opts.website,
|
||||
invite: opts.invite
|
||||
};
|
||||
}
|
||||
|
||||
export function stripBOM(fileContent: string) {
|
||||
if (fileContent.charCodeAt(0) === 0xFEFF) {
|
||||
fileContent = fileContent.slice(1);
|
||||
}
|
||||
return fileContent;
|
||||
}
|
||||
|
||||
export function getThemeInfo(css: string, fileName: string): UserThemeHeader {
|
||||
if (!css) return makeHeader(fileName);
|
||||
|
||||
const block = css.split("/**", 2)?.[1]?.split("*/", 1)?.[0];
|
||||
if (!block) return makeHeader(fileName);
|
||||
|
||||
const header: Partial<UserThemeHeader> = {};
|
||||
let field = "";
|
||||
let accum = "";
|
||||
for (const line of block.split(splitRegex)) {
|
||||
if (line.length === 0) continue;
|
||||
if (line.charAt(0) === "@" && line.charAt(1) !== " ") {
|
||||
header[field] = accum.trim();
|
||||
const l = line.indexOf(" ");
|
||||
field = line.substring(1, l);
|
||||
accum = line.substring(l + 1);
|
||||
}
|
||||
else {
|
||||
accum += " " + line.replace("\\n", "\n").replace(escapedAtRegex, "@");
|
||||
}
|
||||
}
|
||||
header[field] = accum.trim();
|
||||
delete header[""];
|
||||
return makeHeader(fileName, header);
|
||||
}
|
@ -16,11 +16,12 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
export const VENCORD_FILES = [
|
||||
IS_DISCORD_DESKTOP ? "patcher.js" : "vencordDesktopMain.js",
|
||||
IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js",
|
||||
"preload.js",
|
||||
IS_DISCORD_DESKTOP ? "renderer.js" : "vencordDesktopRenderer.js",
|
||||
IS_DISCORD_DESKTOP ? "renderer.css" : "vencordDesktopRenderer.css",
|
||||
"renderer.css"
|
||||
];
|
||||
|
||||
export function serializeErrors(func: (...args: any[]) => any) {
|
||||
|
@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { IpcEvents } from "@utils/IpcEvents";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { execFile as cpExecFile } from "child_process";
|
||||
import { ipcMain } from "electron";
|
||||
import { join } from "path";
|
||||
|
@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
import { VENCORD_USER_AGENT } from "@utils/constants";
|
||||
import { IpcEvents } from "@utils/IpcEvents";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { ipcMain } from "electron";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
@ -25,15 +25,14 @@ export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR ?? (
|
||||
: join(app.getPath("userData"), "..", "Vencord")
|
||||
);
|
||||
export const SETTINGS_DIR = join(DATA_DIR, "settings");
|
||||
export const THEMES_DIR = join(DATA_DIR, "themes");
|
||||
export const PLUGINS_DIR = join(DATA_DIR, "plugins");
|
||||
export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
|
||||
export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
|
||||
export const ALLOWED_PROTOCOLS = [
|
||||
"https:",
|
||||
"http:",
|
||||
"steam:",
|
||||
"spotify:",
|
||||
"com.epicgames.launcher:",
|
||||
"spotify:"
|
||||
];
|
||||
|
||||
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* eslint-disable simple-header/header */
|
||||
/* eslint-disable header/header */
|
||||
|
||||
/*!
|
||||
* crxToZip
|
||||
|
@ -1,48 +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 { type BrowserWindow, shell } from "electron";
|
||||
|
||||
export function makeLinksOpenExternally(win: BrowserWindow) {
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
switch (url) {
|
||||
case "about:blank":
|
||||
case "https://discord.com/popout":
|
||||
case "https://ptb.discord.com/popout":
|
||||
case "https://canary.discord.com/popout":
|
||||
return { action: "allow" };
|
||||
}
|
||||
|
||||
try {
|
||||
var { protocol } = new URL(url);
|
||||
} catch {
|
||||
return { action: "deny" };
|
||||
}
|
||||
|
||||
switch (protocol) {
|
||||
case "http:":
|
||||
case "https:":
|
||||
case "mailto:":
|
||||
case "steam:":
|
||||
case "spotify:":
|
||||
shell.openExternal(url);
|
||||
}
|
||||
|
||||
return { action: "deny" };
|
||||
});
|
||||
}
|
2
src/modules.d.ts
vendored
2
src/modules.d.ts
vendored
@ -20,7 +20,7 @@
|
||||
/// <reference types="standalone-electron-types"/>
|
||||
|
||||
declare module "~plugins" {
|
||||
const plugins: Record<string, import("@utils/types").Plugin>;
|
||||
const plugins: Record<string, import("./utils/types").Plugin>;
|
||||
export default plugins;
|
||||
}
|
||||
|
||||
|
@ -1,193 +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 { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
|
||||
import DonateButton from "@components/DonateButton";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { Heart } from "@components/Heart";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { isPluginDev } from "@utils/misc";
|
||||
import { closeModal, Modals, openModal } from "@utils/modal";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Forms, Toasts } from "@webpack/common";
|
||||
|
||||
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/attachments/1033680203433660458/1092089947126780035/favicon.png";
|
||||
|
||||
const ContributorBadge: ProfileBadge = {
|
||||
description: "Vencord Contributor",
|
||||
image: CONTRIBUTOR_BADGE,
|
||||
position: BadgePosition.START,
|
||||
props: {
|
||||
style: {
|
||||
borderRadius: "50%",
|
||||
transform: "scale(0.9)" // The image is a bit too big compared to default badges
|
||||
}
|
||||
},
|
||||
shouldShow: ({ user }) => isPluginDev(user.id),
|
||||
link: "https://github.com/Vendicated/Vencord"
|
||||
};
|
||||
|
||||
let DonorBadges = {} as Record<string, Pick<ProfileBadge, "image" | "description">[]>;
|
||||
|
||||
async function loadBadges(noCache = false) {
|
||||
DonorBadges = {};
|
||||
|
||||
const init = {} as RequestInit;
|
||||
if (noCache)
|
||||
init.cache = "no-cache";
|
||||
|
||||
const badges = await fetch("https://gist.githubusercontent.com/Vendicated/51a3dd775f6920429ec6e9b735ca7f01/raw/badges.csv", init)
|
||||
.then(r => r.text());
|
||||
|
||||
const lines = badges.trim().split("\n");
|
||||
if (lines.shift() !== "id,tooltip,image") {
|
||||
new Logger("BadgeAPI").error("Invalid badges.csv file!");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
const [id, description, image] = line.split(",");
|
||||
(DonorBadges[id] ??= []).push({ image, description });
|
||||
}
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "BadgeAPI",
|
||||
description: "API to add badges to users.",
|
||||
authors: [Devs.Megu, Devs.Ven, Devs.TheSun],
|
||||
required: true,
|
||||
patches: [
|
||||
/* Patch the badge list component on user profiles */
|
||||
{
|
||||
find: "Messages.PROFILE_USER_BADGES,role:",
|
||||
replacement: [
|
||||
{
|
||||
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
|
||||
replace: "$&$1.unshift(...Vencord.Api.Badges._getBadges(arguments[0]));",
|
||||
},
|
||||
{
|
||||
// alt: "", aria-hidden: false, src: originalSrc
|
||||
match: /alt:" ","aria-hidden":!0,src:(?=(\i)\.src)/g,
|
||||
// ...badge.props, ..., src: badge.image ?? ...
|
||||
replace: "...$1.props,$& $1.image??"
|
||||
},
|
||||
{
|
||||
match: /children:function(?<=(\i)\.(?:tooltip|description),spacing:\d.+?)/g,
|
||||
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) : function"
|
||||
},
|
||||
{
|
||||
match: /onClick:function(?=.{0,200}href:(\i)\.link)/,
|
||||
replace: "onClick:$1.onClick??function"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
toolboxActions: {
|
||||
async "Refetch Badges"() {
|
||||
await loadBadges(true);
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: "Successfully refetched badges!",
|
||||
type: Toasts.Type.SUCCESS
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async start() {
|
||||
Vencord.Api.Badges.addBadge(ContributorBadge);
|
||||
await loadBadges();
|
||||
},
|
||||
|
||||
renderBadgeComponent: ErrorBoundary.wrap((badge: ProfileBadge & BadgeUserArgs) => {
|
||||
const Component = badge.component!;
|
||||
return <Component {...badge} />;
|
||||
}, { noop: true }),
|
||||
|
||||
|
||||
getDonorBadges(userId: string) {
|
||||
return DonorBadges[userId]?.map(badge => ({
|
||||
...badge,
|
||||
position: BadgePosition.START,
|
||||
props: {
|
||||
style: {
|
||||
borderRadius: "50%",
|
||||
transform: "scale(0.9)" // The image is a bit too big compared to default badges
|
||||
}
|
||||
},
|
||||
onClick() {
|
||||
const modalKey = openModal(props => (
|
||||
<ErrorBoundary noop onError={() => {
|
||||
closeModal(modalKey);
|
||||
VencordNative.native.openExternal("https://github.com/sponsors/Vendicated");
|
||||
}}>
|
||||
<Modals.ModalRoot {...props}>
|
||||
<Modals.ModalHeader>
|
||||
<Flex style={{ width: "100%", justifyContent: "center" }}>
|
||||
<Forms.FormTitle
|
||||
tag="h2"
|
||||
style={{
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
margin: 0
|
||||
}}
|
||||
>
|
||||
<Heart />
|
||||
Vencord Donor
|
||||
</Forms.FormTitle>
|
||||
</Flex>
|
||||
</Modals.ModalHeader>
|
||||
<Modals.ModalContent>
|
||||
<Flex>
|
||||
<img
|
||||
role="presentation"
|
||||
src="https://cdn.discordapp.com/emojis/1026533070955872337.png"
|
||||
alt=""
|
||||
style={{ margin: "auto" }}
|
||||
/>
|
||||
<img
|
||||
role="presentation"
|
||||
src="https://cdn.discordapp.com/emojis/1026533090627174460.png"
|
||||
alt=""
|
||||
style={{ margin: "auto" }}
|
||||
/>
|
||||
</Flex>
|
||||
<div style={{ padding: "1em" }}>
|
||||
<Forms.FormText>
|
||||
This Badge is a special perk for Vencord Donors
|
||||
</Forms.FormText>
|
||||
<Forms.FormText className={Margins.top20}>
|
||||
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
||||
</Forms.FormText>
|
||||
</div>
|
||||
</Modals.ModalContent>
|
||||
<Modals.ModalFooter>
|
||||
<Flex style={{ width: "100%", justifyContent: "center" }}>
|
||||
<DonateButton />
|
||||
</Flex>
|
||||
</Modals.ModalFooter>
|
||||
</Modals.ModalRoot>
|
||||
</ErrorBoundary>
|
||||
));
|
||||
},
|
||||
}));
|
||||
}
|
||||
});
|
@ -16,18 +16,16 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { Settings } from "@api/settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
const enum Methods {
|
||||
enum Methods {
|
||||
Random,
|
||||
Consistent,
|
||||
Timestamp,
|
||||
}
|
||||
|
||||
const tarExtMatcher = /\.tar\.\w+$/;
|
||||
|
||||
export default definePlugin({
|
||||
name: "AnonymiseFileNames",
|
||||
authors: [Devs.obscurity],
|
||||
@ -69,8 +67,7 @@ export default definePlugin({
|
||||
|
||||
anonymise(file: string) {
|
||||
let name = "image";
|
||||
const tarMatch = tarExtMatcher.exec(file);
|
||||
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");
|
||||
const extIdx = file.lastIndexOf(".");
|
||||
const ext = extIdx !== -1 ? file.slice(extIdx) : "";
|
||||
|
||||
switch (Settings.plugins.AnonymiseFileNames.method) {
|
||||
|
174
src/plugins/apiBadges.tsx
Normal file
174
src/plugins/apiBadges.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* 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 { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
|
||||
import DonateButton from "@components/DonateButton";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { Heart } from "@components/Heart";
|
||||
import { Devs } from "@utils/constants";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import Logger from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { closeModal, Modals, openModal } from "@utils/modal";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Forms } from "@webpack/common";
|
||||
|
||||
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/attachments/1033680203433660458/1092089947126780035/favicon.png";
|
||||
|
||||
/** List of vencord contributor IDs */
|
||||
const contributorIds: string[] = Object.values(Devs).map(d => d.id.toString());
|
||||
|
||||
const ContributorBadge: ProfileBadge = {
|
||||
description: "Vencord Contributor",
|
||||
image: CONTRIBUTOR_BADGE,
|
||||
position: BadgePosition.START,
|
||||
props: {
|
||||
style: {
|
||||
borderRadius: "50%",
|
||||
transform: "scale(0.9)" // The image is a bit too big compared to default badges
|
||||
}
|
||||
},
|
||||
shouldShow: ({ user }) => contributorIds.includes(user.id),
|
||||
link: "https://github.com/Vendicated/Vencord"
|
||||
};
|
||||
|
||||
const DonorBadges = {} as Record<string, Pick<ProfileBadge, "image" | "description">>;
|
||||
|
||||
export default definePlugin({
|
||||
name: "BadgeAPI",
|
||||
description: "API to add badges to users.",
|
||||
authors: [Devs.Megu, Devs.Ven, Devs.TheSun],
|
||||
required: true,
|
||||
patches: [
|
||||
/* Patch the badge list component on user profiles */
|
||||
{
|
||||
find: "Messages.PROFILE_USER_BADGES,role:",
|
||||
replacement: [
|
||||
{
|
||||
match: /null==\i\?void 0:(\i)\.getBadges\(\)/,
|
||||
replace: (_, badgesMod) => `Vencord.Api.Badges._getBadges(arguments[0]).concat(${badgesMod}?.getBadges()??[])`,
|
||||
},
|
||||
{
|
||||
// alt: "", aria-hidden: false, src: originalSrc
|
||||
match: /alt:" ","aria-hidden":!0,src:(?=(\i)\.src)/g,
|
||||
// ...badge.props, ..., src: badge.image ?? ...
|
||||
replace: "...$1.props,$& $1.image??"
|
||||
},
|
||||
{
|
||||
match: /children:function(?<=(\i)\.(?:tooltip|description),spacing:\d.+?)/g,
|
||||
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) : function"
|
||||
},
|
||||
{
|
||||
match: /onClick:function(?=.{0,200}href:(\i)\.link)/,
|
||||
replace: "onClick:$1.onClick??function"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
renderBadgeComponent: ErrorBoundary.wrap((badge: ProfileBadge & BadgeUserArgs) => {
|
||||
const Component = badge.component!;
|
||||
return <Component {...badge} />;
|
||||
}, { noop: true }),
|
||||
|
||||
async start() {
|
||||
Vencord.Api.Badges.addBadge(ContributorBadge);
|
||||
const badges = await fetch("https://gist.githubusercontent.com/Vendicated/51a3dd775f6920429ec6e9b735ca7f01/raw/badges.csv").then(r => r.text());
|
||||
const lines = badges.trim().split("\n");
|
||||
if (lines.shift() !== "id,tooltip,image") {
|
||||
new Logger("BadgeAPI").error("Invalid badges.csv file!");
|
||||
return;
|
||||
}
|
||||
for (const line of lines) {
|
||||
const [id, description, image] = line.split(",");
|
||||
DonorBadges[id] = { image, description };
|
||||
}
|
||||
},
|
||||
|
||||
getDonorBadge(userId: string) {
|
||||
const badge = DonorBadges[userId];
|
||||
if (badge) {
|
||||
return {
|
||||
...badge,
|
||||
position: BadgePosition.START,
|
||||
props: {
|
||||
style: {
|
||||
borderRadius: "50%",
|
||||
transform: "scale(0.9)" // The image is a bit too big compared to default badges
|
||||
}
|
||||
},
|
||||
onClick() {
|
||||
const modalKey = openModal(props => (
|
||||
<ErrorBoundary noop onError={() => {
|
||||
closeModal(modalKey);
|
||||
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/sponsors/Vendicated");
|
||||
}}>
|
||||
<Modals.ModalRoot {...props}>
|
||||
<Modals.ModalHeader>
|
||||
<Flex style={{ width: "100%", justifyContent: "center" }}>
|
||||
<Forms.FormTitle
|
||||
tag="h2"
|
||||
style={{
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
margin: 0
|
||||
}}
|
||||
>
|
||||
<Heart />
|
||||
Vencord Donor
|
||||
</Forms.FormTitle>
|
||||
</Flex>
|
||||
</Modals.ModalHeader>
|
||||
<Modals.ModalContent>
|
||||
<Flex>
|
||||
<img
|
||||
role="presentation"
|
||||
src="https://cdn.discordapp.com/emojis/1026533070955872337.png"
|
||||
alt=""
|
||||
style={{ margin: "auto" }}
|
||||
/>
|
||||
<img
|
||||
role="presentation"
|
||||
src="https://cdn.discordapp.com/emojis/1026533090627174460.png"
|
||||
alt=""
|
||||
style={{ margin: "auto" }}
|
||||
/>
|
||||
</Flex>
|
||||
<div style={{ padding: "1em" }}>
|
||||
<Forms.FormText>
|
||||
This Badge is a special perk for Vencord Donors
|
||||
</Forms.FormText>
|
||||
<Forms.FormText className={Margins.top20}>
|
||||
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
||||
</Forms.FormText>
|
||||
</div>
|
||||
</Modals.ModalContent>
|
||||
<Modals.ModalFooter>
|
||||
<Flex style={{ width: "100%", justifyContent: "center" }}>
|
||||
<DonateButton />
|
||||
</Flex>
|
||||
</Modals.ModalFooter>
|
||||
</Modals.ModalRoot>
|
||||
</ErrorBoundary>
|
||||
));
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user