mirror of
https://github.com/Vendicated/Vencord
synced 2024-08-26 03:37:47 -04:00
Merge branch 'main' into main
This commit is contained in:
commit
97f7f97df2
303 changed files with 14133 additions and 4990 deletions
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
|
@ -18,14 +18,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 19
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 19
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
4
.github/workflows/codeberg-mirror.yml
vendored
4
.github/workflows/codeberg-mirror.yml
vendored
|
@ -13,10 +13,10 @@ jobs:
|
|||
if: github.repository == 'Vendicated/Vencord'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1
|
||||
with:
|
||||
target_repo_url: "git@codeberg.org:Ven/cord.git"
|
||||
target_repo_url: "git@codeberg.org:Vee/cord.git"
|
||||
ssh_private_key: ${{ secrets.CODEBERG_SSH_PRIVATE_KEY }}
|
||||
|
|
8
.github/workflows/publish.yml
vendored
8
.github/workflows/publish.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: check that tag matches package.json version
|
||||
run: |
|
||||
|
@ -20,12 +20,12 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 19
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 19
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
23
.github/workflows/reportBrokenPlugins.yml
vendored
23
.github/workflows/reportBrokenPlugins.yml
vendored
|
@ -11,28 +11,31 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ github.event_name == 'schedule' }}
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 19
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 19
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm add puppeteer
|
||||
|
||||
sudo apt-get install -y chromium-browser
|
||||
- name: Install Google Chrome
|
||||
id: setup-chrome
|
||||
uses: browser-actions/setup-chrome@82b9ce628cc5595478a9ebadc480958a36457dc2
|
||||
with:
|
||||
chrome-version: stable
|
||||
|
||||
- name: Build web
|
||||
run: pnpm buildWeb --standalone --dev
|
||||
|
@ -41,7 +44,7 @@ jobs:
|
|||
timeout-minutes: 10
|
||||
run: |
|
||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||
export CHROMIUM_BIN=$(which chromium-browser)
|
||||
export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
|
||||
|
||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||
|
@ -54,7 +57,7 @@ jobs:
|
|||
if: success() || failure() # even run if previous one failed
|
||||
run: |
|
||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||
export CHROMIUM_BIN=$(which chromium-browser)
|
||||
export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
|
||||
export USE_CANARY=true
|
||||
|
||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
||||
|
|
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
|
@ -10,13 +10,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
1
.npmrc
1
.npmrc
|
@ -1 +1,2 @@
|
|||
strict-peer-dependencies=false
|
||||
package-manager-strict=false
|
||||
|
|
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
|
@ -1,11 +1,9 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eamodio.gitlens",
|
||||
"EditorConfig.EditorConfig",
|
||||
"ExodiusStudios.comment-anchors",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"GregorBiswanger.json2ts",
|
||||
"stylelint.vscode-stylelint"
|
||||
"stylelint.vscode-stylelint",
|
||||
"Vendicated.vencord-companion"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 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)
|
||||
[![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Vee/cord&color=2185D0&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAKbUlEQVR4nNVae3AV5RX/nW/3Pva+b24e5HHzIICQKGoiYiW8NFBFgohaa6ctglpbFSujSGurzUinohWsOij/gGX6R2fqOK0d1FYTEZXaTrWCBbEikJCEyCvkeXNvkrunf+zdkJDkPnex/c3cmd29+53v/M6e73znnF2Cydj4Tntldzi6qrN/qKqzf2jy6b7BnL4B1dI7oMp9AyoRAIdVsNMqhlxWMZjtspzyK/Jhr036OMsm//bh2vzPzNSPzBD6xFutd7R0Dq758ky4orkjYuc05RCAkixbeEq2/UCJ1/LczxcX/c5IPfU5DMHmxpbCpu7o1k/b+xc1n43YjJI7EqV+W2RmvuPt0oDjB2vn5bQbITNjAzzdeKK8qTO0bU9T77zucNQUjzofHrvENWWu3aUBZfW6+ZOOZiIrbYXrmUXo9daX3v6i667O/iGRiRLpwqtIvKDc+0efJ3hb/UIaSkdGWgZ4sqGt9r2m3lc/P9HvSWe80ZiRp3TPL/UsX1+bvyvVsSkb4NE3WjbuPNj5SM8Fcvdk4bAKrqvwv7DxhuCPUxmXNIn6XSy3nWr6R8OhrqrU1btwqJ3m/bgwu/SqZJdEUgbYsuuka09b9/4Pm3tLMlPvwuAbpe6m+RcplfdcURBKdG9CA2zZddLV2Nx1+JO2vlxj1LswqCpynlxc6SxLZIS40bueWfy9vXvv/xt5APhXa1/u7v+EPqvfxXK8++IaoO2Vpn9+cLS33FjVLhw+bOotOX7q6N/i3TOhAX7y+rHN/+sBLxm8fah71k93tjw/0f/jGuDJxtZrdh7setA8tS4sdn7eef+v3mmfP95/Ywxw6x9Yev9I35/6Iubv83WVfl5a6Uu3VkoavZEo7TnS/Vo98xi+Yy6UKC3bDp7sd5ut1OWFDjyzNMib6oq5Oug0ezp8dqLfG3r92Nbzr48ywNONJ8obDnV/z2xlAk4ZW1aUqhaJIAvCb5YVqwFn3GBtCBoO9dz5TOPxUbnMKAM0dYa2d5lc2AgCNi8r5klui3aBgWynjE11QZbI3FV3NjQkjnYNbB+lj36wubGlcE9T71xTNQDw0Px8nlvmHl73GmfCrKCL19Tkmh4P9jT1LHz2vVP5+vmwAZq71a1m1/PXTPXwD68eS5KIEVUZd1yZwwumeEw1Qld/lJrPhF7Sz4cNsO+rUK2ZExd6rfj10iCPZ2GJCCoAZuCJxQUc9FvNVAX72kPX6ccC0Hp4zR0Ru1kT2mTCSzeXqn5l/EAniMAqoDLDYZWwqa5EVSzmhaKmsxHbLxvbbgdiBmjpHFxj2mwANlxXxBdPUib8nwgQgqAyEFUZxT4L1i/MN3UpHDsTWQvEDHDoTLjCrIluuyzAt8zMSkhGFhp5hrYUFk3z8IqZftOMcKRj4GIAEM80tFccM8n9Z+Qq+MXigqRIWCQCMzQvYIbKwH1X53FFnjkr88iZsLKpoXWa6BiIrjbDzF67hK23lKp2Obm1LAstPEZVjTwDkAio/2ZQ9dolw/VjAB0DfKfoCg9WGy2cADy1NMhBX2rR3CIRGICq8rAhAg4Jj9UWsDBhg+4MR6vF2VC0zGjB99fk8eJp3pQdyyrRMHF9KURVxswCB6+alWO4o3b2RyeLU32D2UYKnVPm5gfm5qWlrF0Wo4hzbCmoDNw0089XlboNNcLpvsFc0RtRDXuNle+x4Lkbi9PO6WWJIBFGEY+qjGjswtq5eVzosRilLnoiUavoH1INiTCyIDy/vETNcmRW1dl0L4gRVxmx3YFhlwnrry1QrZIxASE0yJIIDaiGSHt8UQFXF2Ve1zusYgzxkXGhyGvFvePUE+mgfyAqhGqAqKWVPv5udbYhSjmtkpYWq6OJqzFjqCpjTpmbl1Rk3klSGRBWmTISNC3Hjo1LgoYFJ0GA1aIVR+cTVxlQoS2Pb18a4PLszMKXzSJYuCySmq4Al03CiytKVYfBhYvLKk1IXE+XLRLhwZp81WlNf26HTFHhd0jhdAYTgKduCPLkgPHfQjitYkLiAIEZBDBlu2R6aF7euCV2Mgg45bDw2qWOdAavnp3D109PPdlJBvpTnYg4kVY3MDMuylVw62WJi63x4LHLZ0TAIR9OdWBVodPUclUQwWmT4hLXfgCIUDfDi6oiR8rzBJzyl8LnkD9KZVCOU8aLN5eoshnJ+Qh4bFJC4gztmEjgrtk5anaKnWWfXfpIuBTLjmSpSILw/E0laq7LuGxsIngVCYmIa96hLRG3TaZ1C/KTfjAEQLFIO8TPFk7aH/RZI8kMWrdgEs8udqXLKSUoMkEW4ETEQTRsoHyPlVZfmVw+Uuy3hR9bVHBQAMD0XPu/Ew24dqqH777K/La1DiKCxyYlRRzQymgG4+oyDxZOTdxZnp5r3wvEWmJ5btuL8W4uzbJh87LitLebdOFVpKSJx4IlwIzbL81CcYLO8iSX/IImGQCYae6Wg/2tXQNjNnW7LPDKyilqZd7ETU2zEBlifNTSS4i9PNFIx44x4jh2nZlBsUr0dN8QP/6XVhEaHJvnlfhtkXd/NF0BUextKRFXFznfGk+JDdcX8tdBHtDa6YpFsB4I9ac88omf8wbEgqa2XAIOme6bM35foqrQ+QZIKwGG80ifVbrXZZNGDfhOVYBvviS9JMMoaP3AEcQpPnHdOxiMGXkKbrx4dGfZY5c4T8H9+vmwAeqXFLXOKXW9r59fWuDA44sKv1byAOBzyCkTH+kdS2f4MLPgXJI0p9T17vrFxcf181GVxEUB+0qfIqt+RcKWFSWGNR4ygd4RTpW4HiCJgFWzstmnSPA7ZLU827pypPwxDB/687GXl1X6Vs6bbGz/LRN80hZCT+yLFZ0cgHED4egACeiXm89GsP9EePuzy4rvGil7jAGYmQDsBjDHUBYZ4GhHBMfORigd4rpnyIS9u6d4rqgnGrUtjCmmSYuOqwB0GcwjbWh9xviurpNnxnDA1IspMPe6bOL755MHJvhKjIgOA7jbJD4pw22Thj+kSIW47h2KRaydVezeP57sCdspRPQqgGeNJJIuBAE+ReJUiOv32mXaXjPZs21C2QnmXgdghyEsMoRfkVMiDgCywF/by9z3xJMb1wCxeHAPgDczZpAh/Iq+HSYmDjCsstgThmf5t4ii8eQm7CgS0SCA5QBezoRApnBaBSyCEhIHCLJEb4ZUd+2SqZSwzE+qpUpEQ9CC4qb01M8cRIQsh8zxiKsMtsn08nvlnrpkyAPj5AGJwMw3AtgGwJ/q2ExxvHsQB74KxfKBMblAyGmTHq4pc4/5GjQeUm6qE9FrAK4E8H6ie41GlkN/jTk6F5Ak2ueUpNmpkgfSMAAAENERAAsB3AHgZDoy0oFdFnBYpXPEBfU4beLRD6Z4qmumug+kIzPjaoeZfQDWAHgAQFam8hLh4MkwWjsHemyS2OF08IYrCjynzZ4zKTCzi5nXMvOnzBw16bevIxR95JOj7DNKb1PqXWa+HMDtAGoBXII0lxq0N2OfAmgA8Hsi2muMhudgesHPzNkA5gKoADADwFRoS8UHQO+x9wLoBNAB4AsAnwM4AOADIjLVxf8L9kdXUOE0IskAAAAASUVORK5CYII=)](https://codeberg.org/Vee/cord)
|
||||
|
||||
The cutest Discord client mod
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ function GM_fetch(url, opt) {
|
|||
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
|
||||
resp.text = () => blobTo("text", blob);
|
||||
resp.json = async () => JSON.parse(await blobTo("text", blob));
|
||||
resp.headers = new Headers(parseHeaders(resp.responseHeaders));
|
||||
resp.headers = parseHeaders(resp.responseHeaders);
|
||||
resp.ok = resp.status >= 200 && resp.status < 300;
|
||||
resolve(resp);
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ import { debounce } from "../src/utils";
|
|||
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
|
||||
import { getTheme, Theme } from "../src/utils/discord";
|
||||
import { getThemeInfo } from "../src/main/themes";
|
||||
import { Settings } from "../src/Vencord";
|
||||
|
||||
// Discord deletes this so need to store in variable
|
||||
const { localStorage } = window;
|
||||
|
@ -96,8 +97,15 @@ window.VencordNative = {
|
|||
},
|
||||
|
||||
settings: {
|
||||
get: () => localStorage.getItem("VencordSettings") || "{}",
|
||||
set: async (s: string) => localStorage.setItem("VencordSettings", s),
|
||||
get: () => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem("VencordSettings") || "{}");
|
||||
} catch (e) {
|
||||
console.error("Failed to parse settings from localStorage: ", e);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
set: async (s: Settings) => localStorage.setItem("VencordSettings", JSON.stringify(s)),
|
||||
getSettingsDir: async () => "LocalStorage"
|
||||
},
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
> [!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.
|
||||
> No support will be provided for installing in this fashion. If you cannot figure it out, you should just stick to a regular install.
|
||||
|
||||
# Installation Guide
|
||||
|
||||
|
@ -95,5 +95,3 @@ Simply run:
|
|||
```shell
|
||||
pnpm uninject
|
||||
```
|
||||
|
||||
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
||||
|
|
18
package.json
18
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.6.5",
|
||||
"version": "1.8.6",
|
||||
"description": "The cutest Discord client mod",
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
|
@ -19,16 +19,17 @@
|
|||
"scripts": {
|
||||
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
|
||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||
"watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch",
|
||||
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
||||
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
|
||||
"inject": "node scripts/runInstaller.mjs",
|
||||
"uninject": "node scripts/runInstaller.mjs",
|
||||
"lint": "eslint . --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",
|
||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
||||
"testTsc": "tsc --noEmit",
|
||||
"uninject": "node scripts/runInstaller.mjs",
|
||||
"watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch"
|
||||
"testTsc": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
||||
|
@ -65,13 +66,14 @@
|
|||
"standalone-electron-types": "^1.0.0",
|
||||
"stylelint": "^15.6.0",
|
||||
"stylelint-config-standard": "^33.0.0",
|
||||
"ts-patch": "^3.1.2",
|
||||
"tsx": "^3.12.7",
|
||||
"type-fest": "^3.9.0",
|
||||
"typescript": "^5.0.4",
|
||||
"zip-local": "^0.3.5",
|
||||
"zustand": "^3.7.2"
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-transform-paths": "^3.4.7",
|
||||
"zip-local": "^0.3.5"
|
||||
},
|
||||
"packageManager": "pnpm@8.10.2",
|
||||
"packageManager": "pnpm@9.1.0",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
||||
|
|
7
packages/vencord-types/.gitignore
vendored
Normal file
7
packages/vencord-types/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
*
|
||||
!.*ignore
|
||||
!package.json
|
||||
!*.md
|
||||
!prepare.ts
|
||||
!index.d.ts
|
||||
!globals.d.ts
|
4
packages/vencord-types/.npmignore
Normal file
4
packages/vencord-types/.npmignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
prepare.ts
|
||||
.gitignore
|
||||
HOW2PUB.md
|
5
packages/vencord-types/HOW2PUB.md
Normal file
5
packages/vencord-types/HOW2PUB.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# How to publish
|
||||
|
||||
1. run `pnpm generateTypes` in the project root
|
||||
2. bump package.json version
|
||||
3. npm publish
|
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
|
||||
```
|
24
packages/vencord-types/globals.d.ts
vendored
Normal file
24
packages/vencord-types/globals.d.ts
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare global {
|
||||
export var VencordNative: typeof import("./VencordNative").default;
|
||||
export var Vencord: typeof import("./Vencord");
|
||||
}
|
||||
|
||||
export { };
|
5
packages/vencord-types/index.d.ts
vendored
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" />
|
28
packages/vencord-types/package.json
Normal file
28
packages/vencord-types/package.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "@vencord/types",
|
||||
"private": false,
|
||||
"version": "0.1.3",
|
||||
"description": "",
|
||||
"types": "index.d.ts",
|
||||
"scripts": {
|
||||
"prepublishOnly": "tsx ./prepare.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Vencord",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"tsx": "^3.12.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"discord-types": "^1.3.26",
|
||||
"standalone-electron-types": "^1.0.0",
|
||||
"type-fest": "^3.5.3"
|
||||
}
|
||||
}
|
47
packages/vencord-types/prepare.ts
Normal file
47
packages/vencord-types/prepare.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { cpSync, moveSync, readdirSync, rmSync } from "fs-extra";
|
||||
import { join } from "path";
|
||||
|
||||
readdirSync(join(__dirname, "src"))
|
||||
.forEach(child => moveSync(join(__dirname, "src", child), join(__dirname, child), { overwrite: true }));
|
||||
|
||||
const VencordSrc = join(__dirname, "..", "..", "src");
|
||||
|
||||
for (const file of ["preload.d.ts", "userplugins", "main", "debug", "src", "browser", "scripts"]) {
|
||||
rmSync(join(__dirname, file), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function copyDtsFiles(from: string, to: string) {
|
||||
for (const file of readdirSync(from, { withFileTypes: true })) {
|
||||
// bad
|
||||
if (from === VencordSrc && file.name === "globals.d.ts") continue;
|
||||
|
||||
const fullFrom = join(from, file.name);
|
||||
const fullTo = join(to, file.name);
|
||||
|
||||
if (file.isDirectory()) {
|
||||
copyDtsFiles(fullFrom, fullTo);
|
||||
} else if (file.name.endsWith(".d.ts")) {
|
||||
cpSync(fullFrom, fullTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
copyDtsFiles(VencordSrc, __dirname);
|
5434
pnpm-lock.yaml
5434
pnpm-lock.yaml
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/*
|
|
@ -25,10 +25,11 @@ import { access, 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" };
|
||||
import { getPluginTarget } from "../utils.mjs";
|
||||
|
||||
/** @type {import("../../package.json")} */
|
||||
const PackageJSON = JSON.parse(readFileSync("package.json"));
|
||||
|
||||
export const VERSION = PackageJSON.version;
|
||||
// https://reproducible-builds.org/docs/source-date-epoch/
|
||||
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
|
||||
|
|
|
@ -67,7 +67,8 @@ const IGNORED_DISCORD_ERRORS = [
|
|||
"Unable to process domain list delta: Client revision number is null",
|
||||
"Downloading the full bad domains file",
|
||||
/\[GatewaySocket\].{0,110}Cannot access '/,
|
||||
"search for 'name' in undefined"
|
||||
"search for 'name' in undefined",
|
||||
"Attempting to set fast connect zstd when unsupported"
|
||||
] as Array<string | RegExp>;
|
||||
|
||||
function toCodeBlock(s: string) {
|
||||
|
@ -242,19 +243,27 @@ page.on("console", async e => {
|
|||
}
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
console.error(e.text());
|
||||
} else if (level === "error") {
|
||||
const text = await Promise.all(
|
||||
e.args().map(async a => {
|
||||
try {
|
||||
async function getText() {
|
||||
try {
|
||||
return await Promise.all(
|
||||
e.args().map(async a => {
|
||||
return await maybeGetError(a) || await a.jsonValue();
|
||||
} catch (e) {
|
||||
return a.toString();
|
||||
}
|
||||
})
|
||||
).then(a => a.join(" ").trim());
|
||||
})
|
||||
).then(a => a.join(" ").trim());
|
||||
} catch {
|
||||
return e.text();
|
||||
}
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
const text = await getText();
|
||||
|
||||
console.error(text);
|
||||
if (text.includes("A fatal error occurred:")) {
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (level === "error") {
|
||||
const text = await getText();
|
||||
|
||||
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
||||
console.error("[Unexpected Error]", text);
|
||||
|
@ -268,7 +277,7 @@ page.on("pageerror", e => console.error("[Page Error]", e));
|
|||
|
||||
await page.setBypassCSP(true);
|
||||
|
||||
function runTime(token: string) {
|
||||
async function runtime(token: string) {
|
||||
console.log("[PUP_DEBUG]", "Starting test...");
|
||||
|
||||
try {
|
||||
|
@ -281,9 +290,13 @@ function runTime(token: string) {
|
|||
|
||||
// Monkey patch Logger to not log with custom css
|
||||
// @ts-ignore
|
||||
const originalLog = Vencord.Util.Logger.prototype._log;
|
||||
// @ts-ignore
|
||||
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
|
||||
if (level === "warn" || level === "error")
|
||||
console[level]("[Vencord]", this.name + ":", ...args);
|
||||
return console[level]("[Vencord]", this.name + ":", ...args);
|
||||
|
||||
return originalLog.call(this, level, levelColor, args);
|
||||
};
|
||||
|
||||
// Force enable all plugins and patches
|
||||
|
@ -298,8 +311,10 @@ function runTime(token: string) {
|
|||
delete patch.predicate;
|
||||
delete patch.group;
|
||||
|
||||
if (!Array.isArray(patch.replacement))
|
||||
Vencord.Util.canonicalizeFind(patch);
|
||||
if (!Array.isArray(patch.replacement)) {
|
||||
patch.replacement = [patch.replacement];
|
||||
}
|
||||
|
||||
patch.replacement.forEach(r => {
|
||||
delete r.predicate;
|
||||
|
@ -309,45 +324,39 @@ function runTime(token: string) {
|
|||
});
|
||||
});
|
||||
|
||||
Vencord.Webpack.waitFor(
|
||||
"loginToken",
|
||||
m => {
|
||||
console.log("[PUP_DEBUG]", "Logging in with token...");
|
||||
m.loginToken(token);
|
||||
}
|
||||
);
|
||||
let wreq: typeof Vencord.Webpack.wreq;
|
||||
|
||||
// Force load all chunks
|
||||
Vencord.Webpack.onceReady.then(() => setTimeout(async () => {
|
||||
console.log("[PUP_DEBUG]", "Webpack is ready!");
|
||||
const { canonicalizeMatch, Logger } = Vencord.Util;
|
||||
|
||||
const { wreq } = Vencord.Webpack;
|
||||
const validChunks = new Set<string>();
|
||||
const invalidChunks = new Set<string>();
|
||||
const deferredRequires = new Set<string>();
|
||||
|
||||
console.log("[PUP_DEBUG]", "Loading all chunks...");
|
||||
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
|
||||
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
|
||||
|
||||
let chunks = null as Record<number, string[]> | null;
|
||||
const sym = Symbol("Vencord.chunksExtract");
|
||||
// True if resolved, false otherwise
|
||||
const chunksSearchPromises = [] as Array<() => boolean>;
|
||||
|
||||
Object.defineProperty(Object.prototype, sym, {
|
||||
get() {
|
||||
chunks = this;
|
||||
},
|
||||
set() { },
|
||||
configurable: true,
|
||||
});
|
||||
const LazyChunkRegex = canonicalizeMatch(/(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\)))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g);
|
||||
|
||||
await (wreq as any).el(sym);
|
||||
delete Object.prototype[sym];
|
||||
async function searchAndLoadLazyChunks(factoryCode: string) {
|
||||
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
|
||||
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
|
||||
|
||||
const validChunksEntryPoints = new Set<string>();
|
||||
const validChunks = new Set<string>();
|
||||
const invalidChunks = new Set<string>();
|
||||
// Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
|
||||
// the chunk containing the component
|
||||
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
|
||||
|
||||
if (!chunks) throw new Error("Failed to get chunks");
|
||||
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIdsArray, rawChunkIdsSingle, entryPoint]) => {
|
||||
const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle;
|
||||
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Vencord.Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
|
||||
|
||||
for (const entryPoint in chunks) {
|
||||
const chunkIds = chunks[entryPoint];
|
||||
let invalidEntryPoint = false;
|
||||
if (chunkIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let invalidChunkGroup = false;
|
||||
|
||||
for (const id of chunkIds) {
|
||||
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
|
||||
|
@ -358,120 +367,187 @@ function runTime(token: string) {
|
|||
|
||||
if (isWasm) {
|
||||
invalidChunks.add(id);
|
||||
invalidEntryPoint = true;
|
||||
invalidChunkGroup = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
validChunks.add(id);
|
||||
}
|
||||
|
||||
if (!invalidEntryPoint)
|
||||
validChunksEntryPoints.add(entryPoint);
|
||||
}
|
||||
if (!invalidChunkGroup) {
|
||||
validChunkGroups.add([chunkIds, entryPoint]);
|
||||
}
|
||||
}));
|
||||
|
||||
for (const entryPoint of validChunksEntryPoints) {
|
||||
// Loads all found valid chunk groups
|
||||
await Promise.all(
|
||||
Array.from(validChunkGroups)
|
||||
.map(([chunkIds]) =>
|
||||
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
||||
)
|
||||
);
|
||||
|
||||
// Requires the entry points for all valid chunk groups
|
||||
for (const [, entryPoint] of validChunkGroups) {
|
||||
try {
|
||||
// Loads all chunks required for an entry point
|
||||
await (wreq as any).el(entryPoint);
|
||||
} catch (err) { }
|
||||
}
|
||||
if (shouldForceDefer) {
|
||||
deferredRequires.add(entryPoint);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Matches "id" or id:
|
||||
const chunkIdRegex = /(?:"(\d+?)")|(?:(\d+?):)/g;
|
||||
const wreqU = wreq.u.toString();
|
||||
|
||||
const allChunks = [] as string[];
|
||||
let currentMatch: RegExpExecArray | null;
|
||||
|
||||
while ((currentMatch = chunkIdRegex.exec(wreqU)) != null) {
|
||||
const id = currentMatch[1] ?? currentMatch[2];
|
||||
if (id == null) continue;
|
||||
|
||||
allChunks.push(id);
|
||||
}
|
||||
|
||||
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
||||
const chunksLeft = allChunks.filter(id => {
|
||||
return !(validChunks.has(id) || invalidChunks.has(id));
|
||||
});
|
||||
|
||||
for (const id of chunksLeft) {
|
||||
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"));
|
||||
|
||||
// Loads a chunk
|
||||
if (!isWasm) await wreq.e(id as any);
|
||||
}
|
||||
|
||||
// Make sure every chunk has finished loading
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
for (const entryPoint of validChunksEntryPoints) {
|
||||
try {
|
||||
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[PUP_DEBUG]", "Finished loading all chunks!");
|
||||
// setImmediate to only check if all chunks were loaded after this function resolves
|
||||
// We check if all chunks were loaded every time a factory is loaded
|
||||
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
|
||||
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
|
||||
setTimeout(() => {
|
||||
let allResolved = true;
|
||||
|
||||
for (const patch of Vencord.Plugins.patches) {
|
||||
if (!patch.all) {
|
||||
new Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < chunksSearchPromises.length; i++) {
|
||||
const isResolved = chunksSearchPromises[i]();
|
||||
|
||||
for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) {
|
||||
let method = searchType;
|
||||
|
||||
if (searchType === "findComponent") method = "find";
|
||||
if (searchType === "findExportedComponent") method = "findByProps";
|
||||
if (searchType === "waitFor" || searchType === "waitForComponent" || searchType === "waitForStore") {
|
||||
if (typeof args[0] === "string") method = "findByProps";
|
||||
else method = "find";
|
||||
}
|
||||
|
||||
try {
|
||||
let result: any;
|
||||
|
||||
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
|
||||
const [factory] = args;
|
||||
result = factory();
|
||||
} else if (method === "extractAndLoadChunks") {
|
||||
const [code, matcher] = args;
|
||||
|
||||
const module = Vencord.Webpack.findModuleFactory(...code);
|
||||
if (module) result = module.toString().match(Vencord.Util.canonicalizeMatch(matcher));
|
||||
if (isResolved) {
|
||||
// Remove finished promises to avoid having to iterate through a huge array everytime
|
||||
chunksSearchPromises.splice(i--, 1);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
result = Vencord.Webpack[method](...args);
|
||||
allResolved = false;
|
||||
}
|
||||
|
||||
if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
|
||||
} catch (e) {
|
||||
let logMessage = searchType;
|
||||
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
||||
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
||||
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
||||
|
||||
console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
|
||||
}, 1000));
|
||||
if (allResolved) chunksSearchingResolve();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
Vencord.Webpack.waitFor(
|
||||
"loginToken",
|
||||
m => {
|
||||
console.log("[PUP_DEBUG]", "Logging in with token...");
|
||||
m.loginToken(token);
|
||||
}
|
||||
);
|
||||
|
||||
Vencord.Webpack.beforeInitListeners.add(async webpackRequire => {
|
||||
console.log("[PUP_DEBUG]", "Loading all chunks...");
|
||||
|
||||
wreq = webpackRequire;
|
||||
|
||||
Vencord.Webpack.factoryListeners.add(factory => {
|
||||
let isResolved = false;
|
||||
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
|
||||
|
||||
chunksSearchPromises.push(() => isResolved);
|
||||
});
|
||||
|
||||
// setImmediate to only search the initial factories after Discord initialized the app
|
||||
// our beforeInitListeners are called before Discord initializes the app
|
||||
setTimeout(() => {
|
||||
for (const factoryId in wreq.m) {
|
||||
let isResolved = false;
|
||||
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
|
||||
|
||||
chunksSearchPromises.push(() => isResolved);
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
await chunksSearchingDone;
|
||||
|
||||
// Require deferred entry points
|
||||
for (const deferredRequire of deferredRequires) {
|
||||
wreq!(deferredRequire as any);
|
||||
}
|
||||
|
||||
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
||||
const allChunks = [] as string[];
|
||||
|
||||
// Matches "id" or id:
|
||||
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
|
||||
const id = currentMatch[1] ?? currentMatch[2];
|
||||
if (id == null) continue;
|
||||
|
||||
allChunks.push(id);
|
||||
}
|
||||
|
||||
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
||||
|
||||
// Chunks that are not loaded (not used) by Discord code anymore
|
||||
const chunksLeft = allChunks.filter(id => {
|
||||
return !(validChunks.has(id) || invalidChunks.has(id));
|
||||
});
|
||||
|
||||
await Promise.all(chunksLeft.map(async id => {
|
||||
const 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"));
|
||||
|
||||
// Loads and requires a chunk
|
||||
if (!isWasm) {
|
||||
await wreq.e(id as any);
|
||||
if (wreq.m[id]) wreq(id as any);
|
||||
}
|
||||
}));
|
||||
|
||||
console.log("[PUP_DEBUG]", "Finished loading all chunks!");
|
||||
|
||||
for (const patch of Vencord.Plugins.patches) {
|
||||
if (!patch.all) {
|
||||
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) {
|
||||
let method = searchType;
|
||||
|
||||
if (searchType === "findComponent") method = "find";
|
||||
if (searchType === "findExportedComponent") method = "findByProps";
|
||||
if (searchType === "waitFor" || searchType === "waitForComponent") {
|
||||
if (typeof args[0] === "string") method = "findByProps";
|
||||
else method = "find";
|
||||
}
|
||||
if (searchType === "waitForStore") method = "findStore";
|
||||
|
||||
try {
|
||||
let result: any;
|
||||
|
||||
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
|
||||
const [factory] = args;
|
||||
result = factory();
|
||||
} else if (method === "extractAndLoadChunks") {
|
||||
const [code, matcher] = args;
|
||||
|
||||
const module = Vencord.Webpack.findModuleFactory(...code);
|
||||
if (module) result = module.toString().match(canonicalizeMatch(matcher));
|
||||
} else {
|
||||
// @ts-ignore
|
||||
result = Vencord.Webpack[method](...args);
|
||||
}
|
||||
|
||||
if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
|
||||
} catch (e) {
|
||||
let logMessage = searchType;
|
||||
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
||||
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
||||
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
||||
|
||||
console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
|
||||
} catch (e) {
|
||||
console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await page.evaluateOnNewDocument(`
|
||||
${readFileSync("./dist/browser.js", "utf-8")}
|
||||
|
||||
;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
||||
;(${runtime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
||||
`);
|
||||
|
||||
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
|
||||
|
|
|
@ -35,11 +35,11 @@ const ETAG_FILE = join(FILE_DIR, "etag.txt");
|
|||
function getFilename() {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
return "VencordInstaller.exe";
|
||||
return "VencordInstallerCli.exe";
|
||||
case "darwin":
|
||||
return "VencordInstaller.MacOS.zip";
|
||||
case "linux":
|
||||
return "VencordInstaller-" + (process.env.WAYLAND_DISPLAY ? "wayland" : "x11");
|
||||
return "VencordInstallerCli-linux";
|
||||
default:
|
||||
throw new Error("Unsupported platform: " + process.platform);
|
||||
}
|
||||
|
@ -118,11 +118,15 @@ const installerBin = await ensureBinary();
|
|||
|
||||
console.log("Now running Installer...");
|
||||
|
||||
execFileSync(installerBin, {
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
VENCORD_USER_DATA_DIR: BASE_DIR,
|
||||
VENCORD_DEV_INSTALL: "1"
|
||||
}
|
||||
});
|
||||
try {
|
||||
execFileSync(installerBin, {
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
VENCORD_USER_DATA_DIR: BASE_DIR,
|
||||
VENCORD_DEV_INSTALL: "1"
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
console.error("Something went wrong. Please check the logs above.");
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
export * as Api from "./api";
|
||||
export * as Components from "./components";
|
||||
export * as Plugins from "./plugins";
|
||||
export * as Util from "./utils";
|
||||
export * as QuickCss from "./utils/quickCss";
|
||||
|
@ -27,6 +28,7 @@ export { PlainSettings, Settings };
|
|||
import "./utils/quickCss";
|
||||
import "./webpack/patchWebpack";
|
||||
|
||||
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
||||
import { StartAt } from "@utils/types";
|
||||
|
||||
import { get as dsGet } from "./api/DataStore";
|
||||
|
@ -44,7 +46,7 @@ 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
|
||||
!await dsGet("Vencord_cloudSecret") // 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({
|
||||
|
@ -85,7 +87,7 @@ async function init() {
|
|||
|
||||
syncSettings();
|
||||
|
||||
if (!IS_WEB) {
|
||||
if (!IS_WEB && !IS_UPDATER_DISABLED) {
|
||||
try {
|
||||
const isOutdated = await checkForUpdates();
|
||||
if (!isOutdated) return;
|
||||
|
@ -103,16 +105,13 @@ async function init() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (Settings.notifyAboutUpdates)
|
||||
setTimeout(() => showNotification({
|
||||
title: "A Vencord update is available!",
|
||||
body: "Click here to view the update",
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick() {
|
||||
SettingsRouter.open("VencordUpdater");
|
||||
}
|
||||
}), 10_000);
|
||||
setTimeout(() => showNotification({
|
||||
title: "A Vencord update is available!",
|
||||
body: "Click here to view the update",
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick: openUpdaterModal!
|
||||
}), 10_000);
|
||||
} catch (err) {
|
||||
UpdateLogger.error("Failed to check for updates", err);
|
||||
}
|
||||
|
@ -145,4 +144,3 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
}));
|
||||
}
|
||||
}, { once: true });
|
||||
|
||||
|
|
|
@ -4,11 +4,12 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { IpcEvents } from "@utils/IpcEvents";
|
||||
import { PluginIpcMappings } from "@main/ipcPlugins";
|
||||
import type { UserThemeHeader } from "@main/themes";
|
||||
import { IpcEvents } from "@shared/IpcEvents";
|
||||
import { IpcRes } from "@utils/types";
|
||||
import type { Settings } from "api/Settings";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { PluginIpcMappings } from "main/ipcPlugins";
|
||||
import type { UserThemeHeader } from "main/themes";
|
||||
|
||||
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
|
||||
return ipcRenderer.invoke(event, ...args) as Promise<T>;
|
||||
|
@ -46,8 +47,8 @@ export default {
|
|||
},
|
||||
|
||||
settings: {
|
||||
get: () => sendSync<string>(IpcEvents.GET_SETTINGS),
|
||||
set: (settings: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings),
|
||||
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
|
||||
set: (settings: Settings, pathToNotify?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, pathToNotify),
|
||||
getSettingsDir: () => invoke<string>(IpcEvents.GET_SETTINGS_DIR),
|
||||
},
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ export interface ProfileBadge {
|
|||
image?: string;
|
||||
link?: string;
|
||||
/** Action to perform when you click the badge */
|
||||
onClick?(): void;
|
||||
onClick?(event: React.MouseEvent<HTMLButtonElement, MouseEvent>, props: BadgeUserArgs): void;
|
||||
/** Should the user display this badge? */
|
||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
||||
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
||||
|
@ -87,9 +87,7 @@ export function _getBadges(args: BadgeUserArgs) {
|
|||
|
||||
export interface BadgeUserArgs {
|
||||
user: User;
|
||||
profile: Profile;
|
||||
premiumSince: Date;
|
||||
premiumGuildSince?: Date;
|
||||
guildId: string;
|
||||
}
|
||||
|
||||
interface ConnectedAccount {
|
||||
|
|
4
src/api/ChatButton.css
Normal file
4
src/api/ChatButton.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.vc-chatbar-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
128
src/api/ChatButtons.tsx
Normal file
128
src/api/ChatButtons.tsx
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./ChatButton.css";
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { waitFor } from "@webpack";
|
||||
import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
import { HTMLProps, MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>;
|
||||
waitFor(["buttonContainer", "channelTextArea"], m => ChannelTextAreaClasses = m);
|
||||
|
||||
export interface ChatBarProps {
|
||||
channel: Channel;
|
||||
disabled: boolean;
|
||||
isEmpty: boolean;
|
||||
type: {
|
||||
analyticsName: string;
|
||||
attachments: boolean;
|
||||
autocomplete: {
|
||||
addReactionShortcut: boolean,
|
||||
forceChatLayer: boolean,
|
||||
reactions: boolean;
|
||||
},
|
||||
commands: {
|
||||
enabled: boolean;
|
||||
},
|
||||
drafts: {
|
||||
type: number,
|
||||
commandType: number,
|
||||
autoSave: boolean;
|
||||
},
|
||||
emojis: {
|
||||
button: boolean;
|
||||
},
|
||||
gifs: {
|
||||
button: boolean,
|
||||
allowSending: boolean;
|
||||
},
|
||||
gifts: {
|
||||
button: boolean;
|
||||
},
|
||||
permissions: {
|
||||
requireSendMessages: boolean;
|
||||
},
|
||||
showThreadPromptOnReply: boolean,
|
||||
stickers: {
|
||||
button: boolean,
|
||||
allowSending: boolean,
|
||||
autoSuggest: boolean;
|
||||
},
|
||||
users: {
|
||||
allowMentioning: boolean;
|
||||
},
|
||||
submit: {
|
||||
button: boolean,
|
||||
ignorePreference: boolean,
|
||||
disableEnterToSubmit: boolean,
|
||||
clearOnSubmit: boolean,
|
||||
useDisabledStylesOnSubmit: boolean;
|
||||
},
|
||||
uploadLongMessages: boolean,
|
||||
upsellLongMessages: {
|
||||
iconOnly: boolean;
|
||||
},
|
||||
showCharacterCount: boolean,
|
||||
sedReplace: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type ChatBarButton = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null;
|
||||
|
||||
const buttonFactories = new Map<string, ChatBarButton>();
|
||||
const logger = new Logger("ChatButtons");
|
||||
|
||||
export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) {
|
||||
if (props.disabled) return;
|
||||
|
||||
for (const [key, Button] of buttonFactories) {
|
||||
buttons.push(
|
||||
<ErrorBoundary noop key={key} onError={e => logger.error(`Failed to render ${key}`, e.error)}>
|
||||
<Button {...props} isMainChat={props.type.analyticsName === "normal"} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const addChatBarButton = (id: string, button: ChatBarButton) => buttonFactories.set(id, button);
|
||||
export const removeChatBarButton = (id: string) => buttonFactories.delete(id);
|
||||
|
||||
export interface ChatBarButtonProps {
|
||||
children: ReactNode;
|
||||
tooltip: string;
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
|
||||
buttonProps?: Omit<HTMLProps<HTMLButtonElement>, "size" | "onClick" | "onContextMenu">;
|
||||
}
|
||||
export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
|
||||
return (
|
||||
<Tooltip text={props.tooltip}>
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<div className={`expression-picker-chat-input-button ${ChannelTextAreaClasses?.buttonContainer ?? ""} vc-chatbar-button`}>
|
||||
<Button
|
||||
aria-label={props.tooltip}
|
||||
size=""
|
||||
look={ButtonLooks.BLANK}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
innerClassName={`${ButtonWrapperClasses.button} ${ChannelTextAreaClasses?.button}`}
|
||||
onClick={props.onClick}
|
||||
onContextMenu={props.onContextMenu}
|
||||
{...props.buttonProps}
|
||||
>
|
||||
<div className={ButtonWrapperClasses.buttonWrapper}>
|
||||
{props.children}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}, { noop: true });
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { mergeDefaults } from "@utils/misc";
|
||||
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { MessageActions, SnowflakeUtils } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
|
|
@ -17,22 +17,20 @@
|
|||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { Menu, React } from "@webpack/common";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
type ContextMenuPatchCallbackReturn = (() => void) | void;
|
||||
/**
|
||||
* @param children The rendered context menu elements
|
||||
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
|
||||
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
|
||||
*/
|
||||
export type NavContextMenuPatchCallback = (children: Array<ReactElement | null>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
|
||||
export type NavContextMenuPatchCallback = (children: Array<ReactElement | null>, ...args: Array<any>) => void;
|
||||
/**
|
||||
* @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<ReactElement | null>, ...args: Array<any>) => void;
|
||||
|
||||
const ContextMenuLogger = new Logger("ContextMenu");
|
||||
|
||||
|
@ -93,14 +91,19 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba
|
|||
* @param id The id of the child. If an array is specified, all ids will be tried
|
||||
* @param children The context menu children
|
||||
*/
|
||||
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null>, _itemsArray?: Array<ReactElement | null>): Array<ReactElement | null> | null {
|
||||
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null>): Array<ReactElement | null> | null {
|
||||
for (const child of children) {
|
||||
if (child == null) continue;
|
||||
|
||||
if (Array.isArray(child)) {
|
||||
const found = findGroupChildrenByChildId(id, child);
|
||||
if (found !== null) return found;
|
||||
}
|
||||
|
||||
if (
|
||||
(Array.isArray(id) && id.some(id => child.props?.id === id))
|
||||
|| child.props?.id === id
|
||||
) return _itemsArray ?? null;
|
||||
) return children;
|
||||
|
||||
let nextChildren = child.props?.children;
|
||||
if (nextChildren) {
|
||||
|
@ -109,7 +112,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
|
|||
child.props.children = nextChildren;
|
||||
}
|
||||
|
||||
const found = findGroupChildrenByChildId(id, nextChildren, nextChildren);
|
||||
const found = findGroupChildrenByChildId(id, nextChildren);
|
||||
if (found !== null) return found;
|
||||
}
|
||||
}
|
||||
|
@ -126,9 +129,12 @@ interface ContextMenuProps {
|
|||
onClose: (callback: (...args: Array<any>) => any) => void;
|
||||
}
|
||||
|
||||
const patchedMenus = new WeakSet();
|
||||
export function _usePatchContextMenu(props: ContextMenuProps) {
|
||||
props = {
|
||||
...props,
|
||||
children: cloneMenuChildren(props.children),
|
||||
};
|
||||
|
||||
export function _patchContextMenu(props: ContextMenuProps) {
|
||||
props.contextMenuApiArguments ??= [];
|
||||
const contextMenuPatches = navPatches.get(props.navId);
|
||||
|
||||
|
@ -137,8 +143,7 @@ export function _patchContextMenu(props: ContextMenuProps) {
|
|||
if (contextMenuPatches) {
|
||||
for (const patch of contextMenuPatches) {
|
||||
try {
|
||||
const callback = patch(props.children, ...props.contextMenuApiArguments);
|
||||
if (!patchedMenus.has(props)) callback?.();
|
||||
patch(props.children, ...props.contextMenuApiArguments);
|
||||
} catch (err) {
|
||||
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
|
||||
}
|
||||
|
@ -147,12 +152,30 @@ export function _patchContextMenu(props: ContextMenuProps) {
|
|||
|
||||
for (const patch of globalPatches) {
|
||||
try {
|
||||
const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments);
|
||||
if (!patchedMenus.has(props)) callback?.();
|
||||
patch(props.navId, props.children, ...props.contextMenuApiArguments);
|
||||
} catch (err) {
|
||||
ContextMenuLogger.error("Global patch errored,", err);
|
||||
}
|
||||
}
|
||||
|
||||
patchedMenus.add(props);
|
||||
return props;
|
||||
}
|
||||
|
||||
function cloneMenuChildren(obj: ReactElement | Array<ReactElement | null> | null) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(cloneMenuChildren);
|
||||
}
|
||||
|
||||
if (React.isValidElement(obj)) {
|
||||
obj = React.cloneElement(obj);
|
||||
|
||||
if (
|
||||
obj?.props?.children &&
|
||||
(obj.type !== Menu.MenuControlItem || obj.type === Menu.MenuControlItem && obj.props.control != null)
|
||||
) {
|
||||
obj.props.children = cloneMenuChildren(obj.props.children);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ export interface MessageExtra {
|
|||
}
|
||||
|
||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
|
||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void>;
|
||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
|
||||
|
||||
const sendListeners = new Set<SendListener>();
|
||||
const editListeners = new Set<EditListener>();
|
||||
|
@ -84,7 +84,7 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec
|
|||
for (const listener of sendListeners) {
|
||||
try {
|
||||
const result = await listener(channelId, messageObj, extra);
|
||||
if (result && result.cancel === true) {
|
||||
if (result?.cancel) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -97,11 +97,15 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec
|
|||
export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
|
||||
for (const listener of editListeners) {
|
||||
try {
|
||||
await listener(channelId, messageId, messageObj);
|
||||
const result = await listener(channelId, messageId, messageObj);
|
||||
if (result?.cancel) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -100,6 +100,7 @@ export async function showNotification(data: NotificationData) {
|
|||
const n = new Notification(title, {
|
||||
body,
|
||||
icon,
|
||||
// @ts-expect-error ts is drunk
|
||||
image
|
||||
});
|
||||
n.onclick = onClick;
|
||||
|
|
|
@ -21,7 +21,7 @@ import { Settings } from "@api/Settings";
|
|||
import { classNameFactory } from "@api/Styles";
|
||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
||||
import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { DispatchWithoutAction } from "react";
|
||||
|
||||
|
@ -129,7 +129,7 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
|||
richBody={
|
||||
<div className={cl("body")}>
|
||||
{data.body}
|
||||
<Timestamp timestamp={moment(data.timestamp)} className={cl("timestamp")} />
|
||||
<Timestamp timestamp={new Date(data.timestamp)} className={cl("timestamp")} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -16,10 +16,11 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
|
||||
import { localStorage } from "@utils/localStorage";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { mergeDefaults } from "@utils/misc";
|
||||
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||
import { putCloudSettings } from "@utils/settingsSync";
|
||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||
import { React } from "@webpack/common";
|
||||
|
@ -28,7 +29,6 @@ import plugins from "~plugins";
|
|||
|
||||
const logger = new Logger("Settings");
|
||||
export interface Settings {
|
||||
notifyAboutUpdates: boolean;
|
||||
autoUpdate: boolean;
|
||||
autoUpdateNotification: boolean,
|
||||
useQuickCss: boolean;
|
||||
|
@ -52,7 +52,6 @@ export interface Settings {
|
|||
| "under-page"
|
||||
| "window"
|
||||
| undefined;
|
||||
macosTranslucency: boolean | undefined;
|
||||
disableMinSize: boolean;
|
||||
winNativeTitleBar: boolean;
|
||||
plugins: {
|
||||
|
@ -78,8 +77,7 @@ export interface Settings {
|
|||
}
|
||||
|
||||
const DefaultSettings: Settings = {
|
||||
notifyAboutUpdates: true,
|
||||
autoUpdate: false,
|
||||
autoUpdate: true,
|
||||
autoUpdateNotification: true,
|
||||
useQuickCss: true,
|
||||
themeLinks: [],
|
||||
|
@ -88,8 +86,6 @@ const DefaultSettings: Settings = {
|
|||
frameless: false,
|
||||
transparent: false,
|
||||
winCtrlQ: false,
|
||||
// Replaced by macosVibrancyStyle
|
||||
macosTranslucency: undefined,
|
||||
macosVibrancyStyle: undefined,
|
||||
disableMinSize: false,
|
||||
winNativeTitleBar: false,
|
||||
|
@ -110,13 +106,8 @@ const DefaultSettings: Settings = {
|
|||
}
|
||||
};
|
||||
|
||||
try {
|
||||
var settings = JSON.parse(VencordNative.settings.get()) as Settings;
|
||||
mergeDefaults(settings, DefaultSettings);
|
||||
} catch (err) {
|
||||
var settings = mergeDefaults({} as Settings, DefaultSettings);
|
||||
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
|
||||
}
|
||||
const settings = VencordNative.settings.get();
|
||||
mergeDefaults(settings, DefaultSettings);
|
||||
|
||||
const saveSettingsOnFrequentAction = debounce(async () => {
|
||||
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
|
||||
|
@ -125,76 +116,52 @@ const saveSettingsOnFrequentAction = debounce(async () => {
|
|||
}
|
||||
}, 60_000);
|
||||
|
||||
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _paths?: Array<string>; };
|
||||
const subscriptions = new Set<SubscriptionCallback>();
|
||||
|
||||
const proxyCache = {} as Record<string, any>;
|
||||
export const SettingsStore = new SettingsStoreClass(settings, {
|
||||
readOnly: true,
|
||||
getDefaultValue({
|
||||
target,
|
||||
key,
|
||||
path
|
||||
}) {
|
||||
const v = target[key];
|
||||
if (!plugins) return v; // plugins not initialised yet. this means this path was reached by being called on the top level
|
||||
|
||||
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
|
||||
function makeProxy(settings: any, root = settings, path = ""): Settings {
|
||||
return proxyCache[path] ??= new Proxy(settings, {
|
||||
get(target, p: string) {
|
||||
const v = target[p];
|
||||
if (path === "plugins" && key in plugins)
|
||||
return target[key] = {
|
||||
enabled: plugins[key].required ?? plugins[key].enabledByDefault ?? false
|
||||
};
|
||||
|
||||
// using "in" is important in the following cases to properly handle falsy or nullish values
|
||||
if (!(p in target)) {
|
||||
// Return empty for plugins with no settings
|
||||
if (path === "plugins" && p in plugins)
|
||||
return target[p] = makeProxy({
|
||||
enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
|
||||
}, root, `plugins.${p}`);
|
||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||
// the default value.
|
||||
if (path.startsWith("plugins.")) {
|
||||
const plugin = path.slice("plugins.".length);
|
||||
if (plugin in plugins) {
|
||||
const setting = plugins[plugin].options?.[key];
|
||||
if (!setting) return v;
|
||||
|
||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||
// the default value.
|
||||
if (path.startsWith("plugins.")) {
|
||||
const plugin = path.slice("plugins.".length);
|
||||
if (plugin in plugins) {
|
||||
const setting = plugins[plugin].options?.[p];
|
||||
if (!setting) return v;
|
||||
if ("default" in setting)
|
||||
// normal setting with a default value
|
||||
return (target[p] = setting.default);
|
||||
if (setting.type === OptionType.SELECT) {
|
||||
const def = setting.options.find(o => o.default);
|
||||
if (def)
|
||||
target[p] = def.value;
|
||||
return def?.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
if ("default" in setting)
|
||||
// normal setting with a default value
|
||||
return (target[key] = setting.default);
|
||||
|
||||
// Recursively proxy Objects with the updated property path
|
||||
if (typeof v === "object" && !Array.isArray(v) && v !== null)
|
||||
return makeProxy(v, root, `${path}${path && "."}${p}`);
|
||||
|
||||
// primitive or similar, no need to proxy further
|
||||
return v;
|
||||
},
|
||||
|
||||
set(target, p: string, v) {
|
||||
// avoid unnecessary updates to React Components and other listeners
|
||||
if (target[p] === v) return true;
|
||||
|
||||
target[p] = v;
|
||||
// Call any listeners that are listening to a setting of this path
|
||||
const setPath = `${path}${path && "."}${p}`;
|
||||
delete proxyCache[setPath];
|
||||
for (const subscription of subscriptions) {
|
||||
if (!subscription._paths || subscription._paths.includes(setPath)) {
|
||||
subscription(v, setPath);
|
||||
if (setting.type === OptionType.SELECT) {
|
||||
const def = setting.options.find(o => o.default);
|
||||
if (def)
|
||||
target[key] = def.value;
|
||||
return def?.value;
|
||||
}
|
||||
}
|
||||
// And don't forget to persist the settings!
|
||||
PlainSettings.cloud.settingsSyncVersion = Date.now();
|
||||
localStorage.Vencord_settingsDirty = true;
|
||||
saveSettingsOnFrequentAction();
|
||||
VencordNative.settings.set(JSON.stringify(root, null, 4));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return v;
|
||||
}
|
||||
});
|
||||
|
||||
SettingsStore.addGlobalChangeListener((_, path) => {
|
||||
SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
|
||||
localStorage.Vencord_settingsDirty = true;
|
||||
saveSettingsOnFrequentAction();
|
||||
VencordNative.settings.set(SettingsStore.plain, path);
|
||||
});
|
||||
|
||||
/**
|
||||
* Same as {@link Settings} but unproxied. You should treat this as readonly,
|
||||
|
@ -210,7 +177,7 @@ export const PlainSettings = settings;
|
|||
* the updated settings to disk.
|
||||
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}
|
||||
*/
|
||||
export const Settings = makeProxy(settings);
|
||||
export const Settings = SettingsStore.store;
|
||||
|
||||
/**
|
||||
* Settings hook for React components. Returns a smart settings
|
||||
|
@ -223,43 +190,21 @@ export const Settings = makeProxy(settings);
|
|||
export function useSettings(paths?: UseSettings<Settings>[]) {
|
||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||
|
||||
const onUpdate: SubscriptionCallback = paths
|
||||
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
|
||||
: forceUpdate;
|
||||
|
||||
React.useEffect(() => {
|
||||
subscriptions.add(onUpdate);
|
||||
return () => void subscriptions.delete(onUpdate);
|
||||
if (paths) {
|
||||
paths.forEach(p => SettingsStore.addChangeListener(p, forceUpdate));
|
||||
return () => paths.forEach(p => SettingsStore.removeChangeListener(p, forceUpdate));
|
||||
} else {
|
||||
SettingsStore.addGlobalChangeListener(forceUpdate);
|
||||
return () => SettingsStore.removeGlobalChangeListener(forceUpdate);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return Settings;
|
||||
}
|
||||
|
||||
// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
|
||||
type ResolvePropDeep<T, P> = P extends "" ? T :
|
||||
P extends `${infer Pre}.${infer Suf}` ?
|
||||
Pre extends keyof T ? ResolvePropDeep<T[Pre], Suf> : never : P extends keyof T ? T[P] : never;
|
||||
|
||||
/**
|
||||
* Add a settings listener that will be invoked whenever the desired setting is updated
|
||||
* @param path Path to the setting that you want to watch, for example "plugins.Unindent.enabled" will fire your callback
|
||||
* whenever Unindent is toggled. Pass an empty string to get notified for all changes
|
||||
* @param onUpdate Callback function whenever a setting matching path is updated. It gets passed the new value and the path
|
||||
* to the updated setting. This path will be the same as your path argument, unless it was an empty string.
|
||||
*
|
||||
* @example addSettingsListener("", (newValue, path) => console.log(`${path} is now ${newValue}`))
|
||||
* addSettingsListener("plugins.Unindent.enabled", v => console.log("Unindent is now", v ? "enabled" : "disabled"))
|
||||
*/
|
||||
export function addSettingsListener<Path extends keyof Settings>(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void;
|
||||
export function addSettingsListener<Path extends string>(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep<Settings, Path>, path: Path extends "" ? string : Path) => void): void;
|
||||
export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) {
|
||||
if (path)
|
||||
((onUpdate as SubscriptionCallback)._paths ??= []).push(path);
|
||||
subscriptions.add(onUpdate);
|
||||
return SettingsStore.store;
|
||||
}
|
||||
|
||||
export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
||||
const { plugins } = settings;
|
||||
const { plugins } = SettingsStore.plain;
|
||||
if (name in plugins) return;
|
||||
|
||||
for (const oldName of oldNames) {
|
||||
|
@ -267,7 +212,7 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
|||
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
|
||||
plugins[name] = plugins[oldName];
|
||||
delete plugins[oldName];
|
||||
VencordNative.settings.set(JSON.stringify(settings, null, 4));
|
||||
SettingsStore.markAsChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import * as $Badges from "./Badges";
|
||||
import * as $ChatButtons from "./ChatButtons";
|
||||
import * as $Commands from "./Commands";
|
||||
import * as $ContextMenu from "./ContextMenu";
|
||||
import * as $DataStore from "./DataStore";
|
||||
|
@ -104,3 +105,8 @@ export const Notifications = $Notifications;
|
|||
* An api allowing you to patch and add/remove items to/from context menus
|
||||
*/
|
||||
export const ContextMenu = $ContextMenu;
|
||||
|
||||
/**
|
||||
* An API allowing you to add buttons to the chat input
|
||||
*/
|
||||
export const ChatButtons = $ChatButtons;
|
||||
|
|
|
@ -16,10 +16,12 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./ExpandableHeader.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Text, Tooltip, useState } from "@webpack/common";
|
||||
export const cl = classNameFactory("vc-expandableheader-");
|
||||
import "./ExpandableHeader.css";
|
||||
|
||||
const cl = classNameFactory("vc-expandableheader-");
|
||||
|
||||
export interface ExpandableHeaderProps {
|
||||
onMoreClick?: () => void;
|
||||
|
@ -31,7 +33,7 @@ export interface ExpandableHeaderProps {
|
|||
buttons?: React.ReactNode[];
|
||||
}
|
||||
|
||||
export default function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
|
||||
export function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
|
||||
const [showContent, setShowContent] = useState(defaultState);
|
||||
|
||||
return (
|
||||
|
|
|
@ -9,10 +9,12 @@ import "./contributorModal.css";
|
|||
import { useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Link } from "@components/Link";
|
||||
import { DevsById } from "@utils/constants";
|
||||
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
|
||||
import { pluralise } from "@utils/misc";
|
||||
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
||||
import { Forms, MaskedLink, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||
import { Forms, MaskedLink, showToast, Tooltip, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
|
@ -72,6 +74,8 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
||||
}, [user.id, user.username]);
|
||||
|
||||
const ContributedHyperLink = <Link href="https://vencord.dev/source">contributed</Link>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cl("header")}>
|
||||
|
@ -84,30 +88,48 @@ function ContributorModal({ user }: { user: User; }) {
|
|||
|
||||
<div className={cl("links")}>
|
||||
{website && (
|
||||
<MaskedLink
|
||||
href={"https://" + website}
|
||||
>
|
||||
<WebsiteIcon />
|
||||
</MaskedLink>
|
||||
<Tooltip text={website}>
|
||||
{props => (
|
||||
<MaskedLink {...props} href={"https://" + website}>
|
||||
<WebsiteIcon />
|
||||
</MaskedLink>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
{githubName && (
|
||||
<MaskedLink href={`https://github.com/${githubName}`}>
|
||||
<GithubIcon />
|
||||
</MaskedLink>
|
||||
<Tooltip text={githubName}>
|
||||
{props => (
|
||||
<MaskedLink {...props} href={`https://github.com/${githubName}`}>
|
||||
<GithubIcon />
|
||||
</MaskedLink>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cl("plugins")}>
|
||||
{plugins.map(p =>
|
||||
<PluginCard
|
||||
key={p.name}
|
||||
plugin={p}
|
||||
disabled={p.required ?? false}
|
||||
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{plugins.length ? (
|
||||
<Forms.FormText>
|
||||
This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
|
||||
</Forms.FormText>
|
||||
) : (
|
||||
<Forms.FormText>
|
||||
This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
|
||||
</Forms.FormText>
|
||||
)}
|
||||
|
||||
{!!plugins.length && (
|
||||
<div className={cl("plugins")}>
|
||||
{plugins.map(p =>
|
||||
<PluginCard
|
||||
key={p.name}
|
||||
plugin={p}
|
||||
disabled={p.required ?? false}
|
||||
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,11 +25,13 @@
|
|||
display: block;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 16px;
|
||||
width: 32px;
|
||||
background: var(--background-tertiary);
|
||||
z-index: -1;
|
||||
left: -16px;
|
||||
left: -32px;
|
||||
top: 0;
|
||||
border-top-left-radius: 9999px;
|
||||
border-bottom-left-radius: 9999px;
|
||||
}
|
||||
|
||||
.vc-author-modal-avatar {
|
||||
|
@ -55,4 +57,5 @@
|
|||
.vc-author-modal-plugins {
|
||||
display: grid;
|
||||
gap: 0.5em;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import PluginModal from "@components/PluginSettings/PluginModal";
|
|||
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
||||
import { SettingsTab } from "@components/VencordSettings/shared";
|
||||
import { ChangeList } from "@utils/ChangeList";
|
||||
import { proxyLazy } from "@utils/lazy";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes, isObjectEmpty } from "@utils/misc";
|
||||
|
@ -38,8 +39,8 @@ import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextI
|
|||
|
||||
import Plugins from "~plugins";
|
||||
|
||||
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
||||
|
||||
// Avoid circular dependency
|
||||
const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() => require("../../plugins"));
|
||||
|
||||
const cl = classNameFactory("vc-plugins-");
|
||||
const logger = new Logger("PluginSettings", "#a6d189");
|
||||
|
@ -315,7 +316,6 @@ export default function PluginSettings() {
|
|||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
|
||||
<div className={InputStyles.inputWrapper}>
|
||||
<Select
|
||||
className={InputStyles.inputDefault}
|
||||
options={[
|
||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||
|
|
|
@ -21,7 +21,7 @@ import "./addonCard.css";
|
|||
import { classNameFactory } from "@api/Styles";
|
||||
import { Badge } from "@components/Badge";
|
||||
import { Switch } from "@components/Switch";
|
||||
import { Text } from "@webpack/common";
|
||||
import { Text, useRef } from "@webpack/common";
|
||||
import type { MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
const cl = classNameFactory("vc-addon-");
|
||||
|
@ -42,6 +42,8 @@ interface Props {
|
|||
}
|
||||
|
||||
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
|
||||
const titleRef = useRef<HTMLDivElement>(null);
|
||||
const titleContainerRef = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<div
|
||||
className={cl("card", { "card-disabled": disabled })}
|
||||
|
@ -51,7 +53,21 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
|
|||
<div className={cl("header")}>
|
||||
<div className={cl("name-author")}>
|
||||
<Text variant="text-md/bold" className={cl("name")}>
|
||||
{name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||
<div ref={titleContainerRef} className={cl("title-container")}>
|
||||
<div
|
||||
ref={titleRef}
|
||||
className={cl("title")}
|
||||
onMouseOver={() => {
|
||||
const title = titleRef.current!;
|
||||
const titleContainer = titleContainerRef.current!;
|
||||
|
||||
title.style.setProperty("--offset", `${titleContainer.clientWidth - title.scrollWidth}px`);
|
||||
title.style.setProperty("--duration", `${Math.max(0.5, (title.scrollWidth - titleContainer.clientWidth) / 7)}s`);
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</div>{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||
</Text>
|
||||
{!!author && (
|
||||
<Text variant="text-md/normal" className={cl("author")}>
|
||||
|
|
|
@ -39,9 +39,7 @@ function validateUrl(url: string) {
|
|||
async function eraseAllData() {
|
||||
const res = await fetch(new URL("/v1/", getCloudUrl()), {
|
||||
method: "DELETE",
|
||||
headers: new Headers({
|
||||
Authorization: await getCloudAuth()
|
||||
})
|
||||
headers: { Authorization: await getCloudAuth() }
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
|
|
@ -16,15 +16,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||
import { CodeBlock } from "@components/CodeBlock";
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||
import { makeCodeblock } from "@utils/text";
|
||||
import { ReplaceFn } from "@utils/types";
|
||||
import { Patch, 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, TextArea, TextInput } from "@webpack/common";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
|
@ -47,7 +46,7 @@ const findCandidates = debounce(function ({ find, setModule, setError }) {
|
|||
|
||||
interface ReplacementComponentProps {
|
||||
module: [id: number, factory: Function];
|
||||
match: string | RegExp;
|
||||
match: string;
|
||||
replacement: string | ReplaceFn;
|
||||
setReplacementError(error: any): void;
|
||||
}
|
||||
|
@ -58,7 +57,13 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
|||
|
||||
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
||||
const src: string = fact.toString().replaceAll("\n", "");
|
||||
const canonicalMatch = canonicalizeMatch(match);
|
||||
|
||||
try {
|
||||
new RegExp(match);
|
||||
} catch (e) {
|
||||
return ["", [], []];
|
||||
}
|
||||
const canonicalMatch = canonicalizeMatch(new RegExp(match));
|
||||
try {
|
||||
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
||||
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
||||
|
@ -180,7 +185,8 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Forms.FormTitle>replacement</Forms.FormTitle>
|
||||
{/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */}
|
||||
<Forms.FormTitle className="">replacement</Forms.FormTitle>
|
||||
<TextInput
|
||||
value={replacement?.toString()}
|
||||
onChange={onChange}
|
||||
|
@ -188,7 +194,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||
/>
|
||||
{!isFunc && (
|
||||
<div className="vc-text-selectable">
|
||||
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
|
||||
<Forms.FormTitle className={Margins.top8}>Cheat Sheet</Forms.FormTitle>
|
||||
{Object.entries({
|
||||
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
|
||||
"$$": "Insert a $",
|
||||
|
@ -218,8 +224,66 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|||
);
|
||||
}
|
||||
|
||||
interface FullPatchInputProps {
|
||||
setFind(v: string): void;
|
||||
setParsedFind(v: string | RegExp): void;
|
||||
setMatch(v: string): void;
|
||||
setReplacement(v: string | ReplaceFn): void;
|
||||
}
|
||||
|
||||
function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: FullPatchInputProps) {
|
||||
const [fullPatch, setFullPatch] = React.useState<string>("");
|
||||
const [fullPatchError, setFullPatchError] = React.useState<string>("");
|
||||
|
||||
function update() {
|
||||
if (fullPatch === "") {
|
||||
setFullPatchError("");
|
||||
|
||||
setFind("");
|
||||
setParsedFind("");
|
||||
setMatch("");
|
||||
setReplacement("");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = (0, eval)(`(${fullPatch})`) as Patch;
|
||||
|
||||
if (!parsed.find) throw new Error("No 'find' field");
|
||||
if (!parsed.replacement) throw new Error("No 'replacement' field");
|
||||
|
||||
if (parsed.replacement instanceof Array) {
|
||||
if (parsed.replacement.length === 0) throw new Error("Invalid replacement");
|
||||
|
||||
parsed.replacement = {
|
||||
match: parsed.replacement[0].match,
|
||||
replace: parsed.replacement[0].replace
|
||||
};
|
||||
}
|
||||
|
||||
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
|
||||
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
|
||||
|
||||
setFind(parsed.find instanceof RegExp ? parsed.find.toString() : parsed.find);
|
||||
setParsedFind(parsed.find);
|
||||
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
|
||||
setReplacement(parsed.replacement.replace);
|
||||
setFullPatchError("");
|
||||
} catch (e) {
|
||||
setFullPatchError((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<Forms.FormText className={Margins.bottom8}>Paste your full JSON patch here to fill out the fields</Forms.FormText>
|
||||
<TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} />
|
||||
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
|
||||
</>;
|
||||
}
|
||||
|
||||
function PatchHelper() {
|
||||
const [find, setFind] = React.useState<string>("");
|
||||
const [parsedFind, setParsedFind] = React.useState<string | RegExp>("");
|
||||
const [match, setMatch] = React.useState<string>("");
|
||||
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
||||
|
||||
|
@ -227,40 +291,60 @@ function PatchHelper() {
|
|||
|
||||
const [module, setModule] = React.useState<[number, Function]>();
|
||||
const [findError, setFindError] = React.useState<string>();
|
||||
const [matchError, setMatchError] = React.useState<string>();
|
||||
|
||||
const code = React.useMemo(() => {
|
||||
return `
|
||||
{
|
||||
find: ${JSON.stringify(find)},
|
||||
find: ${parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind)},
|
||||
replacement: {
|
||||
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
||||
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
||||
}
|
||||
}
|
||||
`.trim();
|
||||
}, [find, match, replacement]);
|
||||
}, [parsedFind, match, replacement]);
|
||||
|
||||
function onFindChange(v: string) {
|
||||
setFindError(void 0);
|
||||
setFind(v);
|
||||
if (v.length) {
|
||||
findCandidates({ find: v, setModule, setError: setFindError });
|
||||
}
|
||||
}
|
||||
|
||||
function onMatchChange(v: string) {
|
||||
try {
|
||||
new RegExp(v);
|
||||
let parsedFind = v as string | RegExp;
|
||||
if (/^\/.+?\/$/.test(v)) parsedFind = new RegExp(v.slice(1, -1));
|
||||
|
||||
setFindError(void 0);
|
||||
setMatch(v);
|
||||
setParsedFind(parsedFind);
|
||||
|
||||
if (v.length) {
|
||||
findCandidates({ find: parsedFind, setModule, setError: setFindError });
|
||||
}
|
||||
} catch (e: any) {
|
||||
setFindError((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
function onMatchChange(v: string) {
|
||||
setMatch(v);
|
||||
|
||||
try {
|
||||
new RegExp(v);
|
||||
setMatchError(void 0);
|
||||
} catch (e: any) {
|
||||
setMatchError((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsTab title="Patch Helper">
|
||||
<Forms.FormTitle>find</Forms.FormTitle>
|
||||
<Forms.FormTitle>full patch</Forms.FormTitle>
|
||||
<FullPatchInput
|
||||
setFind={onFindChange}
|
||||
setParsedFind={setParsedFind}
|
||||
setMatch={onMatchChange}
|
||||
setReplacement={setReplacement}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle className={Margins.top8}>find</Forms.FormTitle>
|
||||
<TextInput
|
||||
type="text"
|
||||
value={find}
|
||||
|
@ -268,19 +352,15 @@ function PatchHelper() {
|
|||
error={findError}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle>match</Forms.FormTitle>
|
||||
<CheckedTextInput
|
||||
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
|
||||
<TextInput
|
||||
type="text"
|
||||
value={match}
|
||||
onChange={onMatchChange}
|
||||
validate={v => {
|
||||
try {
|
||||
return (new RegExp(v), true);
|
||||
} catch (e) {
|
||||
return (e as Error).message;
|
||||
}
|
||||
}}
|
||||
error={matchError}
|
||||
/>
|
||||
|
||||
<div className={Margins.top8} />
|
||||
<ReplacementInput
|
||||
replacement={replacement}
|
||||
setReplacement={setReplacement}
|
||||
|
@ -291,7 +371,7 @@ function PatchHelper() {
|
|||
{module && (
|
||||
<ReplacementComponent
|
||||
module={module}
|
||||
match={new RegExp(match)}
|
||||
match={match}
|
||||
replacement={replacement}
|
||||
setReplacementError={setReplacementError}
|
||||
/>
|
||||
|
|
|
@ -21,14 +21,16 @@ import { classNameFactory } from "@api/Styles";
|
|||
import { Flex } from "@components/Flex";
|
||||
import { DeleteIcon } from "@components/Icons";
|
||||
import { Link } from "@components/Link";
|
||||
import PluginModal from "@components/PluginSettings/PluginModal";
|
||||
import type { UserThemeHeader } from "@main/themes";
|
||||
import { openInviteModal } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { openModal } from "@utils/modal";
|
||||
import { showItemInFolder } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { findByPropsLazy, findLazy } from "@webpack";
|
||||
import { Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
||||
import { UserThemeHeader } from "main/themes";
|
||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||
|
||||
import { AddonCard } from "./AddonCard";
|
||||
|
@ -248,6 +250,21 @@ function ThemesTab() {
|
|||
>
|
||||
Edit QuickCSS
|
||||
</Button>
|
||||
|
||||
{Vencord.Settings.plugins.ClientTheme.enabled && (
|
||||
<Button
|
||||
onClick={() => openModal(modalProps => (
|
||||
<PluginModal
|
||||
{...modalProps}
|
||||
plugin={Vencord.Plugins.plugins.ClientTheme}
|
||||
onRestartNeeded={() => { }}
|
||||
/>
|
||||
))}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Edit ClientTheme
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</Card>
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import { Flex } from "@components/Flex";
|
|||
import { Link } from "@components/Link";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
|
||||
|
@ -29,7 +30,7 @@ import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@web
|
|||
|
||||
import gitHash from "~git-hash";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
import { handleSettingsTabError, SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
||||
return async () => {
|
||||
|
@ -38,21 +39,24 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
|
|||
await action();
|
||||
} catch (e: any) {
|
||||
UpdateLogger.error("Failed to update", e);
|
||||
|
||||
let err: string;
|
||||
if (!e) {
|
||||
var err = "An unknown error occurred (error is undefined).\nPlease try again.";
|
||||
err = "An unknown error occurred (error is undefined).\nPlease try again.";
|
||||
} else if (e.code && e.cmd) {
|
||||
const { code, path, cmd, stderr } = e;
|
||||
|
||||
if (code === "ENOENT")
|
||||
var err = `Command \`${path}\` not found.\nPlease install it and try again`;
|
||||
err = `Command \`${path}\` not found.\nPlease install it and try again`;
|
||||
else {
|
||||
var err = `An error occurred while running \`${cmd}\`:\n`;
|
||||
err = `An error occurred while running \`${cmd}\`:\n`;
|
||||
err += stderr || `Code \`${code}\`. See the console for more info`;
|
||||
}
|
||||
|
||||
} else {
|
||||
var err = "An unknown error occurred. See the console for more info.";
|
||||
err = "An unknown error occurred. See the console for more info.";
|
||||
}
|
||||
|
||||
Alerts.show({
|
||||
title: "Oops!",
|
||||
body: (
|
||||
|
@ -81,9 +85,12 @@ function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string
|
|||
|
||||
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
|
||||
return (
|
||||
<Card style={{ padding: ".5em" }}>
|
||||
<Card style={{ padding: "0 0.5em" }}>
|
||||
{updates.map(({ hash, author, message }) => (
|
||||
<div>
|
||||
<div style={{
|
||||
marginTop: "0.5em",
|
||||
marginBottom: "0.5em"
|
||||
}}>
|
||||
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
|
||||
<span style={{
|
||||
marginLeft: "0.5em",
|
||||
|
@ -113,7 +120,7 @@ function Updatable(props: CommonProps) {
|
|||
</>
|
||||
) : (
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
|
||||
{isOutdated ? (updates.length === 1 ? "There is 1 Update" : `There are ${updates.length} Updates`) : "Up to Date!"}
|
||||
</Forms.FormText>
|
||||
)}
|
||||
|
||||
|
@ -183,7 +190,7 @@ function Newer(props: CommonProps) {
|
|||
}
|
||||
|
||||
function Updater() {
|
||||
const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
|
||||
const settings = useSettings(["autoUpdate", "autoUpdateNotification"]);
|
||||
|
||||
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
||||
|
||||
|
@ -200,14 +207,6 @@ function Updater() {
|
|||
return (
|
||||
<SettingsTab title="Vencord Updater">
|
||||
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||
<Switch
|
||||
value={settings.notifyAboutUpdates}
|
||||
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||
note="Shows a notification on startup"
|
||||
disabled={settings.autoUpdate}
|
||||
>
|
||||
Get notified about new updates
|
||||
</Switch>
|
||||
<Switch
|
||||
value={settings.autoUpdate}
|
||||
onChange={(v: boolean) => settings.autoUpdate = v}
|
||||
|
@ -250,3 +249,20 @@ function Updater() {
|
|||
}
|
||||
|
||||
export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");
|
||||
|
||||
export const openUpdaterModal = IS_UPDATER_DISABLED ? null : function () {
|
||||
const UpdaterTab = wrapTab(Updater, "Updater");
|
||||
|
||||
try {
|
||||
openModal(wrapTab((modalProps: ModalProps) => (
|
||||
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||
<ModalContent className="vc-updater-modal">
|
||||
<ModalCloseButton onClick={modalProps.onClose} className="vc-updater-modal-close-button" />
|
||||
<UpdaterTab />
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
), "UpdaterModal"));
|
||||
} catch {
|
||||
handleSettingsTabError();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -50,14 +50,6 @@ function VencordSettings() {
|
|||
const isMac = navigator.platform.toLowerCase().startsWith("mac");
|
||||
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
|
||||
|
||||
// One-time migration of the old setting to the new one if necessary.
|
||||
React.useEffect(() => {
|
||||
if (settings.macosTranslucency === true && !settings.macosVibrancyStyle) {
|
||||
settings.macosVibrancyStyle = "sidebar";
|
||||
settings.macosTranslucency = undefined;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const Switches: Array<false | {
|
||||
key: KeysOfType<typeof settings, boolean>;
|
||||
title: string;
|
||||
|
@ -83,10 +75,10 @@ function VencordSettings() {
|
|||
title: "Use Windows' native title bar instead of Discord's custom one",
|
||||
note: "Requires a full restart"
|
||||
}),
|
||||
!IS_WEB && false /* This causes electron to freeze / white screen for some people */ && {
|
||||
!IS_WEB && {
|
||||
key: "transparent",
|
||||
title: "Enable window transparency",
|
||||
note: "Requires a full restart"
|
||||
title: "Enable window transparency.",
|
||||
note: "You need a theme that supports transparency or this will do nothing. Will stop the window from being resizable. Requires a full restart"
|
||||
},
|
||||
!IS_WEB && isWindows && {
|
||||
key: "winCtrlQ",
|
||||
|
@ -164,7 +156,7 @@ function VencordSettings() {
|
|||
options={[
|
||||
// Sorted from most opaque to most transparent
|
||||
{
|
||||
label: "No vibrancy", default: !settings.macosTranslucency, value: undefined
|
||||
label: "No vibrancy", value: undefined
|
||||
},
|
||||
{
|
||||
label: "Under Page (window tinting)",
|
||||
|
@ -191,9 +183,8 @@ function VencordSettings() {
|
|||
value: "header"
|
||||
},
|
||||
{
|
||||
label: "Sidebar (old value for transparent windows)",
|
||||
value: "sidebar",
|
||||
default: settings.macosTranslucency
|
||||
label: "Sidebar",
|
||||
value: "sidebar"
|
||||
},
|
||||
{
|
||||
label: "Tooltip",
|
||||
|
|
|
@ -62,3 +62,36 @@
|
|||
.vc-addon-author::before {
|
||||
content: "by ";
|
||||
}
|
||||
|
||||
.vc-addon-title-container {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: 1.25em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vc-addon-title {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@keyframes vc-addon-title {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(var(--offset));
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.vc-addon-title:hover {
|
||||
overflow: visible;
|
||||
animation: vc-addon-title var(--duration) linear infinite;
|
||||
}
|
||||
|
|
|
@ -65,3 +65,11 @@
|
|||
/* discord also sets cursor: default which prevents the cursor from showing as text */
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
.vc-updater-modal {
|
||||
padding: 1.5em !important;
|
||||
}
|
||||
|
||||
.vc-updater-modal-close-button {
|
||||
float: right;
|
||||
}
|
||||
|
|
|
@ -42,11 +42,11 @@ export function SettingsTab({ title, children }: PropsWithChildren<{ title: stri
|
|||
);
|
||||
}
|
||||
|
||||
const onError = onlyOnce(handleComponentFailed);
|
||||
export const handleSettingsTabError = onlyOnce(handleComponentFailed);
|
||||
|
||||
export function wrapTab(component: ComponentType, tab: string) {
|
||||
export function wrapTab(component: ComponentType<any>, tab: string) {
|
||||
return ErrorBoundary.wrap(component, {
|
||||
message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`,
|
||||
onError,
|
||||
onError: handleSettingsTabError,
|
||||
});
|
||||
}
|
||||
|
|
18
src/components/index.ts
Normal file
18
src/components/index.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export * from "./Badge";
|
||||
export * from "./CheckedTextInput";
|
||||
export * from "./CodeBlock";
|
||||
export * from "./DonateButton";
|
||||
export { default as ErrorBoundary } from "./ErrorBoundary";
|
||||
export * from "./ErrorCard";
|
||||
export * from "./ExpandableHeader";
|
||||
export * from "./Flex";
|
||||
export * from "./Heart";
|
||||
export * from "./Icons";
|
||||
export * from "./Link";
|
||||
export * from "./Switch";
|
|
@ -19,7 +19,8 @@
|
|||
import { app, protocol, session } from "electron";
|
||||
import { join } from "path";
|
||||
|
||||
import { ensureSafePath, getSettings } from "./ipcMain";
|
||||
import { ensureSafePath } from "./ipcMain";
|
||||
import { RendererSettings } from "./settings";
|
||||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||
import { installExt } from "./utils/extensions";
|
||||
|
||||
|
@ -55,7 +56,7 @@ if (IS_VESKTOP || !IS_VANILLA) {
|
|||
});
|
||||
|
||||
try {
|
||||
if (getSettings().enableReactDevtools)
|
||||
if (RendererSettings.store.enableReactDevtools)
|
||||
installExt("fmkadmapgofadopljbjfkapdkoienihi")
|
||||
.then(() => console.info("[Vencord] Installed React Developer Tools"))
|
||||
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
|
||||
|
|
|
@ -18,22 +18,21 @@
|
|||
|
||||
import "./updater";
|
||||
import "./ipcPlugins";
|
||||
import "./settings";
|
||||
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { IpcEvents } from "@utils/IpcEvents";
|
||||
import { Queue } from "@utils/Queue";
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { IpcEvents } from "@shared/IpcEvents";
|
||||
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
|
||||
import { mkdirSync, readFileSync, watch } from "fs";
|
||||
import { open, readdir, readFile, writeFile } from "fs/promises";
|
||||
import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
|
||||
import { open, readdir, readFile } from "fs/promises";
|
||||
import { join, normalize } from "path";
|
||||
|
||||
import monacoHtml from "~fileContent/monacoWin.html;base64";
|
||||
|
||||
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants";
|
||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
|
||||
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
||||
|
||||
mkdirSync(SETTINGS_DIR, { recursive: true });
|
||||
mkdirSync(THEMES_DIR, { recursive: true });
|
||||
|
||||
export function ensureSafePath(basePath: string, path: string) {
|
||||
|
@ -71,22 +70,6 @@ function getThemeData(fileName: string) {
|
|||
return readFile(safePath, "utf-8");
|
||||
}
|
||||
|
||||
export function readSettings() {
|
||||
try {
|
||||
return readFileSync(SETTINGS_FILE, "utf-8");
|
||||
} catch {
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
export function getSettings(): typeof import("@api/Settings").Settings {
|
||||
try {
|
||||
return JSON.parse(readSettings());
|
||||
} catch {
|
||||
return {} as any;
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
|
||||
|
||||
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
||||
|
@ -101,12 +84,10 @@ ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
|||
shell.openExternal(url);
|
||||
});
|
||||
|
||||
const cssWriteQueue = new Queue();
|
||||
const settingsWriteQueue = new Queue();
|
||||
|
||||
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
||||
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
|
||||
cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css))
|
||||
writeFileSync(QUICKCSS_PATH, css)
|
||||
);
|
||||
|
||||
ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR);
|
||||
|
@ -117,30 +98,37 @@ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
|
|||
"os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}`
|
||||
}));
|
||||
|
||||
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
||||
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
|
||||
|
||||
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
|
||||
settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s));
|
||||
});
|
||||
|
||||
|
||||
export function initIpc(mainWindow: BrowserWindow) {
|
||||
let quickCssWatcher: FSWatcher | undefined;
|
||||
|
||||
open(QUICKCSS_PATH, "a+").then(fd => {
|
||||
fd.close();
|
||||
watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
|
||||
quickCssWatcher = watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
|
||||
mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
|
||||
}, 50));
|
||||
});
|
||||
}).catch(() => { });
|
||||
|
||||
watch(THEMES_DIR, { persistent: false }, debounce(() => {
|
||||
const themesWatcher = watch(THEMES_DIR, { persistent: false }, debounce(() => {
|
||||
mainWindow.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0);
|
||||
}));
|
||||
|
||||
mainWindow.once("closed", () => {
|
||||
quickCssWatcher?.close();
|
||||
themesWatcher.close();
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
||||
const title = "Vencord QuickCSS Editor";
|
||||
const existingWindow = BrowserWindow.getAllWindows().find(w => w.title === title);
|
||||
if (existingWindow && !existingWindow.isDestroyed()) {
|
||||
existingWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const win = new BrowserWindow({
|
||||
title: "Vencord QuickCSS Editor",
|
||||
title,
|
||||
autoHideMenuBar: true,
|
||||
darkTheme: true,
|
||||
webPreferences: {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { IpcEvents } from "@utils/IpcEvents";
|
||||
import { IpcEvents } from "@shared/IpcEvents";
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import PluginNatives from "~pluginNatives";
|
||||
|
|
|
@ -16,11 +16,12 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { onceDefined } from "@utils/onceDefined";
|
||||
import { onceDefined } from "@shared/onceDefined";
|
||||
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
import { getSettings, initIpc } from "./ipcMain";
|
||||
import { initIpc } from "./ipcMain";
|
||||
import { RendererSettings } from "./settings";
|
||||
import { IS_VANILLA } from "./utils/constants";
|
||||
|
||||
console.log("[Vencord] Starting up...");
|
||||
|
@ -41,8 +42,7 @@ require.main!.filename = join(asarPath, discordPkg.main);
|
|||
app.setAppPath(asarPath);
|
||||
|
||||
if (!IS_VANILLA) {
|
||||
const settings = getSettings();
|
||||
|
||||
const settings = RendererSettings.store;
|
||||
// Repatch after host updates on Windows
|
||||
if (process.platform === "win32") {
|
||||
require("./patchWin32Updater");
|
||||
|
@ -73,25 +73,25 @@ if (!IS_VANILLA) {
|
|||
const original = options.webPreferences.preload;
|
||||
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
|
||||
options.webPreferences.sandbox = false;
|
||||
// work around discord unloading when in background
|
||||
options.webPreferences.backgroundThrottling = false;
|
||||
|
||||
if (settings.frameless) {
|
||||
options.frame = false;
|
||||
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
|
||||
delete options.frame;
|
||||
}
|
||||
|
||||
// This causes electron to freeze / white screen for some people
|
||||
if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) {
|
||||
if (settings.transparent) {
|
||||
options.transparent = true;
|
||||
options.backgroundColor = "#00000000";
|
||||
}
|
||||
|
||||
const needsVibrancy = process.platform === "darwin" || (settings.macosVibrancyStyle || settings.macosTranslucency);
|
||||
const needsVibrancy = process.platform === "darwin" && settings.macosVibrancyStyle;
|
||||
|
||||
if (needsVibrancy) {
|
||||
options.backgroundColor = "#00000000";
|
||||
if (settings.macosTranslucency) {
|
||||
options.vibrancy = "sidebar";
|
||||
} else if (settings.macosVibrancyStyle) {
|
||||
if (settings.macosVibrancyStyle) {
|
||||
options.vibrancy = settings.macosVibrancyStyle;
|
||||
}
|
||||
}
|
||||
|
@ -130,6 +130,18 @@ if (!IS_VANILLA) {
|
|||
});
|
||||
|
||||
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
||||
|
||||
// Monkey patch commandLine to disable WidgetLayering: Fix DevTools context menus https://github.com/electron/electron/issues/38790
|
||||
const originalAppend = app.commandLine.appendSwitch;
|
||||
app.commandLine.appendSwitch = function (...args) {
|
||||
if (args[0] === "disable-features" && !args[1]?.includes("WidgetLayering")) {
|
||||
args[1] += ",WidgetLayering";
|
||||
}
|
||||
return originalAppend.apply(this, args);
|
||||
};
|
||||
|
||||
// Work around discord unloading when in background
|
||||
app.commandLine.appendSwitch("disable-renderer-backgrounding");
|
||||
} else {
|
||||
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
||||
}
|
||||
|
|
69
src/main/settings.ts
Normal file
69
src/main/settings.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Settings } from "@api/Settings";
|
||||
import { IpcEvents } from "@shared/IpcEvents";
|
||||
import { SettingsStore } from "@shared/SettingsStore";
|
||||
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||
import { ipcMain } from "electron";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
|
||||
import { NATIVE_SETTINGS_FILE, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants";
|
||||
|
||||
mkdirSync(SETTINGS_DIR, { recursive: true });
|
||||
|
||||
function readSettings<T = object>(name: string, file: string): Partial<T> {
|
||||
try {
|
||||
return JSON.parse(readFileSync(file, "utf-8"));
|
||||
} catch (err: any) {
|
||||
if (err?.code !== "ENOENT")
|
||||
console.error(`Failed to read ${name} settings`, err);
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export const RendererSettings = new SettingsStore(readSettings<Settings>("renderer", SETTINGS_FILE));
|
||||
|
||||
RendererSettings.addGlobalChangeListener(() => {
|
||||
try {
|
||||
writeFileSync(SETTINGS_FILE, JSON.stringify(RendererSettings.plain, null, 4));
|
||||
} catch (e) {
|
||||
console.error("Failed to write renderer settings", e);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
||||
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = RendererSettings.plain);
|
||||
|
||||
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string) => {
|
||||
RendererSettings.setData(data, pathToNotify);
|
||||
});
|
||||
|
||||
export interface NativeSettings {
|
||||
plugins: {
|
||||
[plugin: string]: {
|
||||
[setting: string]: any;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const DefaultNativeSettings: NativeSettings = {
|
||||
plugins: {}
|
||||
};
|
||||
|
||||
const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE);
|
||||
mergeDefaults(nativeSettings, DefaultNativeSettings);
|
||||
|
||||
export const NativeSettings = new SettingsStore(nativeSettings);
|
||||
|
||||
NativeSettings.addGlobalChangeListener(() => {
|
||||
try {
|
||||
writeFileSync(NATIVE_SETTINGS_FILE, JSON.stringify(NativeSettings.plain, null, 4));
|
||||
} catch (e) {
|
||||
console.error("Failed to write native settings", e);
|
||||
}
|
||||
});
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { IpcEvents } from "@utils/IpcEvents";
|
||||
import { IpcEvents } from "@shared/IpcEvents";
|
||||
import { execFile as cpExecFile } from "child_process";
|
||||
import { ipcMain } from "electron";
|
||||
import { join } from "path";
|
||||
|
@ -28,7 +28,7 @@ const VENCORD_SRC_DIR = join(__dirname, "..");
|
|||
|
||||
const execFile = promisify(cpExecFile);
|
||||
|
||||
const isFlatpak = process.platform === "linux" && Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
|
||||
const isFlatpak = process.platform === "linux" && !!process.env.FLATPAK_ID;
|
||||
|
||||
if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`;
|
||||
|
||||
|
@ -49,15 +49,19 @@ async function getRepo() {
|
|||
async function calculateGitChanges() {
|
||||
await git("fetch");
|
||||
|
||||
const branch = await git("branch", "--show-current");
|
||||
const branch = (await git("branch", "--show-current")).stdout.trim();
|
||||
|
||||
const res = await git("log", `HEAD...origin/${branch.stdout.trim()}`, "--pretty=format:%an/%h/%s");
|
||||
const existsOnOrigin = (await git("ls-remote", "origin", branch)).stdout.length > 0;
|
||||
if (!existsOnOrigin) return [];
|
||||
|
||||
const res = await git("log", `HEAD...origin/${branch}`, "--pretty=format:%an/%h/%s");
|
||||
|
||||
const commits = res.stdout.trim();
|
||||
return commits ? commits.split("\n").map(line => {
|
||||
const [author, hash, ...rest] = line.split("/");
|
||||
return {
|
||||
hash, author, message: rest.join("/")
|
||||
hash, author,
|
||||
message: rest.join("/").split("\n")[0]
|
||||
};
|
||||
}) : [];
|
||||
}
|
||||
|
@ -73,6 +77,8 @@ async function build() {
|
|||
const command = isFlatpak ? "flatpak-spawn" : "node";
|
||||
const args = isFlatpak ? ["--host", "node", "scripts/build/build.mjs"] : ["scripts/build/build.mjs"];
|
||||
|
||||
if (IS_DEV) args.push("--dev");
|
||||
|
||||
const res = await execFile(command, args, opts);
|
||||
|
||||
return !res.stderr.includes("Build failed");
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { VENCORD_USER_AGENT } from "@utils/constants";
|
||||
import { IpcEvents } from "@utils/IpcEvents";
|
||||
import { IpcEvents } from "@shared/IpcEvents";
|
||||
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
|
||||
import { ipcMain } from "electron";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
@ -53,7 +53,7 @@ async function calculateGitChanges() {
|
|||
// github api only sends the long sha
|
||||
hash: c.sha.slice(0, 7),
|
||||
author: c.author.login,
|
||||
message: c.commit.message
|
||||
message: c.commit.message.split("\n")[0]
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -28,12 +28,14 @@ export const SETTINGS_DIR = join(DATA_DIR, "settings");
|
|||
export const THEMES_DIR = join(DATA_DIR, "themes");
|
||||
export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
|
||||
export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
|
||||
export const NATIVE_SETTINGS_FILE = join(SETTINGS_DIR, "native-settings.json");
|
||||
export const ALLOWED_PROTOCOLS = [
|
||||
"https:",
|
||||
"http:",
|
||||
"steam:",
|
||||
"spotify:",
|
||||
"com.epicgames.launcher:",
|
||||
"tidal:"
|
||||
];
|
||||
|
||||
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");
|
||||
|
|
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;
|
||||
}
|
||||
|
||||
|
|
3
src/plugins/_api/badges/fixBadgeOverflow.css
Normal file
3
src/plugins/_api/badges/fixBadgeOverflow.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
[class*="profileBadges"] {
|
||||
flex: none;
|
||||
}
|
|
@ -16,11 +16,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./fixBadgeOverflow.css";
|
||||
|
||||
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 { openContributorModal } from "@components/PluginSettings/ContributorModal";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { isPluginDev } from "@utils/misc";
|
||||
|
@ -34,14 +37,8 @@ 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"
|
||||
onClick: (_, { user }) => openContributorModal(user)
|
||||
};
|
||||
|
||||
let DonorBadges = {} as Record<string, Array<Record<"tooltip" | "badge", string>>>;
|
||||
|
@ -65,7 +62,7 @@ export default definePlugin({
|
|||
patches: [
|
||||
/* Patch the badge list component on user profiles */
|
||||
{
|
||||
find: "Messages.PROFILE_USER_BADGES,role:",
|
||||
find: 'id:"premium",',
|
||||
replacement: [
|
||||
{
|
||||
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
|
||||
|
@ -79,13 +76,13 @@ export default definePlugin({
|
|||
},
|
||||
// replace their component with ours if applicable
|
||||
{
|
||||
match: /(?<=text:(\i)\.description,spacing:12,)children:/,
|
||||
match: /(?<=text:(\i)\.description,spacing:12,.{0,50})children:/,
|
||||
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) :"
|
||||
},
|
||||
// conditionally override their onClick with badge.onClick if it exists
|
||||
{
|
||||
match: /href:(\i)\.link/,
|
||||
replace: "...($1.onClick && { onClick: $1.onClick }),$&"
|
||||
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, arguments[0]) }),$&"
|
||||
}
|
||||
]
|
||||
}
|
22
src/plugins/_api/chatButtons.ts
Normal file
22
src/plugins/_api/chatButtons.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "ChatInputButtonAPI",
|
||||
description: "API to add buttons to the chat input",
|
||||
authors: [Devs.Ven],
|
||||
|
||||
patches: [{
|
||||
find: '"sticker")',
|
||||
replacement: {
|
||||
match: /!\i\.isMobile(?=.+?(\i)\.push\(.{0,50}"gift")/,
|
||||
replace: "$& &&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)"
|
||||
}
|
||||
}]
|
||||
});
|
|
@ -22,15 +22,15 @@ import definePlugin from "@utils/types";
|
|||
export default definePlugin({
|
||||
name: "ContextMenuAPI",
|
||||
description: "API for adding/removing items to/from context menus.",
|
||||
authors: [Devs.Nuckyz, Devs.Ven],
|
||||
authors: [Devs.Nuckyz, Devs.Ven, Devs.Kyuuhachi],
|
||||
required: true,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "♫ (つ。◕‿‿◕。)つ ♪",
|
||||
replacement: {
|
||||
match: /let{navId:/,
|
||||
replace: "Vencord.Api.ContextMenu._patchContextMenu(arguments[0]);$&"
|
||||
match: /(?=let{navId:)(?<=function \i\((\i)\).+?)/,
|
||||
replace: "$1=Vencord.Api.ContextMenu._usePatchContextMenu($1);"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -31,7 +31,7 @@ export default definePlugin({
|
|||
match: /let\{[^}]*lostPermissionTooltipText:\i[^}]*\}=(\i),/,
|
||||
replace: "$&vencordProps=$1,"
|
||||
}, {
|
||||
match: /decorators:.{0,100}?children:\[/,
|
||||
match: /\.Messages\.GUILD_OWNER(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/,
|
||||
replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -25,14 +25,17 @@ export default definePlugin({
|
|||
authors: [Devs.Arjix, Devs.hunt, Devs.Ven],
|
||||
patches: [
|
||||
{
|
||||
find: '"MessageActionCreators"',
|
||||
find: ".Messages.EDIT_TEXTAREA_HELP",
|
||||
replacement: {
|
||||
match: /async editMessage\(.+?\)\{/,
|
||||
replace: "$&await Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
|
||||
match: /(?<=,channel:\i\}\)\.then\().+?(?=return \i\.content!==this\.props\.message\.content&&\i\((.+?)\))/,
|
||||
replace: (match, args) => "" +
|
||||
`async ${match}` +
|
||||
`if(await Vencord.Api.MessageEvents._handlePreEdit(${args}))` +
|
||||
"return Promise.resolve({shoudClear:true,shouldRefocus:true});"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".handleSendMessage=",
|
||||
find: ".handleSendMessage",
|
||||
replacement: {
|
||||
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
|
||||
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)
|
||||
|
|
|
@ -26,7 +26,7 @@ export default definePlugin({
|
|||
required: true,
|
||||
patches: [
|
||||
{
|
||||
find: 'displayName="NoticeStore"',
|
||||
find: '"NoticeStore"',
|
||||
replacement: [
|
||||
{
|
||||
match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
|
||||
|
|
|
@ -16,17 +16,31 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
disableAnalytics: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Disable Discord's tracking (analytics/'science')",
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "NoTrack",
|
||||
description: "Disable Discord's tracking ('science'), metrics and Sentry crash reporting",
|
||||
description: "Disable Discord's tracking (analytics/'science'), metrics and Sentry crash reporting",
|
||||
authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz, Devs.Arrow],
|
||||
required: true,
|
||||
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "AnalyticsActionHandlers.handle",
|
||||
predicate: () => settings.store.disableAnalytics,
|
||||
replacement: {
|
||||
match: /^.+$/,
|
||||
replace: "()=>{}",
|
||||
|
@ -44,11 +58,11 @@ export default definePlugin({
|
|||
replacement: [
|
||||
{
|
||||
match: /this\._intervalId=/,
|
||||
replace: "this._intervalId=undefined&&"
|
||||
replace: "this._intervalId=void 0&&"
|
||||
},
|
||||
{
|
||||
match: /(increment\(\i\){)/,
|
||||
replace: "$1return;"
|
||||
match: /(?:increment|distribution)\(\i(?:,\i)?\){/g,
|
||||
replace: "$&return;"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -16,70 +16,92 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addContextMenuPatch } from "@api/ContextMenu";
|
||||
import { Settings } from "@api/Settings";
|
||||
import BackupAndRestoreTab from "@components/VencordSettings/BackupAndRestoreTab";
|
||||
import CloudTab from "@components/VencordSettings/CloudTab";
|
||||
import PatchHelperTab from "@components/VencordSettings/PatchHelperTab";
|
||||
import PluginsTab from "@components/VencordSettings/PluginsTab";
|
||||
import ThemesTab from "@components/VencordSettings/ThemesTab";
|
||||
import UpdaterTab from "@components/VencordSettings/UpdaterTab";
|
||||
import VencordTab from "@components/VencordSettings/VencordTab";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { React, SettingsRouter } from "@webpack/common";
|
||||
import { i18n, React } from "@webpack/common";
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
|
||||
type SectionType = "HEADER" | "DIVIDER" | "CUSTOM";
|
||||
type SectionTypes = Record<SectionType, SectionType>;
|
||||
|
||||
export default definePlugin({
|
||||
name: "Settings",
|
||||
description: "Adds Settings UI and debug info",
|
||||
authors: [Devs.Ven, Devs.Megu],
|
||||
required: true,
|
||||
|
||||
start() {
|
||||
// The settings shortcuts in the user settings cog context menu
|
||||
// read the elements from a hardcoded map which for obvious reason
|
||||
// doesn't contain our sections. This patches the actions of our
|
||||
// sections to manually use SettingsRouter (which only works on desktop
|
||||
// but the context menu is usually not available on mobile anyway)
|
||||
addContextMenuPatch("user-settings-cog", children => () => {
|
||||
const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any;
|
||||
section?.forEach(c => {
|
||||
const id = c?.props?.id;
|
||||
if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) {
|
||||
c.props.action = () => SettingsRouter.open(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
patches: [{
|
||||
find: ".versionHash",
|
||||
replacement: [
|
||||
{
|
||||
match: /\[\(0,.{1,3}\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
|
||||
replace: (m, component, props) => {
|
||||
props = props.replace(/children:\[.+\]/, "");
|
||||
return `${m},Vencord.Plugins.plugins.Settings.makeInfoElements(${component}, ${props})`;
|
||||
patches: [
|
||||
{
|
||||
find: ".versionHash",
|
||||
replacement: [
|
||||
{
|
||||
match: /\[\(0,\i\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
|
||||
replace: (m, component, props) => {
|
||||
props = props.replace(/children:\[.+\]/, "");
|
||||
return `${m},$self.makeInfoElements(${component}, ${props})`;
|
||||
}
|
||||
},
|
||||
{
|
||||
match: /copyValue:\i\.join\(" "\)/,
|
||||
replace: "$& + $self.getInfoString()"
|
||||
}
|
||||
]
|
||||
},
|
||||
// Discord Stable
|
||||
// FIXME: remove once change merged to stable
|
||||
{
|
||||
find: "Messages.ACTIVITY_SETTINGS",
|
||||
replacement: {
|
||||
get match() {
|
||||
switch (Settings.plugins.Settings.settingsLocation) {
|
||||
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS/;
|
||||
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS/;
|
||||
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/;
|
||||
case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
|
||||
case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
|
||||
case "aboveActivity":
|
||||
default:
|
||||
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS/;
|
||||
}
|
||||
},
|
||||
replace: "...$self.makeSettingsCategories($1),$&"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "Messages.ACTIVITY_SETTINGS",
|
||||
replacement: {
|
||||
match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
|
||||
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "useDefaultUserSettingsSections:function",
|
||||
replacement: {
|
||||
match: /(?<=useDefaultUserSettingsSections:function\(\){return )(\i)\}/,
|
||||
replace: "$self.wrapSettingsHook($1)}"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
||||
replacement: {
|
||||
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
|
||||
replace: "$2.default.open($1);return;"
|
||||
}
|
||||
]
|
||||
}, {
|
||||
find: "Messages.ACTIVITY_SETTINGS",
|
||||
replacement: {
|
||||
get match() {
|
||||
switch (Settings.plugins.Settings.settingsLocation) {
|
||||
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS\}/;
|
||||
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS\}/;
|
||||
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS\}/;
|
||||
case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
|
||||
case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
|
||||
case "aboveActivity":
|
||||
default:
|
||||
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS\}/;
|
||||
}
|
||||
},
|
||||
replace: "...$self.makeSettingsCategories($1),$&"
|
||||
}
|
||||
}],
|
||||
],
|
||||
|
||||
customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[],
|
||||
customSections: [] as ((SectionTypes: SectionTypes) => any)[],
|
||||
|
||||
makeSettingsCategories(SectionTypes: Record<string, unknown>) {
|
||||
makeSettingsCategories(SectionTypes: SectionTypes) {
|
||||
return [
|
||||
{
|
||||
section: SectionTypes.HEADER,
|
||||
|
@ -89,43 +111,43 @@ export default definePlugin({
|
|||
{
|
||||
section: "VencordSettings",
|
||||
label: "Vencord",
|
||||
element: require("@components/VencordSettings/VencordTab").default,
|
||||
element: VencordTab,
|
||||
className: "vc-settings"
|
||||
},
|
||||
{
|
||||
section: "VencordPlugins",
|
||||
label: "Plugins",
|
||||
element: require("@components/VencordSettings/PluginsTab").default,
|
||||
element: PluginsTab,
|
||||
className: "vc-plugins"
|
||||
},
|
||||
{
|
||||
section: "VencordThemes",
|
||||
label: "Themes",
|
||||
element: require("@components/VencordSettings/ThemesTab").default,
|
||||
element: ThemesTab,
|
||||
className: "vc-themes"
|
||||
},
|
||||
!IS_UPDATER_DISABLED && {
|
||||
section: "VencordUpdater",
|
||||
label: "Updater",
|
||||
element: require("@components/VencordSettings/UpdaterTab").default,
|
||||
element: UpdaterTab,
|
||||
className: "vc-updater"
|
||||
},
|
||||
{
|
||||
section: "VencordCloud",
|
||||
label: "Cloud",
|
||||
element: require("@components/VencordSettings/CloudTab").default,
|
||||
element: CloudTab,
|
||||
className: "vc-cloud"
|
||||
},
|
||||
{
|
||||
section: "VencordSettingsSync",
|
||||
label: "Backup & Restore",
|
||||
element: require("@components/VencordSettings/BackupAndRestoreTab").default,
|
||||
element: BackupAndRestoreTab,
|
||||
className: "vc-backup-restore"
|
||||
},
|
||||
IS_DEV && {
|
||||
section: "VencordPatchHelper",
|
||||
label: "Patch Helper",
|
||||
element: require("@components/VencordSettings/PatchHelperTab").default,
|
||||
element: PatchHelperTab,
|
||||
className: "vc-patch-helper"
|
||||
},
|
||||
...this.customSections.map(func => func(SectionTypes)),
|
||||
|
@ -135,19 +157,63 @@ export default definePlugin({
|
|||
].filter(Boolean);
|
||||
},
|
||||
|
||||
isRightSpot({ header, settings }: { header?: string; settings?: string[]; }) {
|
||||
const firstChild = settings?.[0];
|
||||
// lowest two elements... sanity backup
|
||||
if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true;
|
||||
|
||||
const { settingsLocation } = Settings.plugins.Settings;
|
||||
|
||||
if (settingsLocation === "bottom") return firstChild === "LOGOUT";
|
||||
if (settingsLocation === "belowActivity") return firstChild === "CHANGELOG";
|
||||
|
||||
if (!header) return;
|
||||
|
||||
const names = {
|
||||
top: i18n.Messages.USER_SETTINGS,
|
||||
aboveNitro: i18n.Messages.BILLING_SETTINGS,
|
||||
belowNitro: i18n.Messages.APP_SETTINGS,
|
||||
aboveActivity: i18n.Messages.ACTIVITY_SETTINGS
|
||||
};
|
||||
return header === names[settingsLocation];
|
||||
},
|
||||
|
||||
patchedSettings: new WeakSet(),
|
||||
|
||||
addSettings(elements: any[], element: { header?: string; settings: string[]; }, sectionTypes: SectionTypes) {
|
||||
if (this.patchedSettings.has(elements) || !this.isRightSpot(element)) return;
|
||||
|
||||
this.patchedSettings.add(elements);
|
||||
|
||||
elements.push(...this.makeSettingsCategories(sectionTypes));
|
||||
},
|
||||
|
||||
wrapSettingsHook(originalHook: (...args: any[]) => Record<string, unknown>[]) {
|
||||
return (...args: any[]) => {
|
||||
const elements = originalHook(...args);
|
||||
if (!this.patchedSettings.has(elements))
|
||||
elements.unshift(...this.makeSettingsCategories({
|
||||
HEADER: "HEADER",
|
||||
DIVIDER: "DIVIDER",
|
||||
CUSTOM: "CUSTOM"
|
||||
}));
|
||||
|
||||
return elements;
|
||||
};
|
||||
},
|
||||
|
||||
options: {
|
||||
settingsLocation: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Where to put the Vencord settings section",
|
||||
options: [
|
||||
{ label: "At the very top", value: "top" },
|
||||
{ label: "Above the Nitro section", value: "aboveNitro" },
|
||||
{ label: "Above the Nitro section", value: "aboveNitro", default: true },
|
||||
{ label: "Below the Nitro section", value: "belowNitro" },
|
||||
{ label: "Above Activity Settings", value: "aboveActivity", default: true },
|
||||
{ label: "Above Activity Settings", value: "aboveActivity" },
|
||||
{ label: "Below Activity Settings", value: "belowActivity" },
|
||||
{ label: "At the very bottom", value: "bottom" },
|
||||
],
|
||||
restartNeeded: true
|
||||
]
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -174,15 +240,24 @@ export default definePlugin({
|
|||
return "";
|
||||
},
|
||||
|
||||
makeInfoElements(Component: React.ComponentType<React.PropsWithChildren>, props: React.PropsWithChildren) {
|
||||
getInfoRows() {
|
||||
const { electronVersion, chromiumVersion, additionalInfo } = this;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Component {...props}>Vencord {gitHash}{additionalInfo}</Component>
|
||||
{electronVersion && <Component {...props}>Electron {electronVersion}</Component>}
|
||||
{chromiumVersion && <Component {...props}>Chromium {chromiumVersion}</Component>}
|
||||
</>
|
||||
const rows = [`Vencord ${gitHash}${additionalInfo}`];
|
||||
|
||||
if (electronVersion) rows.push(`Electron ${electronVersion}`);
|
||||
if (chromiumVersion) rows.push(`Chromium ${chromiumVersion}`);
|
||||
|
||||
return rows;
|
||||
},
|
||||
|
||||
getInfoString() {
|
||||
return "\n" + this.getInfoRows().join("\n");
|
||||
},
|
||||
|
||||
makeInfoElements(Component: React.ComponentType<React.PropsWithChildren>, props: React.PropsWithChildren) {
|
||||
return this.getInfoRows().map((text, i) =>
|
||||
<Component key={i} {...props}>{text}</Component>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -16,20 +16,24 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { DataStore } from "@api/index";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Link } from "@components/Link";
|
||||
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
||||
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { isPluginDev } from "@utils/misc";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { makeCodeblock } from "@utils/text";
|
||||
import definePlugin from "@utils/types";
|
||||
import { isOutdated } from "@utils/updater";
|
||||
import { Alerts, Forms, UserStore } from "@webpack/common";
|
||||
import { isOutdated, update } from "@utils/updater";
|
||||
import { Alerts, Card, ChannelStore, Forms, GuildMemberStore, NavigationRouter, Parser, RelationshipStore, UserStore } from "@webpack/common";
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
import plugins from "~plugins";
|
||||
|
||||
import settings from "./settings";
|
||||
|
||||
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
|
||||
const VENCORD_GUILD_ID = "1015060230222131221";
|
||||
|
||||
const AllowedChannelIds = [
|
||||
SUPPORT_CHANNEL_ID,
|
||||
|
@ -37,6 +41,12 @@ const AllowedChannelIds = [
|
|||
"1033680203433660458", // Vencord > #v
|
||||
];
|
||||
|
||||
const TrustedRolesIds = [
|
||||
"1026534353167208489", // contributor
|
||||
"1026504932959977532", // regular
|
||||
"1042507929485586532", // donor
|
||||
];
|
||||
|
||||
export default definePlugin({
|
||||
name: "SupportHelper",
|
||||
required: true,
|
||||
|
@ -44,6 +54,14 @@ export default definePlugin({
|
|||
authors: [Devs.Ven],
|
||||
dependencies: ["CommandsAPI"],
|
||||
|
||||
patches: [{
|
||||
find: ".BEGINNING_DM.format",
|
||||
replacement: {
|
||||
match: /BEGINNING_DM\.format\(\{.+?\}\),(?=.{0,100}userId:(\i\.getRecipientId\(\)))/,
|
||||
replace: "$& $self.ContributorDmWarningCard({ userId: $1 }),"
|
||||
}
|
||||
}],
|
||||
|
||||
commands: [{
|
||||
name: "vencord-debug",
|
||||
description: "Send Vencord Debug info",
|
||||
|
@ -64,15 +82,13 @@ export default definePlugin({
|
|||
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
|
||||
|
||||
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
|
||||
const enabledApiPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && isApiPlugin(p));
|
||||
|
||||
const info = {
|
||||
Vencord: `v${VERSION} • ${gitHash}${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
|
||||
"Discord Branch": RELEASE_CHANNEL,
|
||||
Client: client,
|
||||
Platform: window.navigator.platform,
|
||||
Outdated: isOutdated,
|
||||
OpenAsar: "openasar" in window,
|
||||
Vencord:
|
||||
`v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +
|
||||
`${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
|
||||
Client: `${RELEASE_CHANNEL} ~ ${client}`,
|
||||
Platform: window.navigator.platform
|
||||
};
|
||||
|
||||
if (IS_DISCORD_DESKTOP) {
|
||||
|
@ -80,11 +96,10 @@ export default definePlugin({
|
|||
}
|
||||
|
||||
const debugInfo = `
|
||||
**Vencord Debug Info**
|
||||
>>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
|
||||
>>> ${Object.entries(info).map(([k, v]) => `**${k}**: ${v}`).join("\n")}
|
||||
|
||||
Enabled Plugins (${enabledPlugins.length + enabledApiPlugins.length}):
|
||||
${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "))}
|
||||
Enabled Plugins (${enabledPlugins.length}):
|
||||
${makeCodeblock(enabledPlugins.join(", "))}
|
||||
`;
|
||||
|
||||
return {
|
||||
|
@ -97,24 +112,75 @@ ${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "
|
|||
async CHANNEL_SELECT({ channelId }) {
|
||||
if (channelId !== SUPPORT_CHANNEL_ID) return;
|
||||
|
||||
if (isPluginDev(UserStore.getCurrentUser().id)) return;
|
||||
const selfId = UserStore.getCurrentUser()?.id;
|
||||
if (!selfId || isPluginDev(selfId)) return;
|
||||
|
||||
if (isOutdated && gitHash !== await DataStore.get(REMEMBER_DISMISS_KEY)) {
|
||||
const rememberDismiss = () => DataStore.set(REMEMBER_DISMISS_KEY, gitHash);
|
||||
|
||||
Alerts.show({
|
||||
if (isOutdated) {
|
||||
return Alerts.show({
|
||||
title: "Hold on!",
|
||||
body: <div>
|
||||
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
|
||||
<Forms.FormText>
|
||||
Please first update using the Updater Page in Settings, or use the VencordInstaller (Update Vencord Button)
|
||||
to do so, in case you can't access the Updater page.
|
||||
<Forms.FormText className={Margins.top8}>
|
||||
Please first update before asking for support!
|
||||
</Forms.FormText>
|
||||
</div>,
|
||||
onCancel: rememberDismiss,
|
||||
onConfirm: rememberDismiss
|
||||
onCancel: () => openUpdaterModal!(),
|
||||
cancelText: "View Updates",
|
||||
confirmText: "Update & Restart Now",
|
||||
async onConfirm() {
|
||||
await update();
|
||||
relaunch();
|
||||
},
|
||||
secondaryConfirmText: "I know what I'm doing or I can't update"
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore outdated type
|
||||
const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
|
||||
if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return;
|
||||
|
||||
if (!IS_WEB && IS_UPDATER_DISABLED) {
|
||||
return Alerts.show({
|
||||
title: "Hold on!",
|
||||
body: <div>
|
||||
<Forms.FormText>You are using an externally updated Vencord version, which we do not provide support for!</Forms.FormText>
|
||||
<Forms.FormText className={Margins.top8}>
|
||||
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
|
||||
contact your package maintainer for support instead.
|
||||
</Forms.FormText>
|
||||
</div>,
|
||||
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
|
||||
});
|
||||
}
|
||||
|
||||
const repo = await VencordNative.updater.getRepo();
|
||||
if (repo.ok && !repo.value.includes("Vendicated/Vencord")) {
|
||||
return Alerts.show({
|
||||
title: "Hold on!",
|
||||
body: <div>
|
||||
<Forms.FormText>You are using a fork of Vencord, which we do not provide support for!</Forms.FormText>
|
||||
<Forms.FormText className={Margins.top8}>
|
||||
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
|
||||
contact your package maintainer for support instead.
|
||||
</Forms.FormText>
|
||||
</div>,
|
||||
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => {
|
||||
if (!isPluginDev(userId)) return null;
|
||||
if (RelationshipStore.isFriend(userId)) return null;
|
||||
|
||||
return (
|
||||
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
|
||||
Please do not private message Vencord plugin developers for support!
|
||||
<br />
|
||||
Instead, use the Vencord support channel: {Parser.parse("https://discord.com/channels/1015060230222131221/1026515880080842772")}
|
||||
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
|
||||
</Card>
|
||||
);
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
|
@ -16,27 +16,46 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
domain: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Remove the untrusted domain popup when opening links",
|
||||
restartNeeded: true
|
||||
},
|
||||
file: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Remove the 'Potentially Dangerous Download' popup when opening links",
|
||||
restartNeeded: true
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "AlwaysTrust",
|
||||
description: "Removes the annoying untrusted domain and suspicious file popup",
|
||||
authors: [Devs.zt],
|
||||
authors: [Devs.zt, Devs.Trwy],
|
||||
patches: [
|
||||
{
|
||||
find: ".displayName=\"MaskedLinkStore\"",
|
||||
find: '="MaskedLinkStore",',
|
||||
replacement: {
|
||||
match: /(?<=isTrustedDomain\(\i\){)return \i\(\i\)/,
|
||||
replace: "return true"
|
||||
}
|
||||
},
|
||||
predicate: () => settings.store.domain
|
||||
},
|
||||
{
|
||||
find: "isSuspiciousDownload:",
|
||||
replacement: {
|
||||
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
|
||||
replace: "$&return null;"
|
||||
}
|
||||
},
|
||||
predicate: () => settings.store.file
|
||||
}
|
||||
]
|
||||
],
|
||||
settings
|
||||
});
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
const enum Methods {
|
||||
Random,
|
||||
Consistent,
|
||||
Timestamp,
|
||||
}
|
||||
|
||||
const tarExtMatcher = /\.tar\.\w+$/;
|
||||
|
||||
export default definePlugin({
|
||||
name: "AnonymiseFileNames",
|
||||
authors: [Devs.obscurity],
|
||||
description: "Anonymise uploaded file names",
|
||||
patches: [
|
||||
{
|
||||
find: "instantBatchUpload:function",
|
||||
replacement: {
|
||||
match: /uploadFiles:(.{1,2}),/,
|
||||
replace:
|
||||
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f.filename)),$1(...args)),",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
options: {
|
||||
method: {
|
||||
description: "Anonymising method",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Random Characters", value: Methods.Random, default: true },
|
||||
{ label: "Consistent", value: Methods.Consistent },
|
||||
{ label: "Timestamp (4chan-like)", value: Methods.Timestamp },
|
||||
],
|
||||
},
|
||||
randomisedLength: {
|
||||
description: "Random characters length",
|
||||
type: OptionType.NUMBER,
|
||||
default: 7,
|
||||
disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Random,
|
||||
},
|
||||
consistent: {
|
||||
description: "Consistent filename",
|
||||
type: OptionType.STRING,
|
||||
default: "image",
|
||||
disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Consistent,
|
||||
},
|
||||
},
|
||||
|
||||
anonymise(file: string) {
|
||||
let name = "image";
|
||||
const tarMatch = tarExtMatcher.exec(file);
|
||||
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");
|
||||
const ext = extIdx !== -1 ? file.slice(extIdx) : "";
|
||||
|
||||
switch (Settings.plugins.AnonymiseFileNames.method) {
|
||||
case Methods.Random:
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
name = Array.from(
|
||||
{ length: Settings.plugins.AnonymiseFileNames.randomisedLength },
|
||||
() => chars[Math.floor(Math.random() * chars.length)]
|
||||
).join("");
|
||||
break;
|
||||
case Methods.Consistent:
|
||||
name = Settings.plugins.AnonymiseFileNames.consistent;
|
||||
break;
|
||||
case Methods.Timestamp:
|
||||
// UNIX timestamp in nanos, i could not find a better dependency-less way
|
||||
name = `${Math.floor(Date.now() / 1000)}${Math.floor(window.performance.now())}`;
|
||||
break;
|
||||
}
|
||||
return name + ext;
|
||||
},
|
||||
});
|
137
src/plugins/anonymiseFileNames/index.tsx
Normal file
137
src/plugins/anonymiseFileNames/index.tsx
Normal file
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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 { Upload } from "@api/MessageEvents";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||
|
||||
type AnonUpload = Upload & { anonymise?: boolean; };
|
||||
|
||||
const ActionBarIcon = findByCodeLazy(".actionBarIcon)");
|
||||
const UploadDraft = findByPropsLazy("popFirstFile", "update");
|
||||
|
||||
const enum Methods {
|
||||
Random,
|
||||
Consistent,
|
||||
Timestamp,
|
||||
}
|
||||
|
||||
const tarExtMatcher = /\.tar\.\w+$/;
|
||||
|
||||
const settings = definePluginSettings({
|
||||
anonymiseByDefault: {
|
||||
description: "Whether to anonymise file names by default",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
method: {
|
||||
description: "Anonymising method",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Random Characters", value: Methods.Random, default: true },
|
||||
{ label: "Consistent", value: Methods.Consistent },
|
||||
{ label: "Timestamp", value: Methods.Timestamp },
|
||||
],
|
||||
},
|
||||
randomisedLength: {
|
||||
description: "Random characters length",
|
||||
type: OptionType.NUMBER,
|
||||
default: 7,
|
||||
disabled: () => settings.store.method !== Methods.Random,
|
||||
},
|
||||
consistent: {
|
||||
description: "Consistent filename",
|
||||
type: OptionType.STRING,
|
||||
default: "image",
|
||||
disabled: () => settings.store.method !== Methods.Consistent,
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "AnonymiseFileNames",
|
||||
authors: [Devs.fawn],
|
||||
description: "Anonymise uploaded file names",
|
||||
patches: [
|
||||
{
|
||||
find: "instantBatchUpload:function",
|
||||
replacement: {
|
||||
match: /uploadFiles:(\i),/,
|
||||
replace:
|
||||
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
|
||||
},
|
||||
},
|
||||
{
|
||||
find: 'addFilesTo:"message.attachments"',
|
||||
replacement: {
|
||||
match: /(\i.uploadFiles\((\i),)/,
|
||||
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".Messages.ATTACHMENT_UTILITIES_SPOILER",
|
||||
replacement: {
|
||||
match: /(?<=children:\[)(?=.{10,80}tooltip:.{0,100}\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/,
|
||||
replace: "arguments[0].canEdit!==false?$self.renderIcon(arguments[0]):null,"
|
||||
},
|
||||
},
|
||||
],
|
||||
settings,
|
||||
|
||||
renderIcon: ErrorBoundary.wrap(({ upload, channelId, draftType }: { upload: AnonUpload; draftType: unknown; channelId: string; }) => {
|
||||
const anonymise = upload.anonymise ?? settings.store.anonymiseByDefault;
|
||||
return (
|
||||
<ActionBarIcon
|
||||
tooltip={anonymise ? "Using anonymous file name" : "Using normal file name"}
|
||||
onClick={() => {
|
||||
upload.anonymise = !anonymise;
|
||||
UploadDraft.update(channelId, upload.id, draftType, {}); // dummy update so component rerenders
|
||||
}}
|
||||
>
|
||||
{anonymise
|
||||
? <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M17.06 13C15.2 13 13.64 14.33 13.24 16.1C12.29 15.69 11.42 15.8 10.76 16.09C10.35 14.31 8.79 13 6.94 13C4.77 13 3 14.79 3 17C3 19.21 4.77 21 6.94 21C9 21 10.68 19.38 10.84 17.32C11.18 17.08 12.07 16.63 13.16 17.34C13.34 19.39 15 21 17.06 21C19.23 21 21 19.21 21 17C21 14.79 19.23 13 17.06 13M6.94 19.86C5.38 19.86 4.13 18.58 4.13 17S5.39 14.14 6.94 14.14C8.5 14.14 9.75 15.42 9.75 17S8.5 19.86 6.94 19.86M17.06 19.86C15.5 19.86 14.25 18.58 14.25 17S15.5 14.14 17.06 14.14C18.62 14.14 19.88 15.42 19.88 17S18.61 19.86 17.06 19.86M22 10.5H2V12H22V10.5M15.53 2.63C15.31 2.14 14.75 1.88 14.22 2.05L12 2.79L9.77 2.05L9.72 2.04C9.19 1.89 8.63 2.17 8.43 2.68L6 9H18L15.56 2.68L15.53 2.63Z" /></svg>
|
||||
: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style={{ transform: "scale(-1,1)" }}><path fill="currentColor" d="M22.11 21.46L2.39 1.73L1.11 3L6.31 8.2L6 9H7.11L8.61 10.5H2V12H10.11L13.5 15.37C13.38 15.61 13.3 15.85 13.24 16.1C12.29 15.69 11.41 15.8 10.76 16.09C10.35 14.31 8.79 13 6.94 13C4.77 13 3 14.79 3 17C3 19.21 4.77 21 6.94 21C9 21 10.68 19.38 10.84 17.32C11.18 17.08 12.07 16.63 13.16 17.34C13.34 19.39 15 21 17.06 21C17.66 21 18.22 20.86 18.72 20.61L20.84 22.73L22.11 21.46M6.94 19.86C5.38 19.86 4.13 18.58 4.13 17C4.13 15.42 5.39 14.14 6.94 14.14C8.5 14.14 9.75 15.42 9.75 17C9.75 18.58 8.5 19.86 6.94 19.86M17.06 19.86C15.5 19.86 14.25 18.58 14.25 17C14.25 16.74 14.29 16.5 14.36 16.25L17.84 19.73C17.59 19.81 17.34 19.86 17.06 19.86M22 12H15.2L13.7 10.5H22V12M17.06 13C19.23 13 21 14.79 21 17C21 17.25 20.97 17.5 20.93 17.73L19.84 16.64C19.68 15.34 18.66 14.32 17.38 14.17L16.29 13.09C16.54 13.03 16.8 13 17.06 13M12.2 9L7.72 4.5L8.43 2.68C8.63 2.17 9.19 1.89 9.72 2.04L9.77 2.05L12 2.79L14.22 2.05C14.75 1.88 15.32 2.14 15.54 2.63L15.56 2.68L18 9H12.2Z" /></svg>
|
||||
}
|
||||
</ActionBarIcon>
|
||||
);
|
||||
}, { noop: true }),
|
||||
|
||||
anonymise(upload: AnonUpload) {
|
||||
if ((upload.anonymise ?? settings.store.anonymiseByDefault) === false) return upload.filename;
|
||||
|
||||
const file = upload.filename;
|
||||
const tarMatch = tarExtMatcher.exec(file);
|
||||
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");
|
||||
const ext = extIdx !== -1 ? file.slice(extIdx) : "";
|
||||
|
||||
switch (settings.store.method) {
|
||||
case Methods.Random:
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
return Array.from(
|
||||
{ length: settings.store.randomisedLength },
|
||||
() => chars[Math.floor(Math.random() * chars.length)]
|
||||
).join("") + ext;
|
||||
case Methods.Consistent:
|
||||
return settings.store.consistent + ext;
|
||||
case Methods.Timestamp:
|
||||
return Date.now() + ext;
|
||||
}
|
||||
},
|
||||
});
|
5
src/plugins/automodContext/README.md
Normal file
5
src/plugins/automodContext/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# AutomodContext
|
||||
|
||||
Allows you to jump to the messages surrounding an automod hit
|
||||
|
||||
![Visualization](https://github.com/Vendicated/Vencord/assets/61953774/d13740c8-2062-4553-b975-82fd3d6cc08b)
|
73
src/plugins/automodContext/index.tsx
Normal file
73
src/plugins/automodContext/index.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Button, ChannelStore, Text } from "@webpack/common";
|
||||
|
||||
const { selectChannel } = findByPropsLazy("selectChannel", "selectVoiceChannel");
|
||||
|
||||
function jumpToMessage(channelId: string, messageId: string) {
|
||||
const guildId = ChannelStore.getChannel(channelId)?.guild_id;
|
||||
|
||||
selectChannel({
|
||||
guildId,
|
||||
channelId,
|
||||
messageId,
|
||||
jumpType: "INSTANT"
|
||||
});
|
||||
}
|
||||
|
||||
function findChannelId(message: any): string | null {
|
||||
const { embeds: [embed] } = message;
|
||||
const channelField = embed.fields.find(({ rawName }) => rawName === "channel_id");
|
||||
|
||||
if (!channelField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return channelField.rawValue;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "AutomodContext",
|
||||
description: "Allows you to jump to the messages surrounding an automod hit.",
|
||||
authors: [Devs.JohnyTheCarrot],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".Messages.GUILD_AUTOMOD_REPORT_ISSUES",
|
||||
replacement: {
|
||||
match: /\.Messages\.ACTIONS.+?}\)(?=,(\(0.{0,40}\.dot.*?}\)),)/,
|
||||
replace: (m, dot) => `${m},${dot},$self.renderJumpButton({message:arguments[0].message})`
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
renderJumpButton: ErrorBoundary.wrap(({ message }: { message: any; }) => {
|
||||
const channelId = findChannelId(message);
|
||||
|
||||
if (!channelId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
style={{ padding: "2px 8px" }}
|
||||
look={Button.Looks.LINK}
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.LINK}
|
||||
onClick={() => jumpToMessage(channelId, message.id)}
|
||||
>
|
||||
<Text color="text-link" variant="text-xs/normal">
|
||||
Jump to Surrounding
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
}, { noop: true })
|
||||
});
|
|
@ -20,7 +20,7 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { FluxDispatcher, i18n } from "@webpack/common";
|
||||
import { FluxDispatcher, i18n, useMemo } from "@webpack/common";
|
||||
|
||||
import FolderSideBar from "./FolderSideBar";
|
||||
|
||||
|
@ -117,8 +117,8 @@ export default definePlugin({
|
|||
},
|
||||
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
|
||||
{
|
||||
match: /(useStateFromStoresArray\).{0,25}let \i)=(\i\.\i.getGuildsTree\(\))/,
|
||||
replace: (_, rest, guildsTree) => `${rest}=$self.getGuildTree(!!arguments[0].isBetterFolders,${guildsTree},arguments[0].betterFoldersExpandedIds)`
|
||||
match: /\[(\i)\]=(\(0,\i\.useStateFromStoresArray\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
|
||||
replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0].isBetterFolders,betterFoldersOriginalTree,arguments[0].betterFoldersExpandedIds)`
|
||||
},
|
||||
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
|
||||
{
|
||||
|
@ -127,7 +127,7 @@ export default definePlugin({
|
|||
},
|
||||
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
|
||||
{
|
||||
match: /unreadMentionsIndicatorBottom,barClassName.+?}\)\]/,
|
||||
match: /unreadMentionsIndicatorBottom,.+?}\)\]/,
|
||||
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
|
||||
},
|
||||
// Export the isBetterFolders variable to the folders component
|
||||
|
@ -209,7 +209,7 @@ export default definePlugin({
|
|||
predicate: () => settings.store.closeAllHomeButton,
|
||||
replacement: {
|
||||
// Close all folders when clicking the home button
|
||||
match: /(?<=onClick:\(\)=>{)(?=.{0,200}"discodo")/,
|
||||
match: /(?<=onClick:\(\)=>{)(?=.{0,300}"discodo")/,
|
||||
replace: "$self.closeFolders();"
|
||||
}
|
||||
}
|
||||
|
@ -252,19 +252,21 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
|
||||
getGuildTree(isBetterFolders: boolean, oldTree: any, expandedFolderIds?: Set<any>) {
|
||||
if (!isBetterFolders || expandedFolderIds == null) return oldTree;
|
||||
getGuildTree(isBetterFolders: boolean, originalTree: any, expandedFolderIds?: Set<any>) {
|
||||
return useMemo(() => {
|
||||
if (!isBetterFolders || expandedFolderIds == null) return originalTree;
|
||||
|
||||
const newTree = new GuildsTree();
|
||||
// Children is every folder and guild which is not in a folder, this filters out only the expanded folders
|
||||
newTree.root.children = oldTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
|
||||
// Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
|
||||
newTree.nodes = Object.fromEntries(
|
||||
Object.entries(oldTree.nodes)
|
||||
.filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
|
||||
);
|
||||
const newTree = new GuildsTree();
|
||||
// Children is every folder and guild which is not in a folder, this filters out only the expanded folders
|
||||
newTree.root.children = originalTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
|
||||
// Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
|
||||
newTree.nodes = Object.fromEntries(
|
||||
Object.entries(originalTree.nodes)
|
||||
.filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
|
||||
);
|
||||
|
||||
return newTree;
|
||||
return newTree;
|
||||
}, [isBetterFolders, originalTree, expandedFolderIds]);
|
||||
},
|
||||
|
||||
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
|
||||
|
@ -279,7 +281,7 @@ export default definePlugin({
|
|||
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
|
||||
return child => {
|
||||
if (isBetterFolders) {
|
||||
return "onScroll" in child.props;
|
||||
return child?.props?.onScroll != null;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
|
|
@ -26,7 +26,7 @@ export default definePlugin({
|
|||
"Change GIF alt text from simply being 'GIF' to containing the gif tags / filename",
|
||||
patches: [
|
||||
{
|
||||
find: "onCloseImage=",
|
||||
find: '"onCloseImage",',
|
||||
replacement: {
|
||||
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
|
||||
replace:
|
||||
|
|
23
src/plugins/betterGifPicker/index.ts
Normal file
23
src/plugins/betterGifPicker/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterGifPicker",
|
||||
description: "Makes the gif picker open the favourite category by default",
|
||||
authors: [Devs.Samwich],
|
||||
patches: [
|
||||
{
|
||||
find: ".GIFPickerResultTypes.SEARCH",
|
||||
replacement: [{
|
||||
match: /(?<="state",{resultType:)null/,
|
||||
replace: '"Favorites"'
|
||||
}]
|
||||
}
|
||||
]
|
||||
});
|
|
@ -17,7 +17,9 @@
|
|||
*/
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { canonicalizeMatch } from "@utils/patches";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
|
||||
|
@ -39,8 +41,12 @@ export default definePlugin({
|
|||
match: /hideNote:.+?(?=([,}].*?\)))/g,
|
||||
replace: (m, rest) => {
|
||||
const destructuringMatch = rest.match(/}=.+/);
|
||||
if (destructuringMatch == null) return "hideNote:!0";
|
||||
return m;
|
||||
if (destructuringMatch) {
|
||||
const defaultValueMatch = m.match(canonicalizeMatch(/hideNote:(\i)=!?\d/));
|
||||
return defaultValueMatch ? `hideNote:${defaultValueMatch[1]}=!0` : m;
|
||||
}
|
||||
|
||||
return "hideNote:!0";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -52,10 +58,10 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
{
|
||||
find: ".Messages.NOTE}",
|
||||
find: ".popularApplicationCommandIds,",
|
||||
replacement: {
|
||||
match: /(?<=return \i\?)null(?=:\(0,\i\.jsxs)/,
|
||||
replace: "$self.patchPadding(arguments[0])"
|
||||
match: /lastSection:(!?\i)}\),/,
|
||||
replace: "$&$self.patchPadding({lastSection:$1}),"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -75,10 +81,10 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
|
||||
patchPadding(e: any) {
|
||||
if (!e.lastSection) return;
|
||||
patchPadding: ErrorBoundary.wrap(({ lastSection }) => {
|
||||
if (!lastSection) return null;
|
||||
return (
|
||||
<div className={UserPopoutSectionCssClasses.lastSection}></div>
|
||||
<div className={UserPopoutSectionCssClasses.lastSection} ></div>
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
6
src/plugins/betterRoleContext/README.md
Normal file
6
src/plugins/betterRoleContext/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# BetterRoleContext
|
||||
|
||||
Adds options to copy role color, edit role and view role icon when right clicking roles in the user profile
|
||||
|
||||
![](https://github.com/Vendicated/Vencord/assets/45497981/354220a4-09f3-4c5f-a28e-4b19ca775190)
|
||||
|
121
src/plugins/betterRoleContext/index.tsx
Normal file
121
src/plugins/betterRoleContext/index.tsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { ImageIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getCurrentGuild, openImageModal } from "@utils/discord";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
|
||||
|
||||
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
|
||||
|
||||
function PencilIcon() {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
width="18"
|
||||
height="18"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill="currentColor" d="m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function AppearanceIcon() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M 12,0 C 5.3733333,0 0,5.3733333 0,12 c 0,6.626667 5.3733333,12 12,12 1.106667,0 2,-0.893333 2,-2 0,-0.52 -0.2,-0.986667 -0.52,-1.346667 -0.306667,-0.346666 -0.506667,-0.813333 -0.506667,-1.32 0,-1.106666 0.893334,-2 2,-2 h 2.36 C 21.013333,17.333333 24,14.346667 24,10.666667 24,4.7733333 18.626667,0 12,0 Z M 4.6666667,12 c -1.1066667,0 -2,-0.893333 -2,-2 0,-1.1066667 0.8933333,-2 2,-2 1.1066666,0 2,0.8933333 2,2 0,1.106667 -0.8933334,2 -2,2 z M 8.666667,6.6666667 c -1.106667,0 -2.0000003,-0.8933334 -2.0000003,-2 0,-1.1066667 0.8933333,-2 2.0000003,-2 1.106666,0 2,0.8933333 2,2 0,1.1066666 -0.893334,2 -2,2 z m 6.666666,0 c -1.106666,0 -2,-0.8933334 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.1066666 -0.893333,2 -2,2 z m 4,5.3333333 c -1.106666,0 -2,-0.893333 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.106667 -0.893333,2 -2,2 z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
roleIconFileFormat: {
|
||||
type: OptionType.SELECT,
|
||||
description: "File format to use when viewing role icons",
|
||||
options: [
|
||||
{
|
||||
label: "png",
|
||||
value: "png",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "webp",
|
||||
value: "webp",
|
||||
},
|
||||
{
|
||||
label: "jpg",
|
||||
value: "jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterRoleContext",
|
||||
description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
|
||||
authors: [Devs.Ven, Devs.goodbee],
|
||||
|
||||
settings,
|
||||
|
||||
start() {
|
||||
// DeveloperMode needs to be enabled for the context menu to be shown
|
||||
TextAndImagesSettingsStores.DeveloperMode.updateSetting(true);
|
||||
},
|
||||
|
||||
contextMenus: {
|
||||
"dev-context"(children, { id }: { id: string; }) {
|
||||
const guild = getCurrentGuild();
|
||||
if (!guild) return;
|
||||
|
||||
const role = GuildStore.getRole(guild.id, id);
|
||||
if (!role) return;
|
||||
|
||||
if (role.colorString) {
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="vc-copy-role-color"
|
||||
label="Copy Role Color"
|
||||
action={() => Clipboard.copy(role.colorString!)}
|
||||
icon={AppearanceIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (role.icon) {
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="vc-view-role-icon"
|
||||
label="View Role Icon"
|
||||
action={() => {
|
||||
openImageModal(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`);
|
||||
}}
|
||||
icon={ImageIcon}
|
||||
/>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="vc-edit-role"
|
||||
label="Edit Role"
|
||||
action={async () => {
|
||||
await GuildSettingsActions.open(guild.id, "ROLES");
|
||||
GuildSettingsActions.selectRole(id);
|
||||
}}
|
||||
icon={PencilIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
5
src/plugins/betterSessions/README.md
Normal file
5
src/plugins/betterSessions/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# BetterSessions
|
||||
|
||||
Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions.
|
||||
|
||||
![](https://github.com/Vendicated/Vencord/assets/9750071/4a44b617-bb8f-4dcb-93f1-b7d2575ed3d8)
|
37
src/plugins/betterSessions/components/RenameButton.tsx
Normal file
37
src/plugins/betterSessions/components/RenameButton.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { openModal } from "@utils/modal";
|
||||
import { Button } from "@webpack/common";
|
||||
|
||||
import { SessionInfo } from "../types";
|
||||
import { RenameModal } from "./RenameModal";
|
||||
|
||||
export function RenameButton({ session, state }: { session: SessionInfo["session"], state: [string, React.Dispatch<React.SetStateAction<string>>]; }) {
|
||||
return (
|
||||
<Button
|
||||
look={Button.Looks.LINK}
|
||||
color={Button.Colors.LINK}
|
||||
size={Button.Sizes.NONE}
|
||||
style={{
|
||||
paddingTop: "0px",
|
||||
paddingBottom: "0px",
|
||||
top: "-2px"
|
||||
}}
|
||||
onClick={() =>
|
||||
openModal(props => (
|
||||
<RenameModal
|
||||
props={props}
|
||||
session={session}
|
||||
state={state}
|
||||
/>
|
||||
))
|
||||
}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
);
|
||||
}
|
94
src/plugins/betterSessions/components/RenameModal.tsx
Normal file
94
src/plugins/betterSessions/components/RenameModal.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
|
||||
import { Button, Forms, React, TextInput } from "@webpack/common";
|
||||
import { KeyboardEvent } from "react";
|
||||
|
||||
import { SessionInfo } from "../types";
|
||||
import { getDefaultName, savedSessionsCache, saveSessionsToDataStore } from "../utils";
|
||||
|
||||
export function RenameModal({ props, session, state }: { props: ModalProps, session: SessionInfo["session"], state: [string, React.Dispatch<React.SetStateAction<string>>]; }) {
|
||||
const [title, setTitle] = state;
|
||||
const [value, setValue] = React.useState(savedSessionsCache.get(session.id_hash)?.name ?? "");
|
||||
|
||||
function onSaveClick() {
|
||||
savedSessionsCache.set(session.id_hash, { name: value, isNew: false });
|
||||
if (value !== "") {
|
||||
setTitle(`${value}*`);
|
||||
} else {
|
||||
setTitle(getDefaultName(session.client_info));
|
||||
}
|
||||
|
||||
saveSessionsToDataStore();
|
||||
props.onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalRoot {...props}>
|
||||
<ModalHeader>
|
||||
<Forms.FormTitle tag="h4">Rename</Forms.FormTitle>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>New device name</Forms.FormTitle>
|
||||
<TextInput
|
||||
style={{ marginBottom: "10px" }}
|
||||
placeholder={getDefaultName(session.client_info)}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
onSaveClick();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
marginBottom: "20px",
|
||||
paddingLeft: "1px",
|
||||
paddingRight: "1px",
|
||||
opacity: 0.6
|
||||
}}
|
||||
look={Button.Looks.LINK}
|
||||
color={Button.Colors.LINK}
|
||||
size={Button.Sizes.NONE}
|
||||
onClick={() => setValue("")}
|
||||
>
|
||||
Reset Name
|
||||
</Button>
|
||||
</ModalContent>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color={Button.Colors.BRAND}
|
||||
onClick={onSaveClick}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.LINK}
|
||||
onClick={() => props.onClose()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot >
|
||||
);
|
||||
}
|
106
src/plugins/betterSessions/components/icons.tsx
Normal file
106
src/plugins/betterSessions/components/icons.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 { LazyComponent } from "@utils/react";
|
||||
import { findByCode } from "@webpack";
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export const DiscordIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612Zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ChromeIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path d="M188.8,255.93A67.2,67.2,0,1,0,256,188.75,67.38,67.38,0,0,0,188.8,255.93Z" />
|
||||
<path d="M476.75,217.79s0,0,0,.05a206.63,206.63,0,0,0-7-28.84h-.11a202.16,202.16,0,0,1,7.07,29h0a203.5,203.5,0,0,0-7.07-29H314.24c19.05,17,31.36,40.17,31.36,67.05a86.55,86.55,0,0,1-12.31,44.73L231,478.45a2.44,2.44,0,0,1,0,.27V479h0v-.26A224,224,0,0,0,256,480c6.84,0,13.61-.39,20.3-1a222.91,222.91,0,0,0,29.78-4.74C405.68,451.52,480,362.4,480,255.94A225.25,225.25,0,0,0,476.75,217.79Z" />
|
||||
<path d="M256,345.5c-33.6,0-61.6-17.91-77.29-44.79L76,123.05l-.14-.24A224,224,0,0,0,207.4,474.55l0-.05,77.69-134.6A84.13,84.13,0,0,1,256,345.5Z" />
|
||||
<path d="M91.29,104.57l77.35,133.25A89.19,89.19,0,0,1,256,166H461.17a246.51,246.51,0,0,0-25.78-43.94l.12.08A245.26,245.26,0,0,1,461.17,166h.17a245.91,245.91,0,0,0-25.66-44,2.63,2.63,0,0,1-.35-.26A223.93,223.93,0,0,0,91.14,104.34l.14.24Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const EdgeIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M21.86 17.86q.14 0 .25.12.1.13.1.25t-.11.33l-.32.46-.43.53-.44.5q-.21.25-.38.42l-.22.23q-.58.53-1.34 1.04-.76.51-1.6.91-.86.4-1.74.64t-1.67.24q-.9 0-1.69-.28-.8-.28-1.48-.78-.68-.5-1.22-1.17-.53-.66-.92-1.44-.38-.77-.58-1.6-.2-.83-.2-1.67 0-1 .32-1.96.33-.97.87-1.8.14.95.55 1.77.41.82 1.02 1.5.6.68 1.38 1.21.78.54 1.64.9.86.36 1.77.56.92.2 1.8.2 1.12 0 2.18-.24 1.06-.23 2.06-.72l.2-.1.2-.05zm-15.5-1.27q0 1.1.27 2.15.27 1.06.78 2.03.51.96 1.24 1.77.74.82 1.66 1.4-1.47-.2-2.8-.74-1.33-.55-2.48-1.37-1.15-.83-2.08-1.9-.92-1.07-1.58-2.33T.36 14.94Q0 13.54 0 12.06q0-.81.32-1.49.31-.68.83-1.23.53-.55 1.2-.96.66-.4 1.35-.66.74-.27 1.5-.39.78-.12 1.55-.12.7 0 1.42.1.72.12 1.4.35.68.23 1.32.57.63.35 1.16.83-.35 0-.7.07-.33.07-.65.23v-.02q-.63.28-1.2.74-.57.46-1.05 1.04-.48.58-.87 1.26-.38.67-.65 1.39-.27.71-.42 1.44-.15.72-.15 1.38zM11.96.06q1.7 0 3.33.39 1.63.38 3.07 1.15 1.43.77 2.62 1.93 1.18 1.16 1.98 2.7.49.94.76 1.96.28 1 .28 2.08 0 .89-.23 1.7-.24.8-.69 1.48-.45.68-1.1 1.22-.64.53-1.45.88-.54.24-1.11.36-.58.13-1.16.13-.42 0-.97-.03-.54-.03-1.1-.12-.55-.1-1.05-.28-.5-.19-.84-.5-.12-.09-.23-.24-.1-.16-.1-.33 0-.15.16-.35.16-.2.35-.5.2-.28.36-.68.16-.4.16-.95 0-1.06-.4-1.96-.4-.91-1.06-1.64-.66-.74-1.52-1.28-.86-.55-1.79-.89-.84-.3-1.72-.44-.87-.14-1.76-.14-1.55 0-3.06.45T.94 7.55q.71-1.74 1.81-3.13 1.1-1.38 2.52-2.35Q6.68 1.1 8.37.58q1.7-.52 3.58-.52Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FirefoxIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path d="M130.22 127.548C130.38 127.558 130.3 127.558 130.22 127.548V127.548ZM481.64 172.898C471.03 147.398 449.56 119.898 432.7 111.168C446.42 138.058 454.37 165.048 457.4 185.168C457.405 185.306 457.422 185.443 457.45 185.578C429.87 116.828 383.098 89.1089 344.9 28.7479C329.908 5.05792 333.976 3.51792 331.82 4.08792L331.7 4.15792C284.99 30.1109 256.365 82.5289 249.12 126.898C232.503 127.771 216.219 131.895 201.19 139.035C199.838 139.649 198.736 140.706 198.066 142.031C197.396 143.356 197.199 144.87 197.506 146.323C197.7 147.162 198.068 147.951 198.586 148.639C199.103 149.327 199.76 149.899 200.512 150.318C201.264 150.737 202.096 150.993 202.954 151.071C203.811 151.148 204.676 151.045 205.491 150.768L206.011 150.558C221.511 143.255 238.408 139.393 255.541 139.238C318.369 138.669 352.698 183.262 363.161 201.528C350.161 192.378 326.811 183.338 304.341 187.248C392.081 231.108 368.541 381.784 246.951 376.448C187.487 373.838 149.881 325.467 146.421 285.648C146.421 285.648 157.671 243.698 227.041 243.698C234.541 243.698 255.971 222.778 256.371 216.698C256.281 214.698 213.836 197.822 197.281 181.518C188.434 172.805 184.229 168.611 180.511 165.458C178.499 163.75 176.392 162.158 174.201 160.688C168.638 141.231 168.399 120.638 173.51 101.058C148.45 112.468 128.96 130.508 114.8 146.428H114.68C105.01 134.178 105.68 93.7779 106.25 85.3479C106.13 84.8179 99.022 89.0159 98.1 89.6579C89.5342 95.7103 81.5528 102.55 74.26 110.088C57.969 126.688 30.128 160.242 18.76 211.318C14.224 231.701 12 255.739 12 263.618C12 398.318 121.21 507.508 255.92 507.508C376.56 507.508 478.939 420.281 496.35 304.888C507.922 228.192 481.64 173.82 481.64 172.898Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IEIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path d="M483.049 159.706c10.855-24.575 21.424-60.438 21.424-87.871 0-72.722-79.641-98.371-209.673-38.577-107.632-7.181-211.221 73.67-237.098 186.457 30.852-34.862 78.271-82.298 121.977-101.158C125.404 166.85 79.128 228.002 43.992 291.725 23.246 329.651 0 390.94 0 436.747c0 98.575 92.854 86.5 180.251 42.006 31.423 15.43 66.559 15.573 101.695 15.573 97.124 0 184.249-54.294 216.814-146.022H377.927c-52.509 88.593-196.819 52.996-196.819-47.436H509.9c6.407-43.581-1.655-95.715-26.851-141.162zM64.559 346.877c17.711 51.15 53.703 95.871 100.266 123.304-88.741 48.94-173.267 29.096-100.266-123.304zm115.977-108.873c2-55.151 50.276-94.871 103.98-94.871 53.418 0 101.981 39.72 103.981 94.871H180.536zm184.536-187.6c21.425-10.287 48.563-22.003 72.558-22.003 31.422 0 54.274 21.717 54.274 53.722 0 20.003-7.427 49.007-14.569 67.867-26.28-42.292-65.986-81.584-112.263-99.586z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const OperaIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 496 512"
|
||||
>
|
||||
<path d="M313.9 32.7c-170.2 0-252.6 223.8-147.5 355.1 36.5 45.4 88.6 75.6 147.5 75.6 36.3 0 70.3-11.1 99.4-30.4-43.8 39.2-101.9 63-165.3 63-3.9 0-8 0-11.9-.3C104.6 489.6 0 381.1 0 248 0 111 111 0 248 0h.8c63.1.3 120.7 24.1 164.4 63.1-29-19.4-63.1-30.4-99.3-30.4zm101.8 397.7c-40.9 24.7-90.7 23.6-132-5.8 56.2-20.5 97.7-91.6 97.7-176.6 0-84.7-41.2-155.8-97.4-176.6 41.8-29.2 91.2-30.3 132.9-5 105.9 98.7 105.5 265.7-1.2 364z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SafariIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path d="M274.69,274.69l-37.38-37.38L166,346ZM256,8C119,8,8,119,8,256S119,504,256,504,504,393,504,256,393,8,256,8ZM411.85,182.79l14.78-6.13A8,8,0,0,1,437.08,181h0a8,8,0,0,1-4.33,10.46L418,197.57a8,8,0,0,1-10.45-4.33h0A8,8,0,0,1,411.85,182.79ZM314.43,94l6.12-14.78A8,8,0,0,1,331,74.92h0a8,8,0,0,1,4.33,10.45l-6.13,14.78a8,8,0,0,1-10.45,4.33h0A8,8,0,0,1,314.43,94ZM256,60h0a8,8,0,0,1,8,8V84a8,8,0,0,1-8,8h0a8,8,0,0,1-8-8V68A8,8,0,0,1,256,60ZM181,74.92a8,8,0,0,1,10.46,4.33L197.57,94a8,8,0,1,1-14.78,6.12l-6.13-14.78A8,8,0,0,1,181,74.92Zm-63.58,42.49h0a8,8,0,0,1,11.31,0L140,128.72A8,8,0,0,1,140,140h0a8,8,0,0,1-11.31,0l-11.31-11.31A8,8,0,0,1,117.41,117.41ZM60,256h0a8,8,0,0,1,8-8H84a8,8,0,0,1,8,8h0a8,8,0,0,1-8,8H68A8,8,0,0,1,60,256Zm40.15,73.21-14.78,6.13A8,8,0,0,1,74.92,331h0a8,8,0,0,1,4.33-10.46L94,314.43a8,8,0,0,1,10.45,4.33h0A8,8,0,0,1,100.15,329.21Zm4.33-136h0A8,8,0,0,1,94,197.57l-14.78-6.12A8,8,0,0,1,74.92,181h0a8,8,0,0,1,10.45-4.33l14.78,6.13A8,8,0,0,1,104.48,193.24ZM197.57,418l-6.12,14.78a8,8,0,0,1-14.79-6.12l6.13-14.78A8,8,0,1,1,197.57,418ZM264,444a8,8,0,0,1-8,8h0a8,8,0,0,1-8-8V428a8,8,0,0,1,8-8h0a8,8,0,0,1,8,8Zm67-6.92h0a8,8,0,0,1-10.46-4.33L314.43,418a8,8,0,0,1,4.33-10.45h0a8,8,0,0,1,10.45,4.33l6.13,14.78A8,8,0,0,1,331,437.08Zm63.58-42.49h0a8,8,0,0,1-11.31,0L372,383.28A8,8,0,0,1,372,372h0a8,8,0,0,1,11.31,0l11.31,11.31A8,8,0,0,1,394.59,394.59ZM286.25,286.25,110.34,401.66,225.75,225.75,401.66,110.34ZM437.08,331h0a8,8,0,0,1-10.45,4.33l-14.78-6.13a8,8,0,0,1-4.33-10.45h0A8,8,0,0,1,418,314.43l14.78,6.12A8,8,0,0,1,437.08,331ZM444,264H428a8,8,0,0,1-8-8h0a8,8,0,0,1,8-8h16a8,8,0,0,1,8,8h0A8,8,0,0,1,444,264Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const UnknownIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
||||
<svg
|
||||
{...props}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path fill-rule="evenodd" d="M4.475 5.458c-.284 0-.514-.237-.47-.517C4.28 3.24 5.576 2 7.825 2c2.25 0 3.767 1.36 3.767 3.215 0 1.344-.665 2.288-1.79 2.973-1.1.659-1.414 1.118-1.414 2.01v.03a.5.5 0 0 1-.5.5h-.77a.5.5 0 0 1-.5-.495l-.003-.2c-.043-1.221.477-2.001 1.645-2.712 1.03-.632 1.397-1.135 1.397-2.028 0-.979-.758-1.698-1.926-1.698-1.009 0-1.71.529-1.938 1.402-.066.254-.278.461-.54.461h-.777ZM7.496 14c.622 0 1.095-.474 1.095-1.09 0-.618-.473-1.092-1.095-1.092-.606 0-1.087.474-1.087 1.091S6.89 14 7.496 14Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MobileIcon = LazyComponent(() => findByCode("M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38"));
|
227
src/plugins/betterSessions/index.tsx
Normal file
227
src/plugins/betterSessions/index.tsx
Normal file
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* 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 { showNotification } from "@api/Notifications";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
|
||||
import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
|
||||
|
||||
import { RenameButton } from "./components/RenameButton";
|
||||
import { Session, SessionInfo } from "./types";
|
||||
import { fetchNamesFromDataStore, getDefaultName, GetOsColor, GetPlatformIcon, savedSessionsCache, saveSessionsToDataStore } from "./utils";
|
||||
|
||||
const AuthSessionsStore = findStoreLazy("AuthSessionsStore");
|
||||
const UserSettingsModal = findByPropsLazy("saveAccountChanges", "open");
|
||||
|
||||
const TimestampClasses = findByPropsLazy("timestampTooltip", "blockquoteContainer");
|
||||
const SessionIconClasses = findByPropsLazy("sessionIcon");
|
||||
|
||||
const BlobMask = findExportedComponentLazy("BlobMask");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
backgroundCheck: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Check for new sessions in the background, and display notifications when they are detected",
|
||||
default: false,
|
||||
restartNeeded: true
|
||||
},
|
||||
checkInterval: {
|
||||
description: "How often to check for new sessions in the background (if enabled), in minutes",
|
||||
type: OptionType.NUMBER,
|
||||
default: 20,
|
||||
restartNeeded: true
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterSessions",
|
||||
description: "Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions.",
|
||||
authors: [Devs.amia],
|
||||
|
||||
settings: settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "Messages.AUTH_SESSIONS_SESSION_LOG_OUT",
|
||||
replacement: [
|
||||
// Replace children with a single label with state
|
||||
{
|
||||
match: /({variant:"eyebrow",className:\i\.sessionInfoRow,children:).{70,110}{children:"\\xb7"}\),\(0,\i\.\i\)\("span",{children:\i\[\d+\]}\)\]}\)\]/,
|
||||
replace: "$1$self.renderName(arguments[0])"
|
||||
},
|
||||
{
|
||||
match: /({variant:"text-sm\/medium",className:\i\.sessionInfoRow,children:.{70,110}{children:"\\xb7"}\),\(0,\i\.\i\)\("span",{children:)(\i\[\d+\])}/,
|
||||
replace: "$1$self.renderTimestamp({ ...arguments[0], timeLabel: $2 })}"
|
||||
},
|
||||
// Replace the icon
|
||||
{
|
||||
match: /\.currentSession:null\),children:\[(?<=,icon:(\i)\}.+?)/,
|
||||
replace: "$& $self.renderIcon({ ...arguments[0], DeviceIcon: $1 }), false &&"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
// Add the ability to change BlobMask's lower badge height
|
||||
// (it allows changing width so we can mirror that logic)
|
||||
find: "this.getBadgePositionInterpolation(",
|
||||
replacement: {
|
||||
match: /(\i\.animated\.rect,{id:\i,x:48-\(\i\+8\)\+4,y:)28(,width:\i\+8,height:)24,/,
|
||||
replace: (_, leftPart, rightPart) => `${leftPart} 48 - ((this.props.lowerBadgeHeight ?? 16) + 8) + 4 ${rightPart} (this.props.lowerBadgeHeight ?? 16) + 8,`
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
renderName: ErrorBoundary.wrap(({ session }: SessionInfo) => {
|
||||
const savedSession = savedSessionsCache.get(session.id_hash);
|
||||
|
||||
const state = React.useState(savedSession?.name ? `${savedSession.name}*` : getDefaultName(session.client_info));
|
||||
const [title, setTitle] = state;
|
||||
|
||||
// Show a "NEW" badge if the session is seen for the first time
|
||||
return (
|
||||
<>
|
||||
<span>{title}</span>
|
||||
{(savedSession == null || savedSession.isNew) && (
|
||||
<div
|
||||
className="vc-plugins-badge"
|
||||
style={{
|
||||
backgroundColor: "#ED4245",
|
||||
marginLeft: "2px"
|
||||
}}
|
||||
>
|
||||
NEW
|
||||
</div>
|
||||
)}
|
||||
<RenameButton session={session} state={state} />
|
||||
</>
|
||||
);
|
||||
}, { noop: true }),
|
||||
|
||||
renderTimestamp: ErrorBoundary.wrap(({ session, timeLabel }: { session: Session, timeLabel: string; }) => {
|
||||
return (
|
||||
<Tooltip text={session.approx_last_used_time.toLocaleString()} tooltipClassName={TimestampClasses.timestampTooltip}>
|
||||
{props => (
|
||||
<span {...props} className={TimestampClasses.timestamp}>
|
||||
{timeLabel}
|
||||
</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}, { noop: true }),
|
||||
|
||||
renderIcon: ErrorBoundary.wrap(({ session, DeviceIcon }: { session: Session, DeviceIcon: React.ComponentType<any>; }) => {
|
||||
const PlatformIcon = GetPlatformIcon(session.client_info.platform);
|
||||
|
||||
return (
|
||||
<BlobMask
|
||||
style={{ cursor: "unset" }}
|
||||
selected={false}
|
||||
lowerBadge={
|
||||
<div
|
||||
style={{
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
overflow: "hidden",
|
||||
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--interactive-normal)",
|
||||
color: "var(--background-secondary)",
|
||||
}}
|
||||
>
|
||||
<PlatformIcon width={14} height={14} />
|
||||
</div>
|
||||
}
|
||||
lowerBadgeWidth={20}
|
||||
lowerBadgeHeight={20}
|
||||
>
|
||||
<div
|
||||
className={SessionIconClasses.sessionIcon}
|
||||
style={{ backgroundColor: GetOsColor(session.client_info.os) }}
|
||||
>
|
||||
<DeviceIcon width={28} height={28} />
|
||||
</div>
|
||||
</BlobMask>
|
||||
);
|
||||
}, { noop: true }),
|
||||
|
||||
async checkNewSessions() {
|
||||
const data = await RestAPI.get({
|
||||
url: Constants.Endpoints.AUTH_SESSIONS
|
||||
});
|
||||
|
||||
for (const session of data.body.user_sessions) {
|
||||
if (savedSessionsCache.has(session.id_hash)) continue;
|
||||
|
||||
savedSessionsCache.set(session.id_hash, { name: "", isNew: true });
|
||||
showNotification({
|
||||
title: "BetterSessions",
|
||||
body: `New session:\n${session.client_info.os} · ${session.client_info.platform} · ${session.client_info.location}`,
|
||||
permanent: true,
|
||||
onClick: () => UserSettingsModal.open("Sessions")
|
||||
});
|
||||
}
|
||||
|
||||
saveSessionsToDataStore();
|
||||
},
|
||||
|
||||
flux: {
|
||||
USER_SETTINGS_ACCOUNT_RESET_AND_CLOSE_FORM() {
|
||||
const lastFetchedHashes: string[] = AuthSessionsStore.getSessions().map((session: SessionInfo["session"]) => session.id_hash);
|
||||
|
||||
// Add new sessions to cache
|
||||
lastFetchedHashes.forEach(idHash => {
|
||||
if (!savedSessionsCache.has(idHash)) savedSessionsCache.set(idHash, { name: "", isNew: false });
|
||||
});
|
||||
|
||||
// Delete removed sessions from cache
|
||||
if (lastFetchedHashes.length > 0) {
|
||||
savedSessionsCache.forEach((_, idHash) => {
|
||||
if (!lastFetchedHashes.includes(idHash)) savedSessionsCache.delete(idHash);
|
||||
});
|
||||
}
|
||||
|
||||
// Dismiss the "NEW" badge of all sessions.
|
||||
// Since the only way for a session to be marked as "NEW" is going to the Devices tab,
|
||||
// closing the settings means they've been viewed and are no longer considered new.
|
||||
savedSessionsCache.forEach(data => {
|
||||
data.isNew = false;
|
||||
});
|
||||
saveSessionsToDataStore();
|
||||
}
|
||||
},
|
||||
|
||||
async start() {
|
||||
await fetchNamesFromDataStore();
|
||||
|
||||
this.checkNewSessions();
|
||||
if (settings.store.backgroundCheck) {
|
||||
this.checkInterval = setInterval(this.checkNewSessions, settings.store.checkInterval * 60 * 1000);
|
||||
}
|
||||
},
|
||||
|
||||
stop() {
|
||||
clearInterval(this.checkInterval);
|
||||
}
|
||||
});
|
32
src/plugins/betterSessions/types.ts
Normal file
32
src/plugins/betterSessions/types.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export interface SessionInfo {
|
||||
session: {
|
||||
id_hash: string;
|
||||
approx_last_used_time: Date;
|
||||
client_info: {
|
||||
os: string;
|
||||
platform: string;
|
||||
location: string;
|
||||
};
|
||||
},
|
||||
current?: boolean;
|
||||
}
|
||||
|
||||
export type Session = SessionInfo["session"];
|
90
src/plugins/betterSessions/utils.ts
Normal file
90
src/plugins/betterSessions/utils.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { DataStore } from "@api/index";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
import { ChromeIcon, DiscordIcon, EdgeIcon, FirefoxIcon, IEIcon, MobileIcon, OperaIcon, SafariIcon, UnknownIcon } from "./components/icons";
|
||||
import { SessionInfo } from "./types";
|
||||
|
||||
const getDataKey = () => `BetterSessions_savedSessions_${UserStore.getCurrentUser().id}`;
|
||||
|
||||
export const savedSessionsCache: Map<string, { name: string, isNew: boolean; }> = new Map();
|
||||
|
||||
export function getDefaultName(clientInfo: SessionInfo["session"]["client_info"]) {
|
||||
return `${clientInfo.os} · ${clientInfo.platform}`;
|
||||
}
|
||||
|
||||
export function saveSessionsToDataStore() {
|
||||
return DataStore.set(getDataKey(), savedSessionsCache);
|
||||
}
|
||||
|
||||
export async function fetchNamesFromDataStore() {
|
||||
const savedSessions = await DataStore.get<Map<string, { name: string, isNew: boolean; }>>(getDataKey()) || new Map();
|
||||
savedSessions.forEach((data, idHash) => {
|
||||
savedSessionsCache.set(idHash, data);
|
||||
});
|
||||
}
|
||||
|
||||
export function GetOsColor(os: string) {
|
||||
switch (os) {
|
||||
case "Windows Mobile":
|
||||
case "Windows":
|
||||
return "#55a6ef"; // Light blue
|
||||
case "Linux":
|
||||
return "#cdcd31"; // Yellow
|
||||
case "Android":
|
||||
return "#7bc958"; // Green
|
||||
case "Mac OS X":
|
||||
case "iOS":
|
||||
return ""; // Default to white/black (theme-dependent)
|
||||
default:
|
||||
return "#f3799a"; // Pink
|
||||
}
|
||||
}
|
||||
|
||||
export function GetPlatformIcon(platform: string) {
|
||||
switch (platform) {
|
||||
case "Discord Android":
|
||||
case "Discord iOS":
|
||||
case "Discord Client":
|
||||
return DiscordIcon;
|
||||
case "Android Chrome":
|
||||
case "Chrome iOS":
|
||||
case "Chrome":
|
||||
return ChromeIcon;
|
||||
case "Edge":
|
||||
return EdgeIcon;
|
||||
case "Firefox":
|
||||
return FirefoxIcon;
|
||||
case "Internet Explorer":
|
||||
return IEIcon;
|
||||
case "Opera Mini":
|
||||
case "Opera":
|
||||
return OperaIcon;
|
||||
case "Mobile Safari":
|
||||
case "Safari":
|
||||
return SafariIcon;
|
||||
case "BlackBerry":
|
||||
case "Facebook Mobile":
|
||||
case "Android Mobile":
|
||||
return MobileIcon;
|
||||
default:
|
||||
return UnknownIcon;
|
||||
}
|
||||
}
|
9
src/plugins/betterSettings/README.md
Normal file
9
src/plugins/betterSettings/README.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# BetterSettings
|
||||
|
||||
Improves Discord's Settings via multiple (toggleable) changes:
|
||||
- makes opening settings much faster
|
||||
- removes the scuffed transition animation
|
||||
- organises the settings cog context menu into categories
|
||||
|
||||
![](https://github.com/Vendicated/Vencord/assets/45497981/e8d67a95-3909-4be5-8281-8cf9d2f1c30e)
|
||||
|
185
src/plugins/betterSettings/index.tsx
Normal file
185
src/plugins/betterSettings/index.tsx
Normal file
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { waitFor } from "@webpack";
|
||||
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
|
||||
import type { HTMLAttributes, ReactElement } from "react";
|
||||
|
||||
type SettingsEntry = { section: string, label: string; };
|
||||
|
||||
const cl = classNameFactory("");
|
||||
let Classes: Record<string, string>;
|
||||
waitFor(["animating", "baseLayer", "bg", "layer", "layers"], m => Classes = m);
|
||||
|
||||
const settings = definePluginSettings({
|
||||
disableFade: {
|
||||
description: "Disable the crossfade animation",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
organizeMenu: {
|
||||
description: "Organizes the settings cog context menu into categories",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true
|
||||
},
|
||||
eagerLoad: {
|
||||
description: "Removes the loading delay when opening the menu for the first time",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
}
|
||||
});
|
||||
|
||||
interface LayerProps extends HTMLAttributes<HTMLDivElement> {
|
||||
mode: "SHOWN" | "HIDDEN";
|
||||
baseLayer?: boolean;
|
||||
}
|
||||
|
||||
function Layer({ mode, baseLayer = false, ...props }: LayerProps) {
|
||||
const hidden = mode === "HIDDEN";
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => () => {
|
||||
ComponentDispatch.dispatch("LAYER_POP_START");
|
||||
ComponentDispatch.dispatch("LAYER_POP_COMPLETE");
|
||||
}, []);
|
||||
|
||||
const node = (
|
||||
<div
|
||||
ref={containerRef}
|
||||
aria-hidden={hidden}
|
||||
className={cl({
|
||||
[Classes.layer]: true,
|
||||
[Classes.baseLayer]: baseLayer,
|
||||
"stop-animations": hidden
|
||||
})}
|
||||
style={{ opacity: hidden ? 0 : undefined }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
return baseLayer
|
||||
? node
|
||||
: <FocusLock containerRef={containerRef}>{node}</FocusLock>;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterSettings",
|
||||
description: "Enhances your settings-menu-opening experience",
|
||||
authors: [Devs.Kyuuhachi],
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "this.renderArtisanalHack()",
|
||||
replacement: [
|
||||
{ // Fade in on layer
|
||||
match: /(?<=\((\i),"contextType",\i\.AccessibilityPreferencesContext\);)/,
|
||||
replace: "$1=$self.Layer;",
|
||||
predicate: () => settings.store.disableFade
|
||||
},
|
||||
{ // Lazy-load contents
|
||||
match: /createPromise:\(\)=>([^:}]*?),webpackId:"\d+",name:(?!="CollectiblesShop")"[^"]+"/g,
|
||||
replace: "$&,_:$1",
|
||||
predicate: () => settings.store.eagerLoad
|
||||
}
|
||||
]
|
||||
},
|
||||
{ // For some reason standardSidebarView also has a small fade-in
|
||||
find: "DefaultCustomContentScroller:function()",
|
||||
replacement: [
|
||||
{
|
||||
match: /\(0,\i\.useTransition\)\((\i)/,
|
||||
replace: "(_cb=>_cb(void 0,$1))||$&"
|
||||
},
|
||||
{
|
||||
match: /\i\.animated\.div/,
|
||||
replace: '"div"'
|
||||
}
|
||||
],
|
||||
predicate: () => settings.store.disableFade
|
||||
},
|
||||
{ // Load menu TOC eagerly
|
||||
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
|
||||
replacement: {
|
||||
match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/,
|
||||
replace: "(async ()=>$2)(),"
|
||||
},
|
||||
predicate: () => settings.store.eagerLoad
|
||||
},
|
||||
{ // Settings cog context menu
|
||||
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
||||
replacement: {
|
||||
match: /\(0,\i.useDefaultUserSettingsSections\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
||||
replace: "$self.wrapMenu($&)"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
|
||||
// without possibly also catching unrelated errors of children.
|
||||
//
|
||||
// Thus, we sanity check webpack modules & do this really hacky try catch to hopefully prevent hard crashes if something goes wrong.
|
||||
// try catch will only catch errors in the Layer function (hence why it's called as a plain function rather than a component), but
|
||||
// not in children
|
||||
Layer(props: LayerProps) {
|
||||
if (!FocusLock || !ComponentDispatch || !Classes) {
|
||||
new Logger("BetterSettings").error("Failed to find some components");
|
||||
return props.children;
|
||||
}
|
||||
|
||||
return <Layer {...props} />;
|
||||
},
|
||||
|
||||
wrapMenu(list: SettingsEntry[]) {
|
||||
if (!settings.store.organizeMenu) return list;
|
||||
|
||||
const items = [{ label: null as string | null, items: [] as SettingsEntry[] }];
|
||||
|
||||
for (const item of list) {
|
||||
if (item.section === "HEADER") {
|
||||
items.push({ label: item.label, items: [] });
|
||||
} else if (item.section === "DIVIDER") {
|
||||
items.push({ label: i18n.Messages.OTHER_OPTIONS, items: [] });
|
||||
} else {
|
||||
items.at(-1)!.items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
filter(predicate: (item: SettingsEntry) => boolean) {
|
||||
for (const category of items) {
|
||||
category.items = category.items.filter(predicate);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
map(render: (item: SettingsEntry) => ReactElement) {
|
||||
return items
|
||||
.filter(a => a.items.length > 0)
|
||||
.map(({ label, items }) => {
|
||||
const children = items.map(render);
|
||||
if (label) {
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
id={label.replace(/\W/, "_")}
|
||||
label={label}
|
||||
children={children}
|
||||
action={children[0].props.action}
|
||||
/>);
|
||||
} else {
|
||||
return children;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
|
@ -21,7 +21,7 @@ import definePlugin from "@utils/types";
|
|||
|
||||
export default definePlugin({
|
||||
name: "BetterUploadButton",
|
||||
authors: [Devs.obscurity, Devs.Ven],
|
||||
authors: [Devs.fawn, Devs.Ven],
|
||||
description: "Upload with a single click, open menu with right click",
|
||||
patches: [
|
||||
{
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { ScreenshareIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { openImageModal } from "@utils/discord";
|
||||
|
@ -60,7 +60,7 @@ export const handleViewPreview = async ({ guildId, channelId, ownerId }: Applica
|
|||
openImageModal(previewUrl);
|
||||
};
|
||||
|
||||
export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => () => {
|
||||
export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => {
|
||||
const stream = ApplicationStreamingStore.getAnyStreamForUser(userId);
|
||||
if (!stream) return;
|
||||
|
||||
|
@ -82,19 +82,15 @@ export const streamContextPatch: NavContextMenuPatchCallback = (children, { stre
|
|||
};
|
||||
|
||||
export const userContextPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => {
|
||||
return addViewStreamContext(children, { userId: user.id });
|
||||
if (user) return addViewStreamContext(children, { userId: user.id });
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "BiggerStreamPreview",
|
||||
description: "This plugin allows you to enlarge stream previews",
|
||||
authors: [Devs.phil],
|
||||
start: () => {
|
||||
addContextMenuPatch("user-context", userContextPatch);
|
||||
addContextMenuPatch("stream-context", streamContextPatch);
|
||||
},
|
||||
stop: () => {
|
||||
removeContextMenuPatch("user-context", userContextPatch);
|
||||
removeContextMenuPatch("stream-context", streamContextPatch);
|
||||
contextMenus: {
|
||||
"user-context": userContextPatch,
|
||||
"stream-context": streamContextPatch
|
||||
}
|
||||
});
|
||||
|
|
|
@ -140,11 +140,11 @@ export const defaultRules = [
|
|||
"tt_content",
|
||||
"lr@yandex.*",
|
||||
"redircnt@yandex.*",
|
||||
"feature@youtube.com",
|
||||
"kw@youtube.com",
|
||||
"si@youtube.com",
|
||||
"pp@youtube.com",
|
||||
"si@youtu.be",
|
||||
"feature@*.youtube.com",
|
||||
"kw@*.youtube.com",
|
||||
"si@*.youtube.com",
|
||||
"pp@*.youtube.com",
|
||||
"si@*.youtu.be",
|
||||
"wt_zmc",
|
||||
"utm_source",
|
||||
"utm_content",
|
||||
|
@ -153,5 +153,6 @@ export const defaultRules = [
|
|||
"utm_term",
|
||||
"si@open.spotify.com",
|
||||
"igshid",
|
||||
"igsh",
|
||||
"share_id@reddit.com",
|
||||
];
|
||||
|
|
|
@ -19,6 +19,16 @@
|
|||
border: thin solid var(--background-modifier-accent) !important;
|
||||
}
|
||||
|
||||
.client-theme-warning {
|
||||
.client-theme-warning * {
|
||||
color: var(--text-danger);
|
||||
}
|
||||
|
||||
.client-theme-contrast-warning {
|
||||
background-color: var(--background-primary);
|
||||
padding: 0.5rem;
|
||||
border-radius: .5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -8,19 +8,19 @@ import "./clientTheme.css";
|
|||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getTheme, Theme } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, Forms } from "@webpack/common";
|
||||
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, Forms, useStateFromStores } from "@webpack/common";
|
||||
|
||||
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
|
||||
|
||||
const colorPresets = [
|
||||
"#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D",
|
||||
"#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42",
|
||||
"#3C2E42", "#422938"
|
||||
"#3C2E42", "#422938", "#b6908f", "#bfa088", "#d3c77d",
|
||||
"#86ac86", "#88aab3", "#8693b5", "#8a89ba", "#ad94bb",
|
||||
];
|
||||
|
||||
function onPickColor(color: number) {
|
||||
|
@ -30,9 +30,35 @@ function onPickColor(color: number) {
|
|||
updateColorVars(hexColor);
|
||||
}
|
||||
|
||||
const { saveClientTheme } = findByPropsLazy("saveClientTheme");
|
||||
|
||||
function setTheme(theme: string) {
|
||||
saveClientTheme({ theme });
|
||||
}
|
||||
|
||||
const ThemeStore = findStoreLazy("ThemeStore");
|
||||
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");
|
||||
|
||||
function ThemeSettings() {
|
||||
const lightnessWarning = hexToLightness(settings.store.color) > 45;
|
||||
const lightModeWarning = getTheme() === Theme.Light;
|
||||
const theme = useStateFromStores([ThemeStore], () => ThemeStore.theme);
|
||||
const isLightTheme = theme === "light";
|
||||
const oppositeTheme = isLightTheme ? "dark" : "light";
|
||||
|
||||
const nitroTheme = useStateFromStores([NitroThemeStore], () => NitroThemeStore.gradientPreset);
|
||||
const nitroThemeEnabled = nitroTheme !== undefined;
|
||||
|
||||
const selectedLuminance = relativeLuminance(settings.store.color);
|
||||
|
||||
let contrastWarning = false, fixableContrast = true;
|
||||
if ((isLightTheme && selectedLuminance < 0.26) || !isLightTheme && selectedLuminance > 0.12)
|
||||
contrastWarning = true;
|
||||
if (selectedLuminance < 0.26 && selectedLuminance > 0.12)
|
||||
fixableContrast = false;
|
||||
// light mode with values greater than 65 leads to background colors getting crushed together and poor text contrast for muted channels
|
||||
if (isLightTheme && selectedLuminance > 0.65) {
|
||||
contrastWarning = true;
|
||||
fixableContrast = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="client-theme-settings">
|
||||
|
@ -48,15 +74,18 @@ function ThemeSettings() {
|
|||
suggestedColors={colorPresets}
|
||||
/>
|
||||
</div>
|
||||
{lightnessWarning || lightModeWarning
|
||||
? <div>
|
||||
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
|
||||
<Forms.FormText className="client-theme-warning">Your theme won't look good:</Forms.FormText>
|
||||
{lightnessWarning && <Forms.FormText className="client-theme-warning">Selected color is very light</Forms.FormText>}
|
||||
{lightModeWarning && <Forms.FormText className="client-theme-warning">Light mode isn't supported</Forms.FormText>}
|
||||
{(contrastWarning || nitroThemeEnabled) && (<>
|
||||
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
|
||||
<div className={`client-theme-contrast-warning ${contrastWarning ? (isLightTheme ? "theme-dark" : "theme-light") : ""}`}>
|
||||
<div className="client-theme-warning">
|
||||
<Forms.FormText>Warning, your theme won't look good:</Forms.FormText>
|
||||
{contrastWarning && <Forms.FormText>Selected color won't contrast well with text</Forms.FormText>}
|
||||
{nitroThemeEnabled && <Forms.FormText>Nitro themes aren't supported</Forms.FormText>}
|
||||
</div>
|
||||
{(contrastWarning && fixableContrast) && <Button onClick={() => setTheme(oppositeTheme)} color={Button.Colors.RED}>Switch to {oppositeTheme} mode</Button>}
|
||||
{(nitroThemeEnabled) && <Button onClick={() => setTheme(theme)} color={Button.Colors.RED}>Disable Nitro Theme</Button>}
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -87,9 +116,12 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
startAt: StartAt.DOMContentLoaded,
|
||||
start() {
|
||||
async start() {
|
||||
updateColorVars(settings.store.color);
|
||||
generateColorOffsets();
|
||||
|
||||
const styles = await getStyles();
|
||||
generateColorOffsets(styles);
|
||||
generateLightModeFixes(styles);
|
||||
},
|
||||
|
||||
stop() {
|
||||
|
@ -98,56 +130,86 @@ export default definePlugin({
|
|||
}
|
||||
});
|
||||
|
||||
const variableRegex = /(--primary-[5-9]\d{2}-hsl):.*?(\S*)%;/g;
|
||||
const variableRegex = /(--primary-\d{3}-hsl):.*?(\S*)%;/g;
|
||||
const lightVariableRegex = /^--primary-[1-5]\d{2}-hsl/g;
|
||||
const darkVariableRegex = /^--primary-[5-9]\d{2}-hsl/g;
|
||||
|
||||
async function generateColorOffsets() {
|
||||
|
||||
const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]');
|
||||
const variableLightness = {} as Record<string, number>;
|
||||
|
||||
// Search all stylesheets for color variables
|
||||
for (const styleLinkNode of styleLinkNodes) {
|
||||
const cssLink = styleLinkNode.getAttribute("href");
|
||||
if (!cssLink) continue;
|
||||
|
||||
const res = await fetch(cssLink);
|
||||
const cssString = await res.text();
|
||||
|
||||
// Get lightness values of --primary variables >=500
|
||||
let variableMatch = variableRegex.exec(cssString);
|
||||
while (variableMatch !== null) {
|
||||
const [, variable, lightness] = variableMatch;
|
||||
variableLightness[variable] = parseFloat(lightness);
|
||||
variableMatch = variableRegex.exec(cssString);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate offsets
|
||||
const lightnessOffsets = Object.entries(variableLightness)
|
||||
// generates variables per theme by:
|
||||
// - matching regex (so we can limit what variables are included in light/dark theme, otherwise text becomes unreadable)
|
||||
// - offset from specified center (light/dark theme get different offsets because light uses 100 for background-primary, while dark uses 600)
|
||||
function genThemeSpecificOffsets(variableLightness: Record<string, number>, regex: RegExp, centerVariable: string): string {
|
||||
return Object.entries(variableLightness).filter(([key]) => key.search(regex) > -1)
|
||||
.map(([key, lightness]) => {
|
||||
const lightnessOffset = lightness - variableLightness["--primary-600-hsl"];
|
||||
const lightnessOffset = lightness - variableLightness[centerVariable];
|
||||
const plusOrMinus = lightnessOffset >= 0 ? "+" : "-";
|
||||
return `${key}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute("id", "clientThemeOffsets");
|
||||
style.textContent = `:root:root {
|
||||
${lightnessOffsets}
|
||||
}`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
function generateColorOffsets(styles) {
|
||||
const variableLightness = {} as Record<string, number>;
|
||||
|
||||
// Get lightness values of --primary variables
|
||||
let variableMatch = variableRegex.exec(styles);
|
||||
while (variableMatch !== null) {
|
||||
const [, variable, lightness] = variableMatch;
|
||||
variableLightness[variable] = parseFloat(lightness);
|
||||
variableMatch = variableRegex.exec(styles);
|
||||
}
|
||||
|
||||
createStyleSheet("clientThemeOffsets", [
|
||||
`.theme-light {\n ${genThemeSpecificOffsets(variableLightness, lightVariableRegex, "--primary-345-hsl")} \n}`,
|
||||
`.theme-dark {\n ${genThemeSpecificOffsets(variableLightness, darkVariableRegex, "--primary-600-hsl")} \n}`,
|
||||
].join("\n\n"));
|
||||
}
|
||||
|
||||
function generateLightModeFixes(styles) {
|
||||
const groupLightUsesW500Regex = /\.theme-light[^{]*\{[^}]*var\(--white-500\)[^}]*}/gm;
|
||||
// get light capturing groups that mention --white-500
|
||||
const relevantStyles = [...styles.matchAll(groupLightUsesW500Regex)].flat();
|
||||
|
||||
const groupBackgroundRegex = /^([^{]*)\{background:var\(--white-500\)/m;
|
||||
const groupBackgroundColorRegex = /^([^{]*)\{background-color:var\(--white-500\)/m;
|
||||
// find all capturing groups that assign background or background-color directly to w500
|
||||
const backgroundGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundRegex)).join(",\n");
|
||||
const backgroundColorGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundColorRegex)).join(",\n");
|
||||
// create css to reassign them to --primary-100
|
||||
const reassignBackgrounds = `${backgroundGroups} {\n background: var(--primary-100) \n}`;
|
||||
const reassignBackgroundColors = `${backgroundColorGroups} {\n background-color: var(--primary-100) \n}`;
|
||||
|
||||
const groupBgVarRegex = /\.theme-light\{([^}]*--[^:}]*(?:background|bg)[^:}]*:var\(--white-500\)[^}]*)\}/m;
|
||||
const bgVarRegex = /^(--[^:]*(?:background|bg)[^:]*):var\(--white-500\)/m;
|
||||
// get all global variables used for backgrounds
|
||||
const lightVars = mapReject(relevantStyles, style => captureOne(style, groupBgVarRegex)) // get the insides of capture groups that have at least one background var with w500
|
||||
.map(str => str.split(";")).flat(); // captureGroupInsides[] -> cssRule[]
|
||||
const lightBgVars = mapReject(lightVars, variable => captureOne(variable, bgVarRegex)); // remove vars that aren't for backgrounds or w500
|
||||
// create css to reassign every var
|
||||
const reassignVariables = `.theme-light {\n ${lightBgVars.map(variable => `${variable}: var(--primary-100);`).join("\n")} \n}`;
|
||||
|
||||
createStyleSheet("clientThemeLightModeFixes", [
|
||||
reassignBackgrounds,
|
||||
reassignBackgroundColors,
|
||||
reassignVariables,
|
||||
].join("\n\n"));
|
||||
}
|
||||
|
||||
function captureOne(str, regex) {
|
||||
const result = str.match(regex);
|
||||
return (result === null) ? null : result[1];
|
||||
}
|
||||
|
||||
function mapReject(arr, mapFunc) {
|
||||
return arr.map(mapFunc).filter(Boolean);
|
||||
}
|
||||
|
||||
function updateColorVars(color: string) {
|
||||
const { hue, saturation, lightness } = hexToHSL(color);
|
||||
|
||||
let style = document.getElementById("clientThemeVars");
|
||||
if (!style) {
|
||||
style = document.createElement("style");
|
||||
style.setAttribute("id", "clientThemeVars");
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
if (!style)
|
||||
style = createStyleSheet("clientThemeVars");
|
||||
|
||||
style.textContent = `:root {
|
||||
--theme-h: ${hue};
|
||||
|
@ -156,6 +218,28 @@ function updateColorVars(color: string) {
|
|||
}`;
|
||||
}
|
||||
|
||||
function createStyleSheet(id, content = "") {
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute("id", id);
|
||||
style.textContent = content.split("\n").map(line => line.trim()).join("\n");
|
||||
document.body.appendChild(style);
|
||||
return style;
|
||||
}
|
||||
|
||||
// returns all of discord's native styles in a single string
|
||||
async function getStyles(): Promise<string> {
|
||||
let out = "";
|
||||
const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]');
|
||||
for (const styleLinkNode of styleLinkNodes) {
|
||||
const cssLink = styleLinkNode.getAttribute("href");
|
||||
if (!cssLink) continue;
|
||||
|
||||
const res = await fetch(cssLink);
|
||||
out += await res.text();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// https://css-tricks.com/converting-color-spaces-in-javascript/
|
||||
function hexToHSL(hexCode: string) {
|
||||
// Hex => RGB normalized to 0-1
|
||||
|
@ -198,17 +282,14 @@ function hexToHSL(hexCode: string) {
|
|||
return { hue, saturation, lightness };
|
||||
}
|
||||
|
||||
// Minimized math just for lightness, lowers lag when changing colors
|
||||
function hexToLightness(hexCode: string) {
|
||||
// Hex => RGB normalized to 0-1
|
||||
const r = parseInt(hexCode.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(hexCode.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(hexCode.substring(4, 6), 16) / 255;
|
||||
// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
|
||||
function relativeLuminance(hexCode: string) {
|
||||
const normalize = (x: number) =>
|
||||
x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
|
||||
|
||||
const cMax = Math.max(r, g, b);
|
||||
const cMin = Math.min(r, g, b);
|
||||
const r = normalize(parseInt(hexCode.substring(0, 2), 16) / 255);
|
||||
const g = normalize(parseInt(hexCode.substring(2, 4), 16) / 255);
|
||||
const b = normalize(parseInt(hexCode.substring(4, 6), 16) / 255);
|
||||
|
||||
const lightness = 100 * ((cMax + cMin) / 2);
|
||||
|
||||
return lightness;
|
||||
return r * 0.2126 + g * 0.7152 + b * 0.0722;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { LinkIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
@ -29,7 +29,9 @@ interface UserContextProps {
|
|||
user: User;
|
||||
}
|
||||
|
||||
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => {
|
||||
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => {
|
||||
if (!user) return;
|
||||
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="vc-copy-user-url"
|
||||
|
@ -44,12 +46,7 @@ export default definePlugin({
|
|||
name: "CopyUserURLs",
|
||||
authors: [Devs.castdrian],
|
||||
description: "Adds a 'Copy User URL' option to the user context menu.",
|
||||
|
||||
start() {
|
||||
addContextMenuPatch("user-context", UserContextMenuPatch);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeContextMenuPatch("user-context", UserContextMenuPatch);
|
||||
},
|
||||
contextMenus: {
|
||||
"user-context": UserContextMenuPatch
|
||||
}
|
||||
});
|
||||
|
|
|
@ -24,23 +24,20 @@ import { closeAllModals } from "@utils/modal";
|
|||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { maybePromptToUpdate } from "@utils/updater";
|
||||
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
|
||||
import { FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
|
||||
import type { ReactElement } from "react";
|
||||
import { DraftType, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
|
||||
|
||||
const CrashHandlerLogger = new Logger("CrashHandler");
|
||||
const { ModalStack, DraftManager, DraftType, closeExpressionPicker } = proxyLazyWebpack(() => {
|
||||
const modules = findBulk(
|
||||
|
||||
const { ModalStack, DraftManager, closeExpressionPicker } = proxyLazyWebpack(() => {
|
||||
const [ModalStack, DraftManager, ExpressionManager] = findBulk(
|
||||
filters.byProps("pushLazy", "popAll"),
|
||||
filters.byProps("clearDraft", "saveDraft"),
|
||||
filters.byProps("DraftType"),
|
||||
filters.byProps("closeExpressionPicker", "openExpressionPicker"),
|
||||
);
|
||||
filters.byProps("closeExpressionPicker", "openExpressionPicker"),);
|
||||
|
||||
return {
|
||||
ModalStack: modules[0],
|
||||
DraftManager: modules[1],
|
||||
DraftType: modules[2]?.DraftType,
|
||||
closeExpressionPicker: modules[3]?.closeExpressionPicker,
|
||||
ModalStack,
|
||||
DraftManager,
|
||||
closeExpressionPicker: ExpressionManager?.closeExpressionPicker,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -57,13 +54,13 @@ const settings = definePluginSettings({
|
|||
}
|
||||
});
|
||||
|
||||
let crashCount: number = 0;
|
||||
let lastCrashTimestamp: number = 0;
|
||||
let shouldAttemptNextHandle = false;
|
||||
let hasCrashedOnce = false;
|
||||
let isRecovering = false;
|
||||
let shouldAttemptRecover = true;
|
||||
|
||||
export default definePlugin({
|
||||
name: "CrashHandler",
|
||||
description: "Utility plugin for handling and possibly recovering from Crashes without a restart",
|
||||
description: "Utility plugin for handling and possibly recovering from crashes without a restart",
|
||||
authors: [Devs.Nuckyz],
|
||||
enabledByDefault: true,
|
||||
|
||||
|
@ -73,67 +70,76 @@ export default definePlugin({
|
|||
{
|
||||
find: ".Messages.ERRORS_UNEXPECTED_CRASH",
|
||||
replacement: {
|
||||
match: /(?=this\.setState\()/,
|
||||
replace: "$self.handleCrash(this)||"
|
||||
match: /this\.setState\((.+?)\)/,
|
||||
replace: "$self.handleCrash(this,$1);"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
handleCrash(_this: ReactElement & { forceUpdate: () => void; }) {
|
||||
if (Date.now() - lastCrashTimestamp <= 1_000 && !shouldAttemptNextHandle) return true;
|
||||
handleCrash(_this: any, errorState: any) {
|
||||
_this.setState(errorState);
|
||||
|
||||
shouldAttemptNextHandle = false;
|
||||
// Already recovering, prevent error which happens more than once too fast to trigger another recover
|
||||
if (isRecovering) return;
|
||||
isRecovering = true;
|
||||
|
||||
if (++crashCount > 5) {
|
||||
// 1 ms timeout to avoid react breaking when re-rendering
|
||||
setTimeout(() => {
|
||||
try {
|
||||
showNotification({
|
||||
color: "#eed202",
|
||||
title: "Discord has crashed!",
|
||||
body: "Awn :( Discord has crashed more than five times, not attempting to recover.",
|
||||
noPersist: true,
|
||||
});
|
||||
// Prevent a crash loop with an error that could not be handled
|
||||
if (!shouldAttemptRecover) {
|
||||
try {
|
||||
showNotification({
|
||||
color: "#eed202",
|
||||
title: "Discord has crashed!",
|
||||
body: "Awn :( Discord has crashed two times rapidly, not attempting to recover.",
|
||||
noPersist: true
|
||||
});
|
||||
} catch { }
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
shouldAttemptRecover = false;
|
||||
// This is enough to avoid a crash loop
|
||||
setTimeout(() => shouldAttemptRecover = true, 1000);
|
||||
} catch { }
|
||||
|
||||
lastCrashTimestamp = Date.now();
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (!hasCrashedOnce) {
|
||||
hasCrashedOnce = true;
|
||||
maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true);
|
||||
}
|
||||
} catch { }
|
||||
|
||||
setTimeout(() => crashCount--, 60_000);
|
||||
|
||||
try {
|
||||
if (crashCount === 1) maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true);
|
||||
|
||||
if (settings.store.attemptToPreventCrashes) {
|
||||
this.handlePreventCrash(_this);
|
||||
return true;
|
||||
try {
|
||||
if (settings.store.attemptToPreventCrashes) {
|
||||
this.handlePreventCrash(_this);
|
||||
}
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.error("Failed to handle crash", err);
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.error("Failed to handle crash", err);
|
||||
return false;
|
||||
} finally {
|
||||
lastCrashTimestamp = Date.now();
|
||||
}
|
||||
}, 1);
|
||||
},
|
||||
|
||||
handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) {
|
||||
if (Date.now() - lastCrashTimestamp >= 1_000) {
|
||||
try {
|
||||
showNotification({
|
||||
color: "#eed202",
|
||||
title: "Discord has crashed!",
|
||||
body: "Attempting to recover...",
|
||||
noPersist: true,
|
||||
});
|
||||
} catch { }
|
||||
}
|
||||
handlePreventCrash(_this: any) {
|
||||
try {
|
||||
showNotification({
|
||||
color: "#eed202",
|
||||
title: "Discord has crashed!",
|
||||
body: "Attempting to recover...",
|
||||
noPersist: true
|
||||
});
|
||||
} catch { }
|
||||
|
||||
try {
|
||||
const channelId = SelectedChannelStore.getChannelId();
|
||||
|
||||
DraftManager.clearDraft(channelId, DraftType.ChannelMessage);
|
||||
DraftManager.clearDraft(channelId, DraftType.FirstThreadMessage);
|
||||
for (const key in DraftType) {
|
||||
if (!Number.isNaN(Number(key))) continue;
|
||||
|
||||
DraftManager.clearDraft(channelId, DraftType[key]);
|
||||
}
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.debug("Failed to clear drafts.", err);
|
||||
}
|
||||
|
@ -176,9 +182,12 @@ export default definePlugin({
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Set isRecovering to false before setting the state to allow us to handle the next crash error correcty, in case it happens
|
||||
setImmediate(() => isRecovering = false);
|
||||
|
||||
try {
|
||||
shouldAttemptNextHandle = true;
|
||||
_this.forceUpdate();
|
||||
_this.setState({ error: null, info: null });
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.debug("Failed to update crash handler component.", err);
|
||||
}
|
||||
|
|
68
src/plugins/ctrlEnterSend/index.ts
Normal file
68
src/plugins/ctrlEnterSend/index.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "CtrlEnterSend",
|
||||
authors: [Devs.UlyssesZhan],
|
||||
description: "Use Ctrl+Enter to send messages (customizable)",
|
||||
settings: definePluginSettings({
|
||||
submitRule: {
|
||||
description: "The way to send a message",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{
|
||||
label: "Ctrl+Enter (Enter or Shift+Enter for new line)",
|
||||
value: "ctrl+enter"
|
||||
},
|
||||
{
|
||||
label: "Shift+Enter (Enter for new line)",
|
||||
value: "shift+enter"
|
||||
},
|
||||
{
|
||||
label: "Enter (Shift+Enter for new line; Discord default)",
|
||||
value: "enter"
|
||||
}
|
||||
],
|
||||
default: "ctrl+enter"
|
||||
},
|
||||
sendMessageInTheMiddleOfACodeBlock: {
|
||||
description: "Whether to send a message in the middle of a code block",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
}
|
||||
}),
|
||||
patches: [
|
||||
{
|
||||
find: "KeyboardKeys.ENTER&&(!",
|
||||
replacement: {
|
||||
match: /(?<=(\i)\.which===\i\.KeyboardKeys.ENTER&&).{0,100}(\(0,\i\.hasOpenPlainTextCodeBlock\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/,
|
||||
replace: "$self.shouldSubmit($1, $2)"
|
||||
}
|
||||
}
|
||||
],
|
||||
shouldSubmit(event: KeyboardEvent, codeblock: boolean): boolean {
|
||||
let result = false;
|
||||
switch (this.settings.store.submitRule) {
|
||||
case "shift+enter":
|
||||
result = event.shiftKey;
|
||||
break;
|
||||
case "ctrl+enter":
|
||||
result = event.ctrlKey;
|
||||
break;
|
||||
case "enter":
|
||||
result = !event.shiftKey && !event.ctrlKey;
|
||||
break;
|
||||
}
|
||||
if (!this.settings.store.sendMessageInTheMiddleOfACodeBlock) {
|
||||
result &&= !codeblock;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
|
@ -17,17 +17,20 @@
|
|||
*/
|
||||
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Link } from "@components/Link";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { isTruthy } from "@utils/guards";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
|
||||
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, StatusSettingsStores, UserStore } from "@webpack/common";
|
||||
|
||||
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
|
||||
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
|
||||
const ActivityClassName = findByPropsLazy("activity", "buttonColor");
|
||||
const Colors = findByPropsLazy("profileColors");
|
||||
|
||||
async function getApplicationAsset(key: string): Promise<string> {
|
||||
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
|
||||
|
@ -175,7 +178,7 @@ const settings = definePluginSettings({
|
|||
},
|
||||
startTime: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Start timestamp (only for custom timestamp mode)",
|
||||
description: "Start timestamp in milisecond (only for custom timestamp mode)",
|
||||
onChange: onChange,
|
||||
disabled: isTimestampDisabled,
|
||||
isValid: (value: number) => {
|
||||
|
@ -185,7 +188,7 @@ const settings = definePluginSettings({
|
|||
},
|
||||
endTime: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "End timestamp (only for custom timestamp mode)",
|
||||
description: "End timestamp in milisecond (only for custom timestamp mode)",
|
||||
onChange: onChange,
|
||||
disabled: isTimestampDisabled,
|
||||
isValid: (value: number) => {
|
||||
|
@ -313,12 +316,12 @@ async function createActivity(): Promise<Activity | undefined> {
|
|||
switch (settings.store.timestampMode) {
|
||||
case TimestampMode.NOW:
|
||||
activity.timestamps = {
|
||||
start: Math.floor(Date.now() / 1000)
|
||||
start: Date.now()
|
||||
};
|
||||
break;
|
||||
case TimestampMode.TIME:
|
||||
activity.timestamps = {
|
||||
start: Math.floor(Date.now() / 1000) - (new Date().getHours() * 3600) - (new Date().getMinutes() * 60) - new Date().getSeconds()
|
||||
start: Date.now() - (new Date().getHours() * 3600 + new Date().getMinutes() * 60 + new Date().getSeconds()) * 1000
|
||||
};
|
||||
break;
|
||||
case TimestampMode.CUSTOM:
|
||||
|
@ -386,15 +389,36 @@ async function setRpc(disable?: boolean) {
|
|||
export default definePlugin({
|
||||
name: "CustomRPC",
|
||||
description: "Allows you to set a custom rich presence.",
|
||||
authors: [Devs.captain, Devs.AutumnVN],
|
||||
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
|
||||
start: setRpc,
|
||||
stop: () => setRpc(true),
|
||||
settings,
|
||||
|
||||
settingsAboutComponent: () => {
|
||||
const activity = useAwaiter(createActivity);
|
||||
const gameActivityEnabled = StatusSettingsStores.ShowCurrentGame.useSetting();
|
||||
const { profileThemeStyle } = useProfileThemeStyle({});
|
||||
|
||||
return (
|
||||
<>
|
||||
{!gameActivityEnabled && (
|
||||
<ErrorCard
|
||||
className={classes(Margins.top16, Margins.bottom16)}
|
||||
style={{ padding: "1em" }}
|
||||
>
|
||||
<Forms.FormTitle>Notice</Forms.FormTitle>
|
||||
<Forms.FormText>Game activity isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText>
|
||||
|
||||
<Button
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
className={Margins.top8}
|
||||
onClick={() => StatusSettingsStores.ShowCurrentGame.updateSetting(true)}
|
||||
>
|
||||
Enable
|
||||
</Button>
|
||||
</ErrorCard>
|
||||
)}
|
||||
|
||||
<Forms.FormText>
|
||||
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
|
||||
get the application ID.
|
||||
|
@ -405,8 +429,10 @@ export default definePlugin({
|
|||
<Forms.FormText>
|
||||
If you want to use image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and select "Copy image address".
|
||||
</Forms.FormText>
|
||||
<Forms.FormDivider />
|
||||
<div style={{ width: "284px" }} className={Colors.profileColors}>
|
||||
|
||||
<Forms.FormDivider className={Margins.top8} />
|
||||
|
||||
<div style={{ width: "284px", ...profileThemeStyle }}>
|
||||
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
|
||||
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
|
||||
application={{ id: settings.store.appID }}
|
||||
|
|
5
src/plugins/customidle/README.md
Normal file
5
src/plugins/customidle/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# CustomIdle
|
||||
|
||||
Lets you change the time until your status gets automatically set to idle. You can also prevent idling altogether.
|
||||
|
||||
![Plugin Configuration](https://github.com/Vendicated/Vencord/assets/45801973/4e5259b2-18e0-42e5-b69f-efc672ce1e0b)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue