mirror of
https://github.com/Vendicated/Vencord.git
synced 2025-02-25 07:48:32 +00:00
Merge branch 'Vendicated:main' into main
This commit is contained in:
commit
b098286197
168 changed files with 6653 additions and 2460 deletions
|
@ -37,7 +37,7 @@
|
|||
" * Vencord, a modification for Discord's desktop app",
|
||||
{
|
||||
"pattern": " \\* Copyright \\(c\\) \\d{4}",
|
||||
"template": " * Copyright (c) 2022 Vendicated and contributors"
|
||||
"template": " * Copyright (c) 2023 Vendicated and contributors"
|
||||
},
|
||||
" *",
|
||||
" * This program is free software: you can redistribute it and/or modify",
|
||||
|
@ -82,11 +82,13 @@
|
|||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"no-duplicate-imports": "error",
|
||||
"no-extra-semi": "error",
|
||||
"consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }],
|
||||
"dot-notation": "error",
|
||||
"no-useless-escape": ["error", {
|
||||
"no-useless-escape": [
|
||||
"error",
|
||||
{
|
||||
"extra": "i"
|
||||
}],
|
||||
}
|
||||
],
|
||||
"no-fallthrough": "error",
|
||||
"for-direction": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
|
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
22
.github/ISSUE_TEMPLATE/blank.yml
vendored
Normal file
22
.github/ISSUE_TEMPLATE/blank.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
name: Blank Template
|
||||
description: Use this only if your issue does not fit into another template. **DO NOT ASK FOR SUPPORT OR REQUEST PLUGINS**
|
||||
labels: []
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: info-sec
|
||||
attributes:
|
||||
label: Tell us all about it.
|
||||
description: Go nuts, let us know what you're wanting to bring attention to.
|
||||
placeholder: ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: agreement-check
|
||||
attributes:
|
||||
label: Request Agreement
|
||||
description: DO NOT USE THIS TEMPLATE FOR SUPPORT OR PLUGIN REQUESTS!!! For Support, **join our Discord**. For plugin requests, **use discussions**
|
||||
options:
|
||||
- label: This is not a support or plugin request
|
||||
required: true
|
66
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
66
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
name: Bug/Crash Report
|
||||
description: Create a bug or crash report for Vencord
|
||||
labels: [bug]
|
||||
title: "[Bug] <title>"
|
||||
|
||||
body:
|
||||
- type: input
|
||||
id: discord
|
||||
attributes:
|
||||
label: Discord Account
|
||||
description: Who on Discord is making this request? Not required but encouraged for easier follow-up
|
||||
placeholder: username#0000
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: What happens when the bug or crash occurs?
|
||||
description: Where does this bug or crash occur, when does it occur, etc.
|
||||
placeholder: The bug/crash happens sometimes when I do ..., causing this to not work/the app to crash. I think it happens because of ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behaviour
|
||||
attributes:
|
||||
label: What is the expected behaviour?
|
||||
description: Simply detail what the expected behaviour is.
|
||||
placeholder: I expect Vencord/Discord to open the ... page instead of ..., it prevents me from doing ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps-to-take
|
||||
attributes:
|
||||
label: How do you recreate this bug or crash?
|
||||
description: Give us a list of steps in order to recreate the bug or crash.
|
||||
placeholder: |
|
||||
1. Do ...
|
||||
2. Then ...
|
||||
3. Do this ..., ... and then ...
|
||||
4. Observe "the bug" or "the crash"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: crash-log
|
||||
attributes:
|
||||
label: Errors
|
||||
description: Open the Developer Console with Ctrl/Cmd + Shift + i. Then look for any red errors (Ignore network errors like Failed to load resource) and paste them between the "```".
|
||||
value: |
|
||||
```
|
||||
Replace this text with your crash-log.
|
||||
```
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: agreement-check
|
||||
attributes:
|
||||
label: Request Agreement
|
||||
description: We only accept reports for bugs that happen on Discord Stable. Canary and PTB are Development branches and may be unstable
|
||||
options:
|
||||
- label: I am using Discord Stable or tried on Stable and this bug happens there as well
|
||||
required: true
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Vencord Support Server
|
||||
url: https://discord.gg/D9uwnFnqmd
|
||||
about: If you need help regarding Vencord, please join our support server!
|
||||
- name: Vencord Installer
|
||||
url: https://github.com/Vencord/Installer
|
||||
about: You can find the Vencord Installer here
|
32
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
32
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
name: Feature Request
|
||||
description: Create a feature request for Vencord. To request new plugins, please use the Discussions tab
|
||||
labels: [enhancement]
|
||||
title: "[Feature Request] <title>"
|
||||
|
||||
body:
|
||||
- type: input
|
||||
id: discord
|
||||
attributes:
|
||||
label: Discord Account
|
||||
description: Who on Discord is making this request? Not required but encouraged for easier follow-up
|
||||
placeholder: username#0000
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: feature-basic-description
|
||||
attributes:
|
||||
label: What is it that you'd like to see?
|
||||
description: Describe the feature you want added as detailed as possible
|
||||
placeholder: I think ... would be a cool feature to add. This would be awesome, thanks!
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: agreement-check
|
||||
attributes:
|
||||
label: Request Agreement
|
||||
description: DO NOT USE THIS TEMPLATE FOR PLUGIN REQUESTS!!! For plugin requests, **use discussions**
|
||||
options:
|
||||
- label: This is not a plugin request
|
||||
required: true
|
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
|
@ -34,28 +34,19 @@ jobs:
|
|||
- name: Build web
|
||||
run: pnpm buildWeb --standalone
|
||||
|
||||
- name: Sign firefox extension
|
||||
run: |
|
||||
pnpx web-ext sign --api-key $WEBEXT_USER --api-secret $WEBEXT_SECRET --channel=unlisted
|
||||
env:
|
||||
WEBEXT_USER: ${{ secrets.WEBEXT_USER }}
|
||||
WEBEXT_SECRET: ${{ secrets.WEBEXT_SECRET }}
|
||||
|
||||
- name: Build
|
||||
run: pnpm build --standalone
|
||||
|
||||
- name: Rename extensions for more user friendliness
|
||||
- name: Clean up obsolete files
|
||||
run: |
|
||||
mv dist/*.xpi dist/Vencord-for-Firefox.xpi
|
||||
mv dist/extension-v3.zip dist/Vencord-for-Chrome-and-Edge.zip
|
||||
rm -rf dist/extension-v2-unpacked dist/extension-v2.zip
|
||||
rm -rf dist/extension* Vencord.user.css
|
||||
|
||||
- name: Get some values needed for the release
|
||||
id: release_values
|
||||
run: |
|
||||
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Devbuild as release
|
||||
- name: Upload DevBuild as release
|
||||
run: |
|
||||
gh release upload devbuild --clobber dist/*
|
||||
gh release edit devbuild --title "DevBuild $RELEASE_TAG"
|
||||
|
@ -63,13 +54,15 @@ jobs:
|
|||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ env.release_tag }}
|
||||
|
||||
- name: Upload Devbuild to builds repo
|
||||
- name: Upload DevBuild to builds repo
|
||||
run: |
|
||||
git config --global user.name "$USERNAME"
|
||||
git config --global user.email actions@github.com
|
||||
|
||||
git clone https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git upload
|
||||
cd upload
|
||||
|
||||
GLOBIGNORE=.git:.gitignore:README.md:LICENSE
|
||||
rm -rf *
|
||||
cp -r ../dist/* .
|
||||
|
||||
|
@ -78,6 +71,5 @@ jobs:
|
|||
git push --force https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git
|
||||
env:
|
||||
API_TOKEN: ${{ secrets.BUILDS_TOKEN }}
|
||||
GLOBIGNORE: .git:.gitignore:README.md:LICENSE
|
||||
GH_REPO: Vencord/builds
|
||||
USERNAME: GitHub-Actions
|
||||
|
|
61
.github/workflows/publish.yml
vendored
Normal file
61
.github/workflows/publish.yml
vendored
Normal file
|
@ -0,0 +1,61 @@
|
|||
name: Release Browser Extension
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
Publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: check that tag matches package.json version
|
||||
run: |
|
||||
pkg_version="v$(jq -r .version < package.json)"
|
||||
if [[ "${{ github.ref_name }}" != "$pkg_version" ]]; then
|
||||
echo "Tag ${{ github.ref_name }} does not match package.json version $pkg_version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 19
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 19
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build web
|
||||
run: pnpm buildWeb --standalone
|
||||
|
||||
- name: Publish extension
|
||||
run: |
|
||||
cd dist/extension-unpacked
|
||||
|
||||
# Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later
|
||||
EXIT_CODE=0
|
||||
|
||||
# Chrome
|
||||
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$?
|
||||
|
||||
# Firefox
|
||||
npm i -g web-ext@7.4.0 web-ext-submit@7.4.0
|
||||
web-ext-submit || EXIT_CODE=$?
|
||||
|
||||
exit $EXIT_CODE
|
||||
env:
|
||||
# Chrome
|
||||
EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
|
||||
CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
|
||||
CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||
REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||
|
||||
# Firefox
|
||||
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
|
||||
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,6 +5,7 @@ node_modules
|
|||
vencord_installer
|
||||
|
||||
.idea
|
||||
.DS_Store
|
||||
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
|
6
.stylelintrc.json
Normal file
6
.stylelintrc.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"indentation": 4
|
||||
}
|
||||
}
|
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"EditorConfig.EditorConfig",
|
||||
"pmneo.tsimporter",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eamodio.gitlens",
|
||||
"EditorConfig.EditorConfig",
|
||||
"ExodiusStudios.comment-anchors",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"GregorBiswanger.json2ts",
|
||||
"eamodio.gitlens",
|
||||
"kamikillerto.vscode-colorize"
|
||||
"stylelint.vscode-stylelint"
|
||||
]
|
||||
}
|
||||
|
|
43
README.md
43
README.md
|
@ -1,51 +1,30 @@
|
|||
# Vencord
|
||||
|
||||
A Discord client mod that does things differently
|
||||
The cutest Discord client mod
|
||||
|
||||
## Features
|
||||
|
||||
- Super easy to install, no git or node or anything else required
|
||||
- Many plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
||||
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, custom slash commands, ShowHiddenChannels
|
||||
- Browser Support: Run Vencord in your Browser via extension or UserScript
|
||||
- Super easy to install (one click installer)
|
||||
- 90+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
||||
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
|
||||
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
||||
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
||||
- Works in all Electron versions (Confirmed working on versions 13-23)
|
||||
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
||||
|
||||
## Installing / Uninstalling
|
||||
|
||||
If you're just a normal user, use [our simple gui installer!](https://github.com/Vendicated/VencordInstaller#usage)
|
||||
|
||||
If you're a power user who wants to contribute and make plugins or just want to build from source and install manually, read [Megu's Installation Guide!](docs/1_INSTALLING.md)
|
||||
[](https://github.com/Vencord/Installer#usage)
|
||||
|
||||
## Installing on Browser
|
||||
|
||||
[](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/)
|
||||
[](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
|
||||
|
||||
Or install the browser extension for
|
||||
- [](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip)
|
||||
- [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that QuickCSS, shiki and other plugins making use of external resources will not work with the UserScript.
|
||||
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that QuickCSS and plugins making use of external resources will not work with the UserScript.
|
||||
|
||||
## Building from Source
|
||||
|
||||
You may also build them from source, to do that do the same steps as in the manual regular install method,
|
||||
except run `pnpm buildWeb` instead of `pnpm build`, and your outputs will be in the dist folder
|
||||
|
||||
```sh
|
||||
pnpm buildWeb
|
||||
```
|
||||
|
||||
You will find the built extension at dist/extension.zip. Now just install this extension in your Browser
|
||||
|
||||
## Installing Plugins
|
||||
|
||||
> **Note**
|
||||
> You can only use 3rd party plugins in the manual Vencord install for now.
|
||||
|
||||
Vencord comes with a bunch of plugins out of the box!
|
||||
|
||||
However, if you want to install your own ones, create a `userplugins` folder in the `src` directory and create or clone your plugins in there.
|
||||
Don't forget to rebuild!
|
||||
|
||||
Want to learn how to create your own plugin, and maybe PR it into Vencord? See the [Contributing](#contributing) section below!
|
||||
See the docs folder
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Linnea Gräf
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function setContentTypeOnStylesheets(details) {
|
||||
if (details.type === "stylesheet") {
|
||||
details.responseHeaders = details.responseHeaders.filter(it => it.name.toLowerCase() !== 'content-type');
|
||||
details.responseHeaders.push({ name: "Content-Type", value: "text/css" });
|
||||
}
|
||||
return { responseHeaders: details.responseHeaders };
|
||||
}
|
||||
|
||||
var cspHeaders = [
|
||||
"content-security-policy",
|
||||
"content-security-policy-report-only",
|
||||
];
|
||||
|
||||
function removeCSPHeaders(details) {
|
||||
return {
|
||||
responseHeaders: details.responseHeaders.filter(header =>
|
||||
!cspHeaders.includes(header.name.toLowerCase()))
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
browser.webRequest.onHeadersReceived.addListener(
|
||||
setContentTypeOnStylesheets, { urls: ["https://raw.githubusercontent.com/*"] }, ["blocking", "responseHeaders"]
|
||||
);
|
||||
|
||||
browser.webRequest.onHeadersReceived.addListener(
|
||||
removeCSPHeaders, { urls: ["https://raw.githubusercontent.com/*", "*://*.discord.com/*"] }, ["blocking", "responseHeaders"]
|
||||
);
|
BIN
browser/icon.png
Normal file
BIN
browser/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
|
@ -1,10 +1,14 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"minimum_chrome_version": "91",
|
||||
|
||||
"name": "Vencord Web",
|
||||
"description": "Yeee",
|
||||
"version": "1.0.0",
|
||||
"description": "The cutest Discord mod now in your browser",
|
||||
"author": "Vendicated",
|
||||
"homepage_url": "https://github.com/Vendicated/Vencord",
|
||||
"icons": {
|
||||
"128": "icon.png"
|
||||
},
|
||||
|
||||
"host_permissions": [
|
||||
"*://*.discord.com/*",
|
||||
|
@ -36,5 +40,12 @@
|
|||
"path": "modifyResponseHeaders.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "vencord-firefox@vendicated.dev",
|
||||
"strict_min_version": "109.0"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Vencord Web",
|
||||
"description": "The Vencord Client Mod for Discord Web.",
|
||||
"version": "1.0.0",
|
||||
"author": "Vendicated",
|
||||
"homepage_url": "https://github.com/Vendicated/Vencord",
|
||||
"permissions": [
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"*://*.discord.com/*",
|
||||
"https://raw.githubusercontent.com/*"
|
||||
],
|
||||
"content_scripts": [
|
||||
{
|
||||
"run_at": "document_start",
|
||||
"matches": ["*://*.discord.com/*"],
|
||||
"js": ["content.js"]
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"],
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
// FIXME: Delete this soon, for now it is needed so people can update
|
||||
|
||||
import("./scripts/build/build.mjs");
|
|
@ -183,7 +183,6 @@ In `index.js`:
|
|||
|
||||
```js
|
||||
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
|
||||
require("../app.asar");
|
||||
```
|
||||
|
||||
And in `package.json`:
|
||||
|
|
44
package.json
44
package.json
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.0.1",
|
||||
"description": "A Discord client mod that does things differently",
|
||||
"version": "1.0.6",
|
||||
"description": "The cutest Discord client mod",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
|
@ -20,33 +20,33 @@
|
|||
"scripts": {
|
||||
"build": "node scripts/build/build.mjs",
|
||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||
"inject": "node scripts/patcher/install.js",
|
||||
"inject": "node scripts/runInstaller.mjs",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"lint-styles": "stylelint \"src/**/*.css\"",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"test": "pnpm lint && pnpm build && pnpm testTsc",
|
||||
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc",
|
||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
||||
"testTsc": "tsc --noEmit",
|
||||
"uninject": "node scripts/patcher/uninstall.js",
|
||||
"uninject": "node scripts/runInstaller.mjs",
|
||||
"watch": "node scripts/build/build.mjs --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vap/core": "0.0.12",
|
||||
"@vap/shiki": "0.10.3",
|
||||
"fflate": "^0.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.0.2",
|
||||
"@types/lodash": "^4.14.0",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/yazl": "^2.4.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
||||
"@typescript-eslint/parser": "^5.44.0",
|
||||
"@vap/core": "0.0.12",
|
||||
"@vap/shiki": "0.10.3",
|
||||
"console-menu": "^0.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.49.0",
|
||||
"diff": "^5.1.0",
|
||||
"discord-types": "^1.3.26",
|
||||
"esbuild": "^0.15.16",
|
||||
"esbuild": "^0.15.18",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
|
@ -55,10 +55,12 @@
|
|||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"highlight.js": "10.6.0",
|
||||
"moment": "^2.29.4",
|
||||
"puppeteer-core": "^19.3.0",
|
||||
"puppeteer-core": "^19.6.0",
|
||||
"standalone-electron-types": "^1.0.0",
|
||||
"type-fest": "^3.3.0",
|
||||
"typescript": "^4.9.3"
|
||||
"stylelint": "^14.16.1",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"type-fest": "^3.5.3",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"packageManager": "pnpm@7.13.4",
|
||||
"pnpm": {
|
||||
|
@ -68,7 +70,8 @@
|
|||
},
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
"eslint-plugin-import"
|
||||
"eslint-plugin-import",
|
||||
"eslint"
|
||||
]
|
||||
},
|
||||
"allowedDeprecatedVersions": {
|
||||
|
@ -84,5 +87,8 @@
|
|||
"overwriteDest": true
|
||||
},
|
||||
"sourceDir": "./dist/extension-v2-unpacked"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
|
1125
pnpm-lock.yaml
generated
1125
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -82,10 +82,19 @@ async function buildPluginZip(target, files, shouldZip) {
|
|||
const entries = {
|
||||
"dist/Vencord.js": await readFile("dist/browser.js"),
|
||||
"dist/Vencord.css": await readFile("dist/browser.css"),
|
||||
...Object.fromEntries(await Promise.all(files.map(async f => [
|
||||
(f.startsWith("manifest") ? "manifest.json" : f),
|
||||
await readFile(join("browser", f))
|
||||
]))),
|
||||
...Object.fromEntries(await Promise.all(files.map(async f => {
|
||||
let content = await readFile(join("browser", f));
|
||||
if (f.startsWith("manifest")) {
|
||||
const json = JSON.parse(content.toString("utf-8"));
|
||||
json.version = PackageJSON.version;
|
||||
content = new TextEncoder().encode(JSON.stringify(json));
|
||||
}
|
||||
|
||||
return [
|
||||
f.startsWith("manifest") ? "manifest.json" : f,
|
||||
content
|
||||
];
|
||||
}))),
|
||||
};
|
||||
|
||||
if (shouldZip) {
|
||||
|
@ -115,21 +124,22 @@ async function buildPluginZip(target, files, shouldZip) {
|
|||
}
|
||||
}
|
||||
|
||||
const cssText = "`" + readFileSync("dist/Vencord.user.css", "utf-8").replaceAll("`", "\\`") + "`";
|
||||
const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => {
|
||||
const cssRuntime = `
|
||||
;document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(
|
||||
Object.assign(document.createElement("style"), {
|
||||
textContent: ${cssText},
|
||||
textContent: \`${content.replaceAll("`", "\\`")}\`,
|
||||
id: "vencord-css-core"
|
||||
}),
|
||||
{ once: true }
|
||||
));
|
||||
})
|
||||
), { once: true });
|
||||
`;
|
||||
|
||||
return appendFile("dist/Vencord.user.js", cssRuntime);
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
appendFile("dist/Vencord.user.js", cssRuntime),
|
||||
buildPluginZip("extension-v3.zip", ["modifyResponseHeaders.json", "content.js", "manifestv3.json"], true),
|
||||
buildPluginZip("extension-v2.zip", ["background.js", "content.js", "manifestv2.json"], true),
|
||||
buildPluginZip("extension-v2-unpacked", ["background.js", "content.js", "manifestv2.json"], false),
|
||||
appendCssRuntime,
|
||||
buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true),
|
||||
buildPluginZip("extension-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
|
||||
]);
|
||||
|
||||
|
|
|
@ -68,11 +68,12 @@ export const globPlugins = {
|
|||
if (!existsSync(`./src/${dir}`)) continue;
|
||||
const files = await readdir(`./src/${dir}`);
|
||||
for (const file of files) {
|
||||
if (file.startsWith(".")) continue;
|
||||
if (file === "index.ts") {
|
||||
continue;
|
||||
}
|
||||
const mod = `p${i}`;
|
||||
code += `import ${mod} from "./${dir}/${file.replace(/.tsx?$/, "")}";\n`;
|
||||
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
|
||||
plugins += `[${mod}.name]:${mod},\n`;
|
||||
i++;
|
||||
}
|
||||
|
|
20
scripts/checkNodeVersion.js
Normal file
20
scripts/checkNodeVersion.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
if (Number(process.versions.node.split(".")[0]) < 18)
|
||||
throw `Your node version (${process.version}) is too old, please update to v18 or higher https://nodejs.org/en/download/`;
|
|
@ -1,357 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const path = require("path");
|
||||
const readline = require("readline");
|
||||
const fs = require("fs");
|
||||
const menu = require("console-menu");
|
||||
|
||||
function pathToBranch(dir) {
|
||||
dir = dir.toLowerCase();
|
||||
if (dir.endsWith("development")) {
|
||||
return "development";
|
||||
}
|
||||
if (dir.endsWith("canary")) {
|
||||
return "canary";
|
||||
}
|
||||
if (dir.endsWith("ptb")) {
|
||||
return "ptb";
|
||||
}
|
||||
return "stable";
|
||||
}
|
||||
|
||||
const BRANCH_NAMES = [
|
||||
"Discord",
|
||||
"DiscordPTB",
|
||||
"DiscordCanary",
|
||||
"DiscordDevelopment",
|
||||
"discord",
|
||||
"discordptb",
|
||||
"discordcanary",
|
||||
"discorddevelopment",
|
||||
"discord-ptb",
|
||||
"discord-canary",
|
||||
"discord-development",
|
||||
// Flatpak
|
||||
"com.discordapp.Discord",
|
||||
"com.discordapp.DiscordPTB",
|
||||
"com.discordapp.DiscordCanary",
|
||||
"com.discordapp.DiscordDevelopment",
|
||||
];
|
||||
|
||||
const MACOS_DISCORD_DIRS = [
|
||||
"Discord.app",
|
||||
"Discord PTB.app",
|
||||
"Discord Canary.app",
|
||||
"Discord Development.app",
|
||||
];
|
||||
|
||||
if (process.platform === "linux" && process.env.SUDO_USER) {
|
||||
process.env.HOME = fs
|
||||
.readFileSync("/etc/passwd", "utf-8")
|
||||
.match(new RegExp(`^${process.env.SUDO_USER}.+$`, "m"))[0]
|
||||
.split(":")[5];
|
||||
}
|
||||
|
||||
const LINUX_DISCORD_DIRS = [
|
||||
"/usr/share",
|
||||
"/usr/lib64",
|
||||
"/opt",
|
||||
`${process.env.HOME}/.local/share`,
|
||||
`${process.env.HOME}/.dvm`,
|
||||
"/var/lib/flatpak/app",
|
||||
`${process.env.HOME}/.local/share/flatpak/app`,
|
||||
];
|
||||
|
||||
const FLATPAK_NAME_MAPPING = {
|
||||
DiscordCanary: "discord-canary",
|
||||
DiscordPTB: "discord-ptb",
|
||||
DiscordDevelopment: "discord-development",
|
||||
Discord: "discord",
|
||||
};
|
||||
|
||||
const ENTRYPOINT = path
|
||||
.join(process.cwd(), "dist", "patcher.js")
|
||||
.replace(/\\/g, "/");
|
||||
|
||||
function question(question) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
terminal: false,
|
||||
});
|
||||
|
||||
return new Promise(resolve => {
|
||||
rl.question(question, answer => {
|
||||
rl.close();
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function getMenuItem(installations) {
|
||||
const menuItems = installations.map(info => ({
|
||||
title: info.patched ? "[MODIFIED] " + info.location : info.location,
|
||||
info,
|
||||
}));
|
||||
|
||||
const result = await menu(
|
||||
[
|
||||
...menuItems,
|
||||
{ title: "Specify custom path", info: "custom" },
|
||||
{ title: "Exit without patching", exit: true }
|
||||
],
|
||||
{
|
||||
header: "Select a Discord installation to patch:",
|
||||
border: true,
|
||||
helpMessage:
|
||||
"Use the up/down arrow keys to select an option. " +
|
||||
"Press ENTER to confirm.",
|
||||
}
|
||||
);
|
||||
|
||||
if (!result || !result.info || result.exit) {
|
||||
console.log("No installation selected.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (result.info === "custom") {
|
||||
const customPath = await question("Please enter the path: ");
|
||||
if (!customPath || !fs.existsSync(customPath)) {
|
||||
console.log("No such Path or not specifed.");
|
||||
process.exit();
|
||||
}
|
||||
|
||||
const resourceDir = path.join(customPath, "resources");
|
||||
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
||||
console.log("Unsupported Install. resources/app.asar not found");
|
||||
process.exit();
|
||||
}
|
||||
|
||||
const appDir = path.join(resourceDir, "app");
|
||||
result.info = {
|
||||
branch: "unknown",
|
||||
patched: fs.existsSync(appDir),
|
||||
location: customPath,
|
||||
versions: [{
|
||||
path: appDir,
|
||||
name: null
|
||||
}],
|
||||
arch: process.platform === "linux" ? "linux" : "win32",
|
||||
isFlatpak: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (result.info.patched) {
|
||||
const answer = await question(
|
||||
"This installation has already been modified. Overwrite? [Y/n]: "
|
||||
);
|
||||
|
||||
if (!["y", "yes", "yeah", ""].includes(answer.toLowerCase())) {
|
||||
console.log("Not patching.");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
return result.info;
|
||||
}
|
||||
|
||||
function getWindowsDirs() {
|
||||
const dirs = [];
|
||||
for (const dir of fs.readdirSync(process.env.LOCALAPPDATA)) {
|
||||
if (!BRANCH_NAMES.includes(dir)) continue;
|
||||
|
||||
const location = path.join(process.env.LOCALAPPDATA, dir);
|
||||
if (!fs.statSync(location).isDirectory()) continue;
|
||||
|
||||
const appDirs = fs
|
||||
.readdirSync(location, { withFileTypes: true })
|
||||
.filter(file => file.isDirectory())
|
||||
.filter(file => file.name.startsWith("app-"))
|
||||
.map(file => path.join(location, file.name));
|
||||
|
||||
const versions = [];
|
||||
let patched = false;
|
||||
|
||||
for (const fqAppDir of appDirs) {
|
||||
const resourceDir = path.join(fqAppDir, "resources");
|
||||
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
||||
continue;
|
||||
}
|
||||
const appDir = path.join(resourceDir, "app");
|
||||
if (fs.existsSync(appDir)) {
|
||||
patched = true;
|
||||
}
|
||||
versions.push({
|
||||
path: appDir,
|
||||
name: /app-([0-9.]+)/.exec(fqAppDir)[1],
|
||||
});
|
||||
}
|
||||
|
||||
if (appDirs.length) {
|
||||
dirs.push({
|
||||
branch: dir,
|
||||
patched,
|
||||
location,
|
||||
versions,
|
||||
arch: "win32",
|
||||
flatpak: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function getDarwinDirs() {
|
||||
const dirs = [];
|
||||
for (const dir of fs.readdirSync("/Applications")) {
|
||||
if (!MACOS_DISCORD_DIRS.includes(dir)) continue;
|
||||
|
||||
const location = path.join("/Applications", dir, "Contents");
|
||||
if (!fs.existsSync(location)) continue;
|
||||
if (!fs.statSync(location).isDirectory()) continue;
|
||||
|
||||
const appDirs = fs
|
||||
.readdirSync(location, { withFileTypes: true })
|
||||
.filter(file => file.isDirectory())
|
||||
.filter(file => file.name.startsWith("Resources"))
|
||||
.map(file => path.join(location, file.name));
|
||||
|
||||
const versions = [];
|
||||
let patched = false;
|
||||
|
||||
for (const resourceDir of appDirs) {
|
||||
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
||||
continue;
|
||||
}
|
||||
const appDir = path.join(resourceDir, "app");
|
||||
if (fs.existsSync(appDir)) {
|
||||
patched = true;
|
||||
}
|
||||
|
||||
versions.push({
|
||||
path: appDir,
|
||||
name: null, // MacOS installs have no version number
|
||||
});
|
||||
}
|
||||
|
||||
if (appDirs.length) {
|
||||
dirs.push({
|
||||
branch: dir,
|
||||
patched,
|
||||
location,
|
||||
versions,
|
||||
arch: "win32",
|
||||
});
|
||||
}
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function getLinuxDirs() {
|
||||
const dirs = [];
|
||||
for (const dir of LINUX_DISCORD_DIRS) {
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
for (const branch of fs.readdirSync(dir)) {
|
||||
if (!BRANCH_NAMES.includes(branch)) continue;
|
||||
|
||||
const location = path.join(dir, branch);
|
||||
if (!fs.statSync(location).isDirectory()) continue;
|
||||
|
||||
const isFlatpak = location.includes("/flatpak/");
|
||||
|
||||
let appDirs = [];
|
||||
|
||||
if (isFlatpak) {
|
||||
const fqDir = path.join(location, "current", "active", "files");
|
||||
if (!/com\.discordapp\.(\w+)\//.test(fqDir)) continue;
|
||||
const branchName = /com\.discordapp\.(\w+)\//.exec(fqDir)[1];
|
||||
if (!Object.keys(FLATPAK_NAME_MAPPING).includes(branchName)) {
|
||||
continue;
|
||||
}
|
||||
const appDir = path.join(
|
||||
fqDir,
|
||||
FLATPAK_NAME_MAPPING[branchName]
|
||||
);
|
||||
|
||||
if (!fs.existsSync(appDir)) continue;
|
||||
if (!fs.statSync(appDir).isDirectory()) continue;
|
||||
|
||||
const resourceDir = path.join(appDir, "resources");
|
||||
|
||||
appDirs.push(resourceDir);
|
||||
} else {
|
||||
appDirs = fs
|
||||
.readdirSync(location, { withFileTypes: true })
|
||||
.filter(file => file.isDirectory())
|
||||
.filter(
|
||||
file =>
|
||||
file.name.startsWith("app-") ||
|
||||
file.name === "resources"
|
||||
)
|
||||
.map(file => path.join(location, file.name));
|
||||
}
|
||||
|
||||
const versions = [];
|
||||
let patched = false;
|
||||
|
||||
for (const resourceDir of appDirs) {
|
||||
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
|
||||
continue;
|
||||
}
|
||||
const appDir = path.join(resourceDir, "app");
|
||||
if (fs.existsSync(appDir)) {
|
||||
patched = true;
|
||||
}
|
||||
|
||||
const version = /app-([0-9.]+)/.exec(resourceDir);
|
||||
|
||||
versions.push({
|
||||
path: appDir,
|
||||
name: version && version.length > 1 ? version[1] : null,
|
||||
});
|
||||
}
|
||||
|
||||
if (appDirs.length) {
|
||||
dirs.push({
|
||||
branch,
|
||||
patched,
|
||||
location,
|
||||
versions,
|
||||
arch: "linux",
|
||||
isFlatpak,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pathToBranch,
|
||||
BRANCH_NAMES,
|
||||
MACOS_DISCORD_DIRS,
|
||||
LINUX_DISCORD_DIRS,
|
||||
FLATPAK_NAME_MAPPING,
|
||||
ENTRYPOINT,
|
||||
question,
|
||||
getMenuItem,
|
||||
getWindowsDirs,
|
||||
getDarwinDirs,
|
||||
getLinuxDirs,
|
||||
};
|
|
@ -1,219 +0,0 @@
|
|||
#!/usr/bin/node
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
console.log("\nVencord Installer\n");
|
||||
|
||||
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
|
||||
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.join(process.cwd(), "dist", "patcher.js"))) {
|
||||
console.log("You need to build the project first. Run:", "pnpm build");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const {
|
||||
getMenuItem,
|
||||
getWindowsDirs,
|
||||
getDarwinDirs,
|
||||
getLinuxDirs,
|
||||
ENTRYPOINT,
|
||||
question,
|
||||
pathToBranch
|
||||
} = require("./common");
|
||||
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
install(getWindowsDirs());
|
||||
break;
|
||||
case "darwin":
|
||||
install(getDarwinDirs());
|
||||
break;
|
||||
case "linux":
|
||||
install(getLinuxDirs());
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown OS");
|
||||
break;
|
||||
}
|
||||
|
||||
async function install(installations) {
|
||||
const selected = await getMenuItem(installations);
|
||||
|
||||
// Attempt to give flatpak perms
|
||||
if (selected.isFlatpak) {
|
||||
try {
|
||||
const cwd = process.cwd();
|
||||
const globalCmd = `flatpak override ${selected.branch} --filesystem=${cwd}`;
|
||||
const userCmd = `flatpak override --user ${selected.branch} --filesystem=${cwd}`;
|
||||
const cmd = selected.location.startsWith("/home")
|
||||
? userCmd
|
||||
: globalCmd;
|
||||
execSync(cmd);
|
||||
console.log("Gave write perms to Discord Flatpak.");
|
||||
} catch (e) {
|
||||
console.log("Failed to give write perms to Discord Flatpak.");
|
||||
console.log(
|
||||
"Try running this script as an administrator:",
|
||||
"sudo pnpm inject"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const answer = await question(
|
||||
`Would you like to allow ${selected.branch} to talk to org.freedesktop.Flatpak?\n` +
|
||||
"This is essentially full host access but necessary to spawn git. Without it, the updater will not work\n" +
|
||||
"Consider using the http based updater (using the gui installer) instead if you want to maintain the sandbox.\n" +
|
||||
"[y/N]: "
|
||||
);
|
||||
|
||||
if (["y", "yes", "yeah"].includes(answer.toLowerCase())) {
|
||||
try {
|
||||
const globalCmd = `flatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
|
||||
const userCmd = `flatpak override --user ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
|
||||
const cmd = selected.location.startsWith("/home")
|
||||
? userCmd
|
||||
: globalCmd;
|
||||
execSync(cmd);
|
||||
console.log("Sucessfully gave talk permission");
|
||||
} catch (err) {
|
||||
console.error("Failed to give talk permission\n", err);
|
||||
}
|
||||
} else {
|
||||
console.log(`Not giving full host access. If you change your mind later, you can run:\nflatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`);
|
||||
}
|
||||
}
|
||||
|
||||
const useNewMethod = pathToBranch(selected.branch) !== "stable";
|
||||
|
||||
for (const version of selected.versions) {
|
||||
|
||||
const dir = useNewMethod ? path.join(version.path, "..") : version.path;
|
||||
|
||||
// Check if we have write perms to the install directory...
|
||||
try {
|
||||
fs.accessSync(selected.location, fs.constants.W_OK);
|
||||
} catch (e) {
|
||||
console.error("No write access to", selected.location);
|
||||
console.error(
|
||||
"Make sure Discord isn't running. If that doesn't work,",
|
||||
"try running this script as an administrator:",
|
||||
"sudo pnpm inject"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (useNewMethod) {
|
||||
const appAsar = path.join(dir, "app.asar");
|
||||
const _appAsar = path.join(dir, "_app.asar");
|
||||
|
||||
if (fs.existsSync(_appAsar) && fs.existsSync(appAsar)) {
|
||||
console.log("This copy of Discord already seems to be patched...");
|
||||
console.log("Try running `pnpm uninject` first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
fs.renameSync(appAsar, _appAsar);
|
||||
} catch (err) {
|
||||
if (err.code === "EBUSY") {
|
||||
console.error(selected.branch, "is still running. Make sure you fully close it before running this script.");
|
||||
process.exit(1);
|
||||
}
|
||||
console.error("Failed to rename app.asar to _app.asar");
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
fs.mkdirSync(appAsar);
|
||||
} catch (err) {
|
||||
if (err.code === "EBUSY") {
|
||||
console.error(selected.branch, "is still running. Make sure you fully close it before running this script.");
|
||||
process.exit(1);
|
||||
}
|
||||
console.error("Failed to create app.asar folder");
|
||||
throw err;
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(appAsar, "index.js"),
|
||||
`require("${ENTRYPOINT}");`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(appAsar, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "discord",
|
||||
main: "index.js",
|
||||
})
|
||||
);
|
||||
|
||||
const requiredFiles = ["index.js", "package.json"];
|
||||
|
||||
if (requiredFiles.every(f => fs.existsSync(path.join(appAsar, f)))) {
|
||||
console.log(
|
||||
"Successfully patched",
|
||||
version.name
|
||||
? `${selected.branch} ${version.name}`
|
||||
: selected.branch
|
||||
);
|
||||
} else {
|
||||
console.log("Failed to patch", dir);
|
||||
console.log("Files in directory:", fs.readdirSync(appAsar));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (fs.existsSync(dir) && fs.lstatSync(dir).isDirectory()) {
|
||||
fs.rmSync(dir, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "index.js"),
|
||||
`require("${ENTRYPOINT}");`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "discord",
|
||||
main: "index.js",
|
||||
})
|
||||
);
|
||||
|
||||
const requiredFiles = ["index.js", "package.json"];
|
||||
|
||||
if (requiredFiles.every(f => fs.existsSync(path.join(dir, f)))) {
|
||||
console.log(
|
||||
"Successfully patched",
|
||||
version.name
|
||||
? `${selected.branch} ${version.name}`
|
||||
: selected.branch
|
||||
);
|
||||
} else {
|
||||
console.log("Failed to patch", dir);
|
||||
console.log("Files in directory:", fs.readdirSync(dir));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
#!/usr/bin/node
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
console.log("\nVencord Uninstaller\n");
|
||||
|
||||
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
|
||||
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const {
|
||||
getMenuItem,
|
||||
getWindowsDirs,
|
||||
getDarwinDirs,
|
||||
getLinuxDirs,
|
||||
pathToBranch,
|
||||
} = require("./common");
|
||||
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
uninstall(getWindowsDirs());
|
||||
break;
|
||||
case "darwin":
|
||||
uninstall(getDarwinDirs());
|
||||
break;
|
||||
case "linux":
|
||||
uninstall(getLinuxDirs());
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown OS");
|
||||
break;
|
||||
}
|
||||
|
||||
async function uninstall(installations) {
|
||||
const selected = await getMenuItem(installations);
|
||||
|
||||
const useNewMethod = pathToBranch(selected.branch) !== "stable";
|
||||
|
||||
for (const version of selected.versions) {
|
||||
const dir = useNewMethod ? path.join(version.path, "..") : version.path;
|
||||
|
||||
// Check if we have write perms to the install directory...
|
||||
try {
|
||||
fs.accessSync(selected.location, fs.constants.W_OK);
|
||||
} catch (e) {
|
||||
console.error("No write access to", selected.location);
|
||||
console.error(
|
||||
"Make sure Discord isn't running. If that doesn't work,",
|
||||
"try running this script as an administrator:",
|
||||
"sudo pnpm uninject"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (useNewMethod) {
|
||||
if (!fs.existsSync(path.join(dir, "_app.asar"))) {
|
||||
console.error(
|
||||
"Original app.asar (_app.asar) doesn't exist.",
|
||||
"Is your Discord installation corrupt? Try reinstalling Discord."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (fs.existsSync(path.join(dir, "app.asar"))) {
|
||||
try {
|
||||
fs.rmSync(path.join(dir, "app.asar"), { force: true, recursive: true });
|
||||
} catch (err) {
|
||||
console.error("Failed to delete app.asar folder");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
try {
|
||||
fs.renameSync(
|
||||
path.join(dir, "_app.asar"),
|
||||
path.join(dir, "app.asar")
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Failed to rename _app.asar to app.asar");
|
||||
throw err;
|
||||
}
|
||||
console.log(
|
||||
"Successfully unpatched",
|
||||
version.name
|
||||
? `${selected.branch} ${version.name}`
|
||||
: selected.branch
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmSync(dir, { recursive: true });
|
||||
}
|
||||
console.log(
|
||||
"Successfully unpatched",
|
||||
version.name
|
||||
? `${selected.branch} ${version.name}`
|
||||
: selected.branch
|
||||
);
|
||||
}
|
||||
}
|
128
scripts/runInstaller.mjs
Normal file
128
scripts/runInstaller.mjs
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./checkNodeVersion.js";
|
||||
|
||||
import { execFileSync, execSync } from "child_process";
|
||||
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { Readable } from "stream";
|
||||
import { finished } from "stream/promises";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const BASE_URL = "https://github.com/Vencord/Installer/releases/latest/download/";
|
||||
const INSTALLER_PATH_DARWIN = "VencordInstaller.app/Contents/MacOS/VencordInstaller";
|
||||
|
||||
const BASE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const FILE_DIR = join(BASE_DIR, "dist", "Installer");
|
||||
const ETAG_FILE = join(FILE_DIR, "etag.txt");
|
||||
|
||||
function getFilename() {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
return "VencordInstaller.exe";
|
||||
case "darwin":
|
||||
return "VencordInstaller.MacOS.zip";
|
||||
case "linux":
|
||||
return "VencordInstaller-" + (process.env.WAYLAND_DISPLAY ? "wayland" : "x11");
|
||||
default:
|
||||
throw new Error("Unsupported platform: " + process.platform);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureBinary() {
|
||||
const filename = getFilename();
|
||||
console.log("Downloading " + filename);
|
||||
|
||||
mkdirSync(FILE_DIR, { recursive: true });
|
||||
|
||||
const downloadName = join(FILE_DIR, filename);
|
||||
const outputFile = process.platform === "darwin"
|
||||
? join(FILE_DIR, "VencordInstaller")
|
||||
: downloadName;
|
||||
|
||||
const etag = existsSync(outputFile) && existsSync(ETAG_FILE)
|
||||
? readFileSync(ETAG_FILE, "utf-8")
|
||||
: null;
|
||||
|
||||
const res = await fetch(BASE_URL + filename, {
|
||||
headers: {
|
||||
"User-Agent": "Vencord (https://github.com/Vendicated/Vencord)",
|
||||
"If-None-Match": etag
|
||||
}
|
||||
});
|
||||
|
||||
if (res.status === 304) {
|
||||
console.log("Up to date, not redownloading!");
|
||||
return outputFile;
|
||||
}
|
||||
if (!res.ok)
|
||||
throw new Error(`Failed to download installer: ${res.status} ${res.statusText}`);
|
||||
|
||||
writeFileSync(ETAG_FILE, res.headers.get("etag"));
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
console.log("Unzipping...");
|
||||
const zip = new Uint8Array(await res.arrayBuffer());
|
||||
|
||||
const ff = await import("fflate");
|
||||
const bytes = ff.unzipSync(zip, {
|
||||
filter: f => f.name === INSTALLER_PATH_DARWIN
|
||||
})[INSTALLER_PATH_DARWIN];
|
||||
|
||||
writeFileSync(outputFile, bytes, { mode: 0o755 });
|
||||
|
||||
console.log("Overriding security policy for installer binary (this is required to run it)");
|
||||
console.log("xattr might error, that's okay");
|
||||
|
||||
const logAndRun = cmd => {
|
||||
console.log("Running", cmd);
|
||||
try {
|
||||
execSync(cmd);
|
||||
} catch { }
|
||||
};
|
||||
logAndRun(`sudo spctl --add '${outputFile}' --label "Vencord Installer"`);
|
||||
logAndRun(`sudo xattr -d com.apple.quarantine '${outputFile}'`);
|
||||
} else {
|
||||
// WHY DOES NODE FETCH RETURN A WEB STREAM OH MY GOD
|
||||
const body = Readable.fromWeb(res.body);
|
||||
await finished(body.pipe(createWriteStream(outputFile, {
|
||||
mode: 0o755,
|
||||
autoClose: true
|
||||
})));
|
||||
}
|
||||
|
||||
console.log("Finished downloading!");
|
||||
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const installerBin = await ensureBinary();
|
||||
|
||||
console.log("Now running Installer...");
|
||||
|
||||
execFileSync(installerBin, {
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
VENCORD_USER_DATA_DIR: BASE_DIR,
|
||||
VENCORD_DEV_INSTALL: "1"
|
||||
}
|
||||
});
|
|
@ -30,9 +30,9 @@ import "./webpack/patchWebpack";
|
|||
import { popNotice, showNotice } from "./api/Notices";
|
||||
import { PlainSettings, Settings } from "./api/settings";
|
||||
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
||||
import { checkForUpdates, UpdateLogger } from "./utils/updater";
|
||||
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
|
||||
import { onceReady } from "./webpack";
|
||||
import { Router } from "./webpack/common";
|
||||
import { SettingsRouter } from "./webpack/common";
|
||||
|
||||
export let Components: any;
|
||||
|
||||
|
@ -44,17 +44,37 @@ async function init() {
|
|||
if (!IS_WEB) {
|
||||
try {
|
||||
const isOutdated = await checkForUpdates();
|
||||
if (isOutdated && Settings.notifyAboutUpdates)
|
||||
if (!isOutdated) return;
|
||||
|
||||
if (Settings.autoUpdate) {
|
||||
await update();
|
||||
const needsFullRestart = await rebuild();
|
||||
setTimeout(() => {
|
||||
showNotice(
|
||||
"Vencord has been updated!",
|
||||
"Restart",
|
||||
() => {
|
||||
if (needsFullRestart)
|
||||
window.DiscordNative.app.relaunch();
|
||||
else
|
||||
location.reload();
|
||||
}
|
||||
);
|
||||
}, 10_000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings.notifyAboutUpdates)
|
||||
setTimeout(() => {
|
||||
showNotice(
|
||||
"A Vencord update is available!",
|
||||
"View Update",
|
||||
() => {
|
||||
popNotice();
|
||||
Router.open("VencordUpdater");
|
||||
SettingsRouter.open("VencordUpdater");
|
||||
}
|
||||
);
|
||||
}, 10000);
|
||||
}, 10_000);
|
||||
} catch (err) {
|
||||
UpdateLogger.error("Failed to check for updates", err);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { User } from "discord-types/general";
|
||||
import { ComponentType, HTMLProps } from "react";
|
||||
|
||||
|
@ -52,6 +53,7 @@ const Badges = new Set<ProfileBadge>();
|
|||
* @param badge The badge to register
|
||||
*/
|
||||
export function addBadge(badge: ProfileBadge) {
|
||||
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
|
||||
Badges.add(badge);
|
||||
}
|
||||
|
||||
|
|
92
src/api/Notifications/NotificationComponent.tsx
Normal file
92
src/api/Notifications/NotificationComponent.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { useSettings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Forms, React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
|
||||
|
||||
import { NotificationData } from "./Notifications";
|
||||
|
||||
export default ErrorBoundary.wrap(function NotificationComponent({
|
||||
title,
|
||||
body,
|
||||
richBody,
|
||||
color,
|
||||
icon,
|
||||
onClick,
|
||||
onClose,
|
||||
image
|
||||
}: NotificationData) {
|
||||
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
|
||||
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
|
||||
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
|
||||
const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isHover || !hasFocus || timeout === 0) return void setElapsed(0);
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
const elapsed = Date.now() - start;
|
||||
if (elapsed >= timeout)
|
||||
onClose!();
|
||||
else
|
||||
setElapsed(elapsed);
|
||||
}, 10);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [timeout, isHover, hasFocus]);
|
||||
|
||||
const timeoutProgress = elapsed / timeout;
|
||||
|
||||
return (
|
||||
<button
|
||||
className="vc-notification-root"
|
||||
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
|
||||
onClick={onClick}
|
||||
onContextMenu={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClose!();
|
||||
}}
|
||||
onMouseEnter={() => setIsHover(true)}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
>
|
||||
<div className="vc-notification">
|
||||
{icon && <img className="vc-notification-icon" src={icon} alt="" />}
|
||||
<div className="vc-notification-content">
|
||||
<Forms.FormTitle tag="h2">{title}</Forms.FormTitle>
|
||||
<div>
|
||||
{richBody ?? <p className="vc-notification-p">{body}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{image && <img className="vc-notification-img" src={image} alt="" />}
|
||||
{timeout !== 0 && (
|
||||
<div
|
||||
className="vc-notification-progressbar"
|
||||
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
});
|
99
src/api/Notifications/Notifications.tsx
Normal file
99
src/api/Notifications/Notifications.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Settings } from "@api/settings";
|
||||
import { Queue } from "@utils/Queue";
|
||||
import { ReactDOM } from "@webpack/common";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Root } from "react-dom/client";
|
||||
|
||||
import NotificationComponent from "./NotificationComponent";
|
||||
|
||||
const NotificationQueue = new Queue();
|
||||
|
||||
let reactRoot: Root;
|
||||
let id = 42;
|
||||
|
||||
function getRoot() {
|
||||
if (!reactRoot) {
|
||||
const container = document.createElement("div");
|
||||
container.id = "vc-notification-container";
|
||||
document.body.append(container);
|
||||
reactRoot = ReactDOM.createRoot(container);
|
||||
}
|
||||
return reactRoot;
|
||||
}
|
||||
|
||||
export interface NotificationData {
|
||||
title: string;
|
||||
body: string;
|
||||
/**
|
||||
* Same as body but can be a custom component.
|
||||
* Will be used over body if present.
|
||||
* Not supported on desktop notifications, those will fall back to body */
|
||||
richBody?: ReactNode;
|
||||
/** Small icon. This is for things like profile pictures and should be square */
|
||||
icon?: string;
|
||||
/** Large image. Optimally, this should be around 16x9 but it doesn't matter much. Desktop Notifications might not support this */
|
||||
image?: string;
|
||||
onClick?(): void;
|
||||
onClose?(): void;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
function _showNotification(notification: NotificationData, id: number) {
|
||||
const root = getRoot();
|
||||
return new Promise<void>(resolve => {
|
||||
root.render(
|
||||
<NotificationComponent key={id} {...notification} onClose={() => {
|
||||
notification.onClose?.();
|
||||
root.render(null);
|
||||
resolve();
|
||||
}} />,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBeNative() {
|
||||
const { useNative } = Settings.notifications;
|
||||
if (useNative === "always") return true;
|
||||
if (useNative === "not-focused") return !document.hasFocus();
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function requestPermission() {
|
||||
return (
|
||||
Notification.permission === "granted" ||
|
||||
(Notification.permission !== "denied" && (await Notification.requestPermission()) === "granted")
|
||||
);
|
||||
}
|
||||
|
||||
export async function showNotification(data: NotificationData) {
|
||||
if (shouldBeNative() && await requestPermission()) {
|
||||
const { title, body, icon, image, onClick = null, onClose = null } = data;
|
||||
const n = new Notification(title, {
|
||||
body,
|
||||
icon,
|
||||
image
|
||||
});
|
||||
n.onclick = onClick;
|
||||
n.onclose = onClose;
|
||||
} else {
|
||||
NotificationQueue.push(() => _showNotification(data, id++));
|
||||
}
|
||||
}
|
19
src/api/Notifications/index.ts
Normal file
19
src/api/Notifications/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "./Notifications";
|
49
src/api/Notifications/styles.css
Normal file
49
src/api/Notifications/styles.css
Normal file
|
@ -0,0 +1,49 @@
|
|||
.vc-notification-root {
|
||||
/* clear default button styles */
|
||||
all: unset;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 25vw;
|
||||
min-height: 10vh;
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-secondary-alt);
|
||||
position: absolute;
|
||||
z-index: 2147483647;
|
||||
right: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-notification {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 1.25rem;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.vc-notification-icon {
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Discord adding 3km margin to generic tags */
|
||||
.vc-notification h2 {
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
.vc-notification-progressbar {
|
||||
height: 0.25rem;
|
||||
border-radius: 5px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.vc-notification-p {
|
||||
margin: 0.5rem 0 0;
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
.vc-notification-img {
|
||||
width: 100%;
|
||||
}
|
|
@ -25,6 +25,7 @@ import * as $MessageDecorations from "./MessageDecorations";
|
|||
import * as $MessageEventsAPI from "./MessageEvents";
|
||||
import * as $MessagePopover from "./MessagePopover";
|
||||
import * as $Notices from "./Notices";
|
||||
import * as $Notifications from "./Notifications";
|
||||
import * as $ServerList from "./ServerList";
|
||||
import * as $Styles from "./Styles";
|
||||
|
||||
|
@ -88,3 +89,7 @@ export const MemberListDecorators = $MemberListDecorators;
|
|||
* a
|
||||
*/
|
||||
export const Styles = $Styles;
|
||||
/**
|
||||
* An API allowing you to display notifications
|
||||
*/
|
||||
export const Notifications = $Notifications;
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import IpcEvents from "@utils/IpcEvents";
|
||||
import Logger from "@utils/Logger";
|
||||
import { mergeDefaults } from "@utils/misc";
|
||||
import { OptionType } from "@utils/types";
|
||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
import plugins from "~plugins";
|
||||
|
@ -27,23 +27,43 @@ import plugins from "~plugins";
|
|||
const logger = new Logger("Settings");
|
||||
export interface Settings {
|
||||
notifyAboutUpdates: boolean;
|
||||
autoUpdate: boolean;
|
||||
useQuickCss: boolean;
|
||||
enableReactDevtools: boolean;
|
||||
themeLinks: string[];
|
||||
frameless: boolean;
|
||||
transparent: boolean;
|
||||
winCtrlQ: boolean;
|
||||
plugins: {
|
||||
[plugin: string]: {
|
||||
enabled: boolean;
|
||||
[setting: string]: any;
|
||||
};
|
||||
};
|
||||
|
||||
notifications: {
|
||||
timeout: number;
|
||||
position: "top-right" | "bottom-right";
|
||||
useNative: "always" | "never" | "not-focused";
|
||||
};
|
||||
}
|
||||
|
||||
const DefaultSettings: Settings = {
|
||||
notifyAboutUpdates: true,
|
||||
autoUpdate: false,
|
||||
useQuickCss: true,
|
||||
themeLinks: [],
|
||||
enableReactDevtools: false,
|
||||
plugins: {}
|
||||
frameless: false,
|
||||
transparent: false,
|
||||
winCtrlQ: false,
|
||||
plugins: {},
|
||||
|
||||
notifications: {
|
||||
timeout: 5000,
|
||||
position: "bottom-right",
|
||||
useNative: "not-focused"
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
|
@ -144,6 +164,7 @@ export const Settings = makeProxy(settings);
|
|||
* @param paths An optional list of paths to whitelist for rerenders
|
||||
* @returns Settings
|
||||
*/
|
||||
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
|
||||
export function useSettings(paths?: string[]) {
|
||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||
|
||||
|
@ -198,3 +219,19 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function definePluginSettings<D extends SettingsDefinition, C extends SettingsChecks<D>>(def: D, checks?: C) {
|
||||
const definedSettings: DefinedSettings<D> = {
|
||||
get store() {
|
||||
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
|
||||
return Settings.plugins[definedSettings.pluginName] as any;
|
||||
},
|
||||
use: settings => useSettings(
|
||||
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`)
|
||||
).plugins[definedSettings.pluginName] as any,
|
||||
def,
|
||||
checks: checks ?? {},
|
||||
pluginName: "",
|
||||
};
|
||||
return definedSettings;
|
||||
}
|
||||
|
|
|
@ -16,15 +16,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { BadgeStyle } from "@components/PluginSettings/styles";
|
||||
|
||||
export function Badge({ text, color }): JSX.Element {
|
||||
return (
|
||||
<div style={{
|
||||
<div className="vc-plugins-badge" style={{
|
||||
backgroundColor: color,
|
||||
justifySelf: "flex-end",
|
||||
marginLeft: "auto",
|
||||
...BadgeStyle
|
||||
}}>{text}</div>
|
||||
marginLeft: "auto"
|
||||
}}>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -103,7 +103,7 @@ const ErrorBoundary = LazyComponent(() => {
|
|||
};
|
||||
}) as
|
||||
React.ComponentType<React.PropsWithChildren<Props>> & {
|
||||
wrap<T extends JSX.IntrinsicAttributes = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
|
||||
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
|
||||
};
|
||||
|
||||
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
||||
|
|
|
@ -144,6 +144,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
onChange={onChange}
|
||||
onError={onError}
|
||||
pluginSettings={pluginSettings}
|
||||
definedSettings={plugin.settings}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ import { Forms, React, Select } from "@webpack/common";
|
|||
|
||||
import { ISettingElementProps } from ".";
|
||||
|
||||
export function SettingBooleanComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
|
||||
export function SettingBooleanComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
|
||||
const def = pluginSettings[id] ?? option.default;
|
||||
|
||||
const [state, setState] = React.useState(def ?? false);
|
||||
|
@ -37,7 +37,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange,
|
|||
];
|
||||
|
||||
function handleChange(newValue: boolean): void {
|
||||
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
if (typeof isValid === "string") setError(isValid);
|
||||
else if (!isValid) setError("Invalid input provided.");
|
||||
else {
|
||||
|
@ -51,7 +51,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange,
|
|||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<Select
|
||||
isDisabled={option.disabled?.() ?? false}
|
||||
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
||||
options={options}
|
||||
placeholder={option.placeholder ?? "Select an option"}
|
||||
maxVisibleItems={5}
|
||||
|
|
|
@ -23,7 +23,7 @@ import { ISettingElementProps } from ".";
|
|||
|
||||
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||
|
||||
export function SettingNumericComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
|
||||
export function SettingNumericComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
|
||||
function serialize(value: any) {
|
||||
if (option.type === OptionType.BIGINT) return BigInt(value);
|
||||
return Number(value);
|
||||
|
@ -37,7 +37,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
|
|||
}, [error]);
|
||||
|
||||
function handleChange(newValue) {
|
||||
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
if (typeof isValid === "string") setError(isValid);
|
||||
else if (!isValid) setError("Invalid input provided.");
|
||||
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
||||
|
@ -58,7 +58,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
|
|||
value={state}
|
||||
onChange={handleChange}
|
||||
placeholder={option.placeholder ?? "Enter a number"}
|
||||
disabled={option.disabled?.() ?? false}
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
{...option.componentProps}
|
||||
/>
|
||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { Forms, React, Select } from "@webpack/common";
|
|||
|
||||
import { ISettingElementProps } from ".";
|
||||
|
||||
export function SettingSelectComponent({ option, pluginSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
|
||||
export function SettingSelectComponent({ option, pluginSettings, definedSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
|
||||
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
|
||||
|
||||
const [state, setState] = React.useState<any>(def ?? null);
|
||||
|
@ -32,7 +32,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
|
|||
}, [error]);
|
||||
|
||||
function handleChange(newValue) {
|
||||
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
if (typeof isValid === "string") setError(isValid);
|
||||
else if (!isValid) setError("Invalid input provided.");
|
||||
else {
|
||||
|
@ -45,7 +45,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
|
|||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<Select
|
||||
isDisabled={option.disabled?.() ?? false}
|
||||
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
||||
options={option.options}
|
||||
placeholder={option.placeholder ?? "Select an option"}
|
||||
maxVisibleItems={5}
|
||||
|
|
|
@ -29,7 +29,7 @@ export function makeRange(start: number, end: number, step = 1) {
|
|||
return ranges;
|
||||
}
|
||||
|
||||
export function SettingSliderComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) {
|
||||
export function SettingSliderComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) {
|
||||
const def = pluginSettings[id] ?? option.default;
|
||||
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
@ -39,7 +39,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o
|
|||
}, [error]);
|
||||
|
||||
function handleChange(newValue: number): void {
|
||||
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
if (typeof isValid === "string") setError(isValid);
|
||||
else if (!isValid) setError("Invalid input provided.");
|
||||
else {
|
||||
|
@ -52,7 +52,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o
|
|||
<Forms.FormSection>
|
||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
||||
<Slider
|
||||
disabled={option.disabled?.() ?? false}
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
markers={option.markers}
|
||||
minValue={option.markers[0]}
|
||||
maxValue={option.markers[option.markers.length - 1]}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { Forms, React, TextInput } from "@webpack/common";
|
|||
|
||||
import { ISettingElementProps } from ".";
|
||||
|
||||
export function SettingTextComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
|
||||
export function SettingTextComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
|
||||
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
|
@ -30,7 +30,7 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE
|
|||
}, [error]);
|
||||
|
||||
function handleChange(newValue) {
|
||||
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
if (typeof isValid === "string") setError(isValid);
|
||||
else if (!isValid) setError("Invalid input provided.");
|
||||
else {
|
||||
|
@ -47,7 +47,7 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE
|
|||
value={state}
|
||||
onChange={handleChange}
|
||||
placeholder={option.placeholder ?? "Enter a value"}
|
||||
disabled={option.disabled?.() ?? false}
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
{...option.componentProps}
|
||||
/>
|
||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { PluginOptionBase } from "@utils/types";
|
||||
import { DefinedSettings, PluginOptionBase } from "@utils/types";
|
||||
|
||||
export interface ISettingElementProps<T extends PluginOptionBase> {
|
||||
option: T;
|
||||
|
@ -27,9 +27,10 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
|
|||
};
|
||||
id: string;
|
||||
onError(hasError: boolean): void;
|
||||
definedSettings?: DefinedSettings;
|
||||
}
|
||||
|
||||
export * from "./BadgeComponent";
|
||||
export * from "../../Badge";
|
||||
export * from "./SettingBooleanComponent";
|
||||
export * from "./SettingCustomComponent";
|
||||
export * from "./SettingNumericComponent";
|
||||
|
|
|
@ -16,28 +16,32 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import * as DataStore from "@api/DataStore";
|
||||
import { showNotice } from "@api/Notices";
|
||||
import { Settings, useSettings } from "@api/settings";
|
||||
import { useSettings } from "@api/settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { Badge } from "@components/PluginSettings/components";
|
||||
import PluginModal from "@components/PluginSettings/PluginModal";
|
||||
import * as styles from "@components/PluginSettings/styles";
|
||||
import { Switch } from "@components/Switch";
|
||||
import { ChangeList } from "@utils/ChangeList";
|
||||
import Logger from "@utils/Logger";
|
||||
import { classes, LazyComponent, useAwaiter } from "@utils/misc";
|
||||
import { openModalLazy } from "@utils/modal";
|
||||
import { Plugin } from "@utils/types";
|
||||
import { findByCode, findByPropsLazy } from "@webpack";
|
||||
import { Alerts, Button, Forms, Margins, Parser, React, Select, Switch, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
||||
import { Alerts, Button, Card, Forms, Margins, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
|
||||
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
||||
|
||||
|
||||
const cl = classNameFactory("vc-plugins-");
|
||||
const logger = new Logger("PluginSettings", "#a6d189");
|
||||
|
||||
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
|
||||
|
@ -56,23 +60,27 @@ function showErrorToast(message: string) {
|
|||
});
|
||||
}
|
||||
|
||||
interface ReloadRequiredCardProps extends React.HTMLProps<HTMLDivElement> {
|
||||
plugins: string[];
|
||||
}
|
||||
|
||||
function ReloadRequiredCard({ plugins, ...props }: ReloadRequiredCardProps) {
|
||||
if (plugins.length === 0) return null;
|
||||
|
||||
const pluginPrefix = plugins.length === 1 ? "The plugin" : "The following plugins require a reload to apply changes:";
|
||||
const pluginSuffix = plugins.length === 1 ? " requires a reload to apply changes." : ".";
|
||||
|
||||
function ReloadRequiredCard({ required }: { required: boolean; }) {
|
||||
return (
|
||||
<ErrorCard {...props} style={{ padding: "1em", display: "grid", gridTemplateColumns: "1fr auto", gap: 8, ...props.style }}>
|
||||
<span style={{ margin: "auto 0" }}>
|
||||
{pluginPrefix} <code>{plugins.join(", ")}</code>{pluginSuffix}
|
||||
</span>
|
||||
<Button look={Button.Looks.INVERTED} onClick={() => location.reload()}>Reload</Button>
|
||||
</ErrorCard>
|
||||
<Card className={cl("info-card", { "restart-card": required })}>
|
||||
{required ? (
|
||||
<>
|
||||
<Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle>
|
||||
<Forms.FormText className={cl("dep-text")}>
|
||||
Restart now to apply new plugins and their settings
|
||||
</Forms.FormText>
|
||||
<Button color={Button.Colors.YELLOW} onClick={() => location.reload()}>
|
||||
Restart
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Forms.FormTitle tag="h5">Plugin Management</Forms.FormTitle>
|
||||
<Forms.FormText>Press the cog wheel or info icon to get more info on a plugin</Forms.FormText>
|
||||
<Forms.FormText>Plugins with a cog wheel have settings you can modify!</Forms.FormText>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -84,14 +92,9 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
|
|||
}
|
||||
|
||||
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
|
||||
const settings = useSettings();
|
||||
const pluginSettings = settings.plugins[plugin.name];
|
||||
const settings = useSettings([`plugins.${plugin.name}`]).plugins[plugin.name];
|
||||
|
||||
const [iconHover, setIconHover] = React.useState(false);
|
||||
|
||||
function isEnabled() {
|
||||
return pluginSettings?.enabled || plugin.started;
|
||||
}
|
||||
const isEnabled = () => settings.enabled ?? false;
|
||||
|
||||
function openModal() {
|
||||
openModalLazy(async () => {
|
||||
|
@ -113,7 +116,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||
return;
|
||||
} else if (restartNeeded) {
|
||||
// If any dependencies have patches, don't start the plugin yet.
|
||||
pluginSettings.enabled = true;
|
||||
settings.enabled = true;
|
||||
onRestartNeeded(plugin.name);
|
||||
return;
|
||||
}
|
||||
|
@ -121,14 +124,14 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||
|
||||
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
|
||||
if (plugin.patches) {
|
||||
pluginSettings.enabled = !wasEnabled;
|
||||
settings.enabled = !wasEnabled;
|
||||
onRestartNeeded(plugin.name);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the plugin is enabled, but hasn't been started, then we can just toggle it off.
|
||||
if (wasEnabled && !plugin.started) {
|
||||
pluginSettings.enabled = !wasEnabled;
|
||||
settings.enabled = !wasEnabled;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -141,60 +144,38 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||
return;
|
||||
}
|
||||
|
||||
pluginSettings.enabled = !wasEnabled;
|
||||
settings.enabled = !wasEnabled;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex style={styles.PluginsGridItem} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<Flex className={cl("card", { "card-disabled": disabled })} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<div className={cl("card-header")}>
|
||||
<Text variant="text-md/bold" className={cl("name")}>
|
||||
{plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||
</Text>
|
||||
<button role="switch" onClick={() => openModal()} className={classes("button-12Fmur", cl("info-button"))}>
|
||||
{plugin.options
|
||||
? <CogWheel />
|
||||
: <InfoIcon width="24" height="24" />}
|
||||
</button>
|
||||
<Switch
|
||||
checked={isEnabled()}
|
||||
onChange={toggleEnabled}
|
||||
disabled={disabled}
|
||||
value={isEnabled()}
|
||||
note={<Text variant="text-md/normal" style={{
|
||||
height: 40,
|
||||
overflow: "hidden",
|
||||
// mfw css is so bad you need whatever this is to get multi line overflow ellipsis to work
|
||||
textOverflow: "ellipsis",
|
||||
display: "-webkit-box", // firefox users will cope (it doesn't support it)
|
||||
WebkitLineClamp: 2,
|
||||
lineClamp: 2,
|
||||
WebkitBoxOrient: "vertical",
|
||||
boxOrient: "vertical"
|
||||
}}>
|
||||
{plugin.description}
|
||||
</Text>}
|
||||
hideBorder={true}
|
||||
>
|
||||
<Flex style={{ marginTop: "auto", width: "100%", height: "100%", alignItems: "center", gap: "8px" }}>
|
||||
<Text
|
||||
variant="text-md/bold"
|
||||
style={{
|
||||
display: "flex", width: "100%", alignItems: "center", flexGrow: "1", gap: "8px"
|
||||
}}
|
||||
>
|
||||
{plugin.name}{(isNew) && <Badge text="NEW" color="#ED4245" />}
|
||||
</Text>
|
||||
<button role="switch" onClick={() => openModal()} style={styles.SettingsIcon} className="button-12Fmur">
|
||||
{plugin.options
|
||||
? <CogWheel
|
||||
style={{ color: iconHover ? "" : "var(--text-muted)" }}
|
||||
onMouseEnter={() => setIconHover(true)}
|
||||
onMouseLeave={() => setIconHover(false)}
|
||||
/>
|
||||
: <InfoIcon
|
||||
width="24" height="24"
|
||||
style={{ color: iconHover ? "" : "var(--text-muted)" }}
|
||||
onMouseEnter={() => setIconHover(true)}
|
||||
onMouseLeave={() => setIconHover(false)}
|
||||
/>}
|
||||
</button>
|
||||
</Flex>
|
||||
</Switch>
|
||||
</div>
|
||||
<Text className={cl("note")} variant="text-sm/normal">{plugin.description}</Text>
|
||||
</Flex >
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorBoundary.wrap(function Settings() {
|
||||
enum SearchStatus {
|
||||
ALL,
|
||||
ENABLED,
|
||||
DISABLED
|
||||
}
|
||||
|
||||
export default ErrorBoundary.wrap(function PluginSettings() {
|
||||
const settings = useSettings();
|
||||
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
||||
|
||||
|
@ -235,21 +216,19 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
|
||||
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
||||
|
||||
const [searchValue, setSearchValue] = React.useState({ value: "", status: "all" });
|
||||
const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL });
|
||||
|
||||
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
||||
const onStatusChange = (status: string) => setSearchValue(prev => ({ ...prev, status }));
|
||||
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
|
||||
|
||||
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
|
||||
const showEnabled = searchValue.status === "enabled" || searchValue.status === "all";
|
||||
const showDisabled = searchValue.status === "disabled" || searchValue.status === "all";
|
||||
const enabled = settings.plugins[plugin.name]?.enabled || plugin.started;
|
||||
const enabled = settings.plugins[plugin.name]?.enabled;
|
||||
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
|
||||
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
|
||||
if (!searchValue.value.length) return true;
|
||||
return (
|
||||
((showEnabled && enabled) || (showDisabled && !enabled)) &&
|
||||
(
|
||||
plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) ||
|
||||
plugin.description.toLowerCase().includes(searchValue.value.toLowerCase())
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -270,23 +249,69 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||
return window._.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
|
||||
}));
|
||||
|
||||
type P = JSX.Element | JSX.Element[];
|
||||
let plugins: P, requiredPlugins: P;
|
||||
if (sortedPlugins?.length) {
|
||||
plugins = [];
|
||||
requiredPlugins = [];
|
||||
|
||||
for (const p of sortedPlugins) {
|
||||
if (!pluginFilter(p)) continue;
|
||||
|
||||
const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled);
|
||||
|
||||
if (isRequired) {
|
||||
const tooltipText = p.required
|
||||
? "This plugin is required for Vencord to function."
|
||||
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
|
||||
|
||||
requiredPlugins.push(
|
||||
<Tooltip text={tooltipText} key={p.name}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<PluginCard
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onRestartNeeded={name => changes.handleChange(name)}
|
||||
disabled={true}
|
||||
plugin={p}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
plugins.push(
|
||||
<PluginCard
|
||||
onRestartNeeded={name => changes.handleChange(name)}
|
||||
disabled={false}
|
||||
plugin={p}
|
||||
isNew={newPlugins?.includes(p.name)}
|
||||
key={p.name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
plugins = requiredPlugins = <Text variant="text-md/normal">No plugins meet search criteria.</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormSection className={Margins.marginTop16}>
|
||||
<ReloadRequiredCard required={changes.hasChanges} />
|
||||
|
||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||
Filters
|
||||
</Forms.FormTitle>
|
||||
|
||||
<ReloadRequiredCard plugins={[...changes.getChanges()]} style={{ marginBottom: 16 }} />
|
||||
|
||||
<div style={styles.FiltersBar}>
|
||||
<TextInput value={searchValue.value} placeholder={"Search for a plugin..."} onChange={onSearch} style={{ marginBottom: 24 }} />
|
||||
<div className={cl("filter-controls")}>
|
||||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.marginBottom20} />
|
||||
<div className={InputStyles.inputWrapper}>
|
||||
<Select
|
||||
className={InputStyles.inputDefault}
|
||||
options={[
|
||||
{ label: "Show All", value: "all", default: true },
|
||||
{ label: "Show Enabled", value: "enabled" },
|
||||
{ label: "Show Disabled", value: "disabled" }
|
||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||
{ label: "Show Disabled", value: SearchStatus.DISABLED }
|
||||
]}
|
||||
serialize={String}
|
||||
select={onStatusChange}
|
||||
|
@ -298,50 +323,17 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||
|
||||
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
|
||||
|
||||
<div style={styles.PluginsGrid}>
|
||||
{sortedPlugins?.length ? sortedPlugins
|
||||
.filter(a => !a.required && !dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
||||
.map(plugin => {
|
||||
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
||||
const dependency = enabledDependants?.length;
|
||||
return <PluginCard
|
||||
onRestartNeeded={name => changes.add(name)}
|
||||
disabled={plugin.required || !!dependency}
|
||||
plugin={plugin}
|
||||
isNew={newPlugins?.includes(plugin.name)}
|
||||
key={plugin.name}
|
||||
/>;
|
||||
})
|
||||
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
||||
}
|
||||
<div className={cl("grid")}>
|
||||
{plugins}
|
||||
</div>
|
||||
<Forms.FormDivider />
|
||||
|
||||
<Forms.FormDivider className={Margins.marginTop20} />
|
||||
|
||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||
Required Plugins
|
||||
</Forms.FormTitle>
|
||||
<div style={styles.PluginsGrid}>
|
||||
{sortedPlugins?.length ? sortedPlugins
|
||||
.filter(a => a.required || dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
||||
.map(plugin => {
|
||||
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
||||
const dependency = enabledDependants?.length;
|
||||
const tooltipText = plugin.required
|
||||
? "This plugin is required for Vencord to function."
|
||||
: makeDependencyList(dependencyCheck(plugin.name, depMap));
|
||||
return <Tooltip text={tooltipText} key={plugin.name}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<PluginCard
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onRestartNeeded={name => changes.handleChange(name)}
|
||||
disabled={plugin.required || !!dependency}
|
||||
plugin={plugin}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>;
|
||||
})
|
||||
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
||||
}
|
||||
<div className={cl("grid")}>
|
||||
{requiredPlugins}
|
||||
</div>
|
||||
</Forms.FormSection >
|
||||
);
|
||||
|
@ -354,11 +346,7 @@ function makeDependencyList(deps: string[]) {
|
|||
return (
|
||||
<React.Fragment>
|
||||
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
||||
{deps.map((dep: string) => <Forms.FormText style={{ margin: "0 auto" }}>{dep}</Forms.FormText>)}
|
||||
{deps.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function dependencyCheck(pluginName: string, depMap: Record<string, string[]>): string[] {
|
||||
return depMap[pluginName]?.filter(d => Settings.plugins[d].enabled) || [];
|
||||
}
|
||||
|
|
138
src/components/PluginSettings/styles.css
Normal file
138
src/components/PluginSettings/styles.css
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.vc-plugins-grid {
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.vc-plugins-card {
|
||||
background-color: var(--background-secondary-alt);
|
||||
color: var(--interactive-active);
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
width: 100%;
|
||||
transition: 0.1s ease-out;
|
||||
transition-property: box-shadow, transform, background, opacity;
|
||||
}
|
||||
|
||||
.vc-plugins-card-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.vc-plugins-card:hover {
|
||||
background-color: var(--background-tertiary);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--elevation-high);
|
||||
}
|
||||
|
||||
.vc-plugins-card-header {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
height: 1.5rem;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vc-plugins-info-button {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.vc-plugins-settings-button:hover {
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.vc-plugins-filter-controls {
|
||||
display: grid;
|
||||
height: 40px;
|
||||
gap: 10px;
|
||||
grid-template-columns: 1fr 150px;
|
||||
}
|
||||
|
||||
.vc-plugins-badge {
|
||||
padding: 0 6px;
|
||||
font-family: var(--font-display);
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
height: 16px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: var(--white-500);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vc-plugins-note {
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
/* stylelint-disable-next-line property-no-unknown */
|
||||
box-orient: vertical;
|
||||
}
|
||||
|
||||
.vc-plugins-name {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
gap: 8px;
|
||||
cursor: "default";
|
||||
}
|
||||
|
||||
.vc-plugins-dep-name {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.vc-plugins-info-card {
|
||||
padding: 1em;
|
||||
height: 8em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vc-plugins-info-card div {
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.vc-plugins-restart-card {
|
||||
padding: 1em;
|
||||
background: var(--info-warning-background);
|
||||
border: 1px solid var(--info-warning-foreground);
|
||||
color: var(--info-warning-text);
|
||||
}
|
||||
|
||||
.vc-plugins-restart-card button {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.vc-plugins-info-button svg:not(:hover, :focus) {
|
||||
color: var(--text-muted);
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const PluginsGrid: React.CSSProperties = {
|
||||
marginTop: 16,
|
||||
display: "grid",
|
||||
gridGap: 16,
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
|
||||
};
|
||||
|
||||
export const PluginsGridItem: React.CSSProperties = {
|
||||
backgroundColor: "var(--background-modifier-selected)",
|
||||
color: "var(--interactive-active)",
|
||||
borderRadius: 3,
|
||||
cursor: "pointer",
|
||||
display: "block",
|
||||
height: "100%",
|
||||
padding: 10,
|
||||
width: "100%",
|
||||
};
|
||||
|
||||
export const FiltersBar: React.CSSProperties = {
|
||||
gap: 10,
|
||||
height: 40,
|
||||
gridTemplateColumns: "1fr 150px",
|
||||
display: "grid"
|
||||
};
|
||||
|
||||
export const SettingsIcon: React.CSSProperties = {
|
||||
height: "24px",
|
||||
width: "24px",
|
||||
padding: "0",
|
||||
background: "transparent",
|
||||
marginRight: 8
|
||||
};
|
||||
|
||||
export const BadgeStyle: React.CSSProperties = {
|
||||
padding: "0 6px",
|
||||
fontFamily: "var(--font-display)",
|
||||
fontWeight: "500",
|
||||
borderRadius: "8px",
|
||||
height: "16px",
|
||||
fontSize: "12px",
|
||||
lineHeight: "16px",
|
||||
color: "var(--white-500)",
|
||||
};
|
3
src/components/Switch.css
Normal file
3
src/components/Switch.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.vc-switch-slider {
|
||||
transition: 100ms transform ease-in-out;
|
||||
}
|
76
src/components/Switch.tsx
Normal file
76
src/components/Switch.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 "./Switch.css";
|
||||
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
|
||||
interface SwitchProps {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SWITCH_ON = "var(--green-360)";
|
||||
const SWITCH_OFF = "var(--primary-400)";
|
||||
const SwitchClasses = findByPropsLazy("slider", "input", "container");
|
||||
|
||||
export function Switch({ checked, onChange, disabled }: SwitchProps) {
|
||||
return (
|
||||
<div>
|
||||
<div className={`${SwitchClasses.container} default-colors`} style={{
|
||||
backgroundColor: checked ? SWITCH_ON : SWITCH_OFF,
|
||||
opacity: disabled ? 0.3 : 1
|
||||
}}>
|
||||
<svg
|
||||
className={SwitchClasses.slider + " vc-switch-slider"}
|
||||
viewBox="0 0 28 20"
|
||||
preserveAspectRatio="xMinYMid meet"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
transform: checked ? "translateX(12px)" : "translateX(-3px)",
|
||||
}}
|
||||
>
|
||||
<rect fill="white" x="4" y="0" height="20" width="20" rx="10" />
|
||||
<svg viewBox="0 0 20 20" fill="none">
|
||||
{checked ? (
|
||||
<>
|
||||
<path fill={SWITCH_ON} d="M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z" />
|
||||
<path fill={SWITCH_ON} d="M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<path fill={SWITCH_OFF} d="M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z" />
|
||||
<path fill={SWITCH_OFF} d="M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z" />
|
||||
</>
|
||||
)}
|
||||
|
||||
</svg>
|
||||
</svg>
|
||||
<input
|
||||
disabled={disabled}
|
||||
type="checkbox"
|
||||
className={SwitchClasses.input}
|
||||
tabIndex={0}
|
||||
checked={checked}
|
||||
onChange={e => onChange(e.currentTarget.checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -18,19 +18,14 @@
|
|||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { classes } from "@utils/misc";
|
||||
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
||||
import { Button, Card, Forms, Margins, Text } from "@webpack/common";
|
||||
|
||||
function BackupRestoreTab() {
|
||||
return (
|
||||
<Forms.FormSection title="Settings Sync">
|
||||
<Card style={{
|
||||
backgroundColor: "var(--info-warning-background)",
|
||||
borderColor: "var(--info-warning-foreground)",
|
||||
color: "var(--info-warning-text)",
|
||||
padding: "1em",
|
||||
marginBottom: "0.5em",
|
||||
}}>
|
||||
<Forms.FormSection title="Settings Sync" className={Margins.marginTop16}>
|
||||
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
||||
<Flex flexDirection="column">
|
||||
<strong>Warning</strong>
|
||||
<span>Importing a settings file will overwrite your current settings.</span>
|
||||
|
@ -50,7 +45,7 @@ function BackupRestoreTab() {
|
|||
</Text>
|
||||
<Flex>
|
||||
<Button
|
||||
onClick={uploadSettingsBackup}
|
||||
onClick={() => uploadSettingsBackup()}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Import Settings
|
||||
|
|
|
@ -75,11 +75,11 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
|||
|
||||
export default ErrorBoundary.wrap(function () {
|
||||
const settings = useSettings();
|
||||
const ref = React.useRef<HTMLTextAreaElement>();
|
||||
const [themeText, setThemeText] = React.useState(settings.themeLinks.join("\n"));
|
||||
|
||||
function onBlur() {
|
||||
settings.themeLinks = [...new Set(
|
||||
ref.current!.value
|
||||
themeText
|
||||
.trim()
|
||||
.split(/\n+/)
|
||||
.map(s => s.trim())
|
||||
|
@ -89,15 +89,11 @@ export default ErrorBoundary.wrap(function () {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Card style={{
|
||||
padding: "1em",
|
||||
marginBottom: "1em",
|
||||
marginTop: "1em"
|
||||
}}>
|
||||
<Card className="vc-settings-card">
|
||||
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
|
||||
<Forms.FormText>One link per line</Forms.FormText>
|
||||
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
|
||||
<Forms.FormDivider />
|
||||
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
||||
<div style={{ marginBottom: ".5em" }}>
|
||||
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
||||
|
@ -123,8 +119,8 @@ export default ErrorBoundary.wrap(function () {
|
|||
padding: ".5em",
|
||||
border: "1px solid var(--background-modifier-accent)"
|
||||
}}
|
||||
ref={ref}
|
||||
defaultValue={settings.themeLinks.join("\n")}
|
||||
value={themeText}
|
||||
onChange={e => setThemeText(e.currentTarget.value)}
|
||||
className={TextAreaProps.textarea}
|
||||
placeholder="Theme Links"
|
||||
spellCheck={false}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useSettings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Flex } from "@components/Flex";
|
||||
|
@ -23,7 +24,7 @@ import { handleComponentFailed } from "@components/handleComponentFailed";
|
|||
import { Link } from "@components/Link";
|
||||
import { classes, useAwaiter } from "@utils/misc";
|
||||
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
||||
import { Alerts, Button, Card, Forms, Margins, Parser, React, Toasts } from "@webpack/common";
|
||||
import { Alerts, Button, Card, Forms, Margins, Parser, React, Switch, Toasts } from "@webpack/common";
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
|
||||
|
@ -69,14 +70,18 @@ interface CommonProps {
|
|||
repoPending: boolean;
|
||||
}
|
||||
|
||||
function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string, disabled?: boolean; }) {
|
||||
return <Link href={`${repo}/commit/${hash}`} disabled={disabled}>
|
||||
{hash}
|
||||
</Link>;
|
||||
}
|
||||
|
||||
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
|
||||
return (
|
||||
<Card style={{ padding: ".5em" }}>
|
||||
{updates.map(({ hash, author, message }) => (
|
||||
<div>
|
||||
<Link href={`${repo}/commit/${hash}`} disabled={repoPending}>
|
||||
<code>{hash}</code>
|
||||
</Link>
|
||||
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
|
||||
<span style={{
|
||||
marginLeft: "0.5em",
|
||||
color: "var(--text-normal)"
|
||||
|
@ -179,6 +184,8 @@ function Newer(props: CommonProps) {
|
|||
}
|
||||
|
||||
function Updater() {
|
||||
const settings = useSettings(["notifyAboutUpdates", "autoUpdate"]);
|
||||
|
||||
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -192,16 +199,33 @@ function Updater() {
|
|||
};
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormSection className={Margins.marginTop16}>
|
||||
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||
<Switch
|
||||
value={settings.notifyAboutUpdates}
|
||||
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||
note="Shows a toast on startup"
|
||||
disabled={settings.autoUpdate}
|
||||
>
|
||||
Get notified about new updates
|
||||
</Switch>
|
||||
<Switch
|
||||
value={settings.autoUpdate}
|
||||
onChange={(v: boolean) => settings.autoUpdate = v}
|
||||
note="Automatically update Vencord without confirmation prompt"
|
||||
>
|
||||
Automatically update
|
||||
</Switch>
|
||||
|
||||
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
|
||||
|
||||
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
|
||||
<Link href={repo}>
|
||||
{repo.split("/").slice(-2).join("/")}
|
||||
</Link>
|
||||
)} ({gitHash})</Forms.FormText>
|
||||
)} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText>
|
||||
|
||||
<Forms.FormDivider />
|
||||
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
||||
|
||||
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
||||
|
||||
|
|
|
@ -18,31 +18,73 @@
|
|||
|
||||
|
||||
import { useSettings } from "@api/settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import DonateButton from "@components/DonateButton";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { useAwaiter } from "@utils/misc";
|
||||
import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity, useAwaiter } from "@utils/misc";
|
||||
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
|
||||
|
||||
const st = (style: string) => `vcSettings${style}`;
|
||||
const cl = classNameFactory("vc-settings-");
|
||||
|
||||
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
|
||||
const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png";
|
||||
|
||||
type KeysOfType<Object, Type> = {
|
||||
[K in keyof Object]: Object[K] extends Type ? K : never;
|
||||
}[keyof Object];
|
||||
|
||||
function VencordSettings() {
|
||||
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
|
||||
fallbackValue: "Loading..."
|
||||
});
|
||||
const settings = useSettings();
|
||||
const notifSettings = settings.notifications;
|
||||
|
||||
const [donateImage] = React.useState(
|
||||
Math.random() > 0.5
|
||||
? "https://cdn.discordapp.com/emojis/1026533090627174460.png"
|
||||
: "https://media.discordapp.net/stickers/1039992459209490513.png"
|
||||
);
|
||||
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
|
||||
|
||||
const isWindows = navigator.platform.toLowerCase().startsWith("win");
|
||||
|
||||
const Switches: Array<false | {
|
||||
key: KeysOfType<typeof settings, boolean>;
|
||||
title: string;
|
||||
note: string;
|
||||
}> =
|
||||
[
|
||||
{
|
||||
key: "useQuickCss",
|
||||
title: "Enable Custom CSS",
|
||||
note: "Loads your Custom CSS"
|
||||
},
|
||||
!IS_WEB && {
|
||||
key: "enableReactDevtools",
|
||||
title: "Enable React Developer Tools",
|
||||
note: "Requires a full restart"
|
||||
},
|
||||
!IS_WEB && !isWindows && {
|
||||
key: "frameless",
|
||||
title: "Disable the window frame",
|
||||
note: "Requires a full restart"
|
||||
},
|
||||
!IS_WEB && {
|
||||
key: "transparent",
|
||||
title: "Enable window transparency",
|
||||
note: "Requires a full restart"
|
||||
},
|
||||
!IS_WEB && isWindows && {
|
||||
key: "winCtrlQ",
|
||||
title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
|
||||
note: "Requires a full restart"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DonateCard image={donateImage} />
|
||||
<Forms.FormSection title="Quick Actions">
|
||||
<Card className={st("QuickActionCard")}>
|
||||
<Card className={cl("quick-actions-card")}>
|
||||
{IS_WEB ? (
|
||||
<Button
|
||||
onClick={() => require("../Monaco").launchMonacoEditor()}
|
||||
|
@ -82,34 +124,76 @@ function VencordSettings() {
|
|||
|
||||
<Forms.FormDivider />
|
||||
|
||||
<Forms.FormSection title="Settings">
|
||||
<Forms.FormText className={Margins.marginBottom20}>
|
||||
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
|
||||
<Forms.FormText className={Margins.bottom20}>
|
||||
Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
|
||||
</Forms.FormText>
|
||||
{Switches.map(s => s && (
|
||||
<Switch
|
||||
value={settings.useQuickCss}
|
||||
onChange={(v: boolean) => settings.useQuickCss = v}
|
||||
note="Loads styles from your QuickCSS file">
|
||||
Use QuickCSS
|
||||
key={s.key}
|
||||
value={settings[s.key]}
|
||||
onChange={v => settings[s.key] = v}
|
||||
note={s.note}
|
||||
>
|
||||
{s.title}
|
||||
</Switch>
|
||||
{!IS_WEB && (
|
||||
<React.Fragment>
|
||||
<Switch
|
||||
value={settings.enableReactDevtools}
|
||||
onChange={(v: boolean) => settings.enableReactDevtools = v}
|
||||
note="Requires a full restart">
|
||||
Enable React Developer Tools
|
||||
</Switch>
|
||||
<Switch
|
||||
value={settings.notifyAboutUpdates}
|
||||
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||
note="Shows a toast on startup">
|
||||
Get notified about new updates
|
||||
</Switch>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
))}
|
||||
</Forms.FormSection>
|
||||
|
||||
|
||||
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
|
||||
{notifSettings.useNative !== "never" && Notification.permission === "denied" && (
|
||||
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
|
||||
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
|
||||
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
|
||||
</ErrorCard>
|
||||
)}
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
Some plugins may show you notifications. These come in two styles:
|
||||
<ul>
|
||||
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
|
||||
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
|
||||
</ul>
|
||||
</Forms.FormText>
|
||||
<Select
|
||||
placeholder="Notification Style"
|
||||
options={[
|
||||
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
|
||||
{ label: "Always use Desktop notifications", value: "always" },
|
||||
{ label: "Always use Vencord notifications", value: "never" },
|
||||
]satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
|
||||
closeOnSelect={true}
|
||||
select={v => notifSettings.useNative = v}
|
||||
isSelected={v => v === notifSettings.useNative}
|
||||
serialize={identity}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
|
||||
<Select
|
||||
isDisabled={notifSettings.useNative === "always"}
|
||||
placeholder="Notification Position"
|
||||
options={[
|
||||
{ label: "Bottom Right", value: "bottom-right", default: true },
|
||||
{ label: "Top Right", value: "top-right" },
|
||||
]satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
|
||||
select={v => notifSettings.position = v}
|
||||
isSelected={v => v === notifSettings.position}
|
||||
serialize={identity}
|
||||
/>
|
||||
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
|
||||
<Slider
|
||||
disabled={notifSettings.useNative === "always"}
|
||||
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
|
||||
minValue={0}
|
||||
maxValue={20_000}
|
||||
initialValue={notifSettings.timeout}
|
||||
onValueChange={v => notifSettings.timeout = v}
|
||||
onValueRender={v => (v / 1000).toFixed(2) + "s"}
|
||||
onMarkerRender={v => (v / 1000) + "s"}
|
||||
stickToMarkers={false}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -121,18 +205,10 @@ interface DonateCardProps {
|
|||
|
||||
function DonateCard({ image }: DonateCardProps) {
|
||||
return (
|
||||
<Card style={{
|
||||
padding: "1em",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
marginBottom: "1em",
|
||||
marginTop: "1em"
|
||||
}}>
|
||||
<Card className={cl("card", "donate")}>
|
||||
<div>
|
||||
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle>
|
||||
<Forms.FormText>
|
||||
Please consider supporting the development of Vencord by donating!
|
||||
</Forms.FormText>
|
||||
<Forms.FormText>Please consider supporting the development of Vencord by donating!</Forms.FormText>
|
||||
<DonateButton style={{ transform: "translateX(-1em)" }} />
|
||||
</div>
|
||||
<img
|
||||
|
@ -140,7 +216,7 @@ function DonateCard({ image }: DonateCardProps) {
|
|||
src={image}
|
||||
alt=""
|
||||
height={128}
|
||||
style={{ marginLeft: "auto", transform: "rotate(10deg)" }}
|
||||
style={{ marginLeft: "auto", transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : "" }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -18,9 +18,10 @@
|
|||
|
||||
import "./settingsStyles.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { Forms, Router, Text } from "@webpack/common";
|
||||
import { Forms, SettingsRouter, Text } from "@webpack/common";
|
||||
|
||||
import BackupRestoreTab from "./BackupRestoreTab";
|
||||
import PluginsTab from "./PluginsTab";
|
||||
|
@ -28,7 +29,7 @@ import ThemesTab from "./ThemesTab";
|
|||
import Updater from "./Updater";
|
||||
import VencordSettings from "./VencordTab";
|
||||
|
||||
const st = (style: string) => `vcSettings${style}`;
|
||||
const cl = classNameFactory("vc-settings-");
|
||||
|
||||
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');
|
||||
|
||||
|
@ -62,15 +63,15 @@ function Settings(props: SettingsProps) {
|
|||
<TabBar
|
||||
type={TabBar.Types.TOP}
|
||||
look={TabBar.Looks.BRAND}
|
||||
className={st("TabBar")}
|
||||
className={cl("tab-bar")}
|
||||
selectedItem={tab}
|
||||
onItemSelect={Router.open}
|
||||
onItemSelect={SettingsRouter.open}
|
||||
>
|
||||
{Object.entries(SettingsTabs).map(([key, { name, component }]) => {
|
||||
if (!component) return null;
|
||||
return <TabBar.Item
|
||||
id={key}
|
||||
className={st("TabBarItem")}
|
||||
className={cl("tab-bar-item")}
|
||||
key={key}>
|
||||
{name}
|
||||
</TabBar.Item>;
|
||||
|
|
|
@ -1,23 +1,40 @@
|
|||
.vcSettingsTabBar {
|
||||
.vc-settings-tab-bar {
|
||||
margin-top: 20px;
|
||||
margin-bottom: -2px;
|
||||
border-bottom: 2px solid var(--background-modifier-accent);
|
||||
}
|
||||
|
||||
.vcSettingsTabBarItem {
|
||||
.vc-settings-tab-bar-item {
|
||||
margin-right: 32px;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.vcSettingsQuickActionCard {
|
||||
.vc-settings-quick-actions-card {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
flex-flow: row wrap;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.vc-settings-donate {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.vc-settings-card {
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.vc-backup-restore-card {
|
||||
background-color: var(--info-warning-background);
|
||||
border-color: var(--info-warning-foreground);
|
||||
color: var(--info-warning-text);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
|
@ -67,9 +67,18 @@ export async function installExt(id: string) {
|
|||
try {
|
||||
await access(extDir, fsConstants.F_OK);
|
||||
} catch (err) {
|
||||
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`;
|
||||
const buf = await get(url);
|
||||
await extract(crxToZip(buf), extDir);
|
||||
const url = id === "fmkadmapgofadopljbjfkapdkoienihi"
|
||||
// React Devtools v4.25
|
||||
// v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843
|
||||
// Unfortunately, Google does not serve old versions, so this is the only way
|
||||
? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip"
|
||||
: `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`;
|
||||
const buf = await get(url, {
|
||||
headers: {
|
||||
"User-Agent": "Vencord (https://github.com/Vendicated/Vencord)"
|
||||
}
|
||||
});
|
||||
await extract(crxToZip(buf), extDir).catch(console.error);
|
||||
}
|
||||
|
||||
session.defaultSession.loadExtension(extDir);
|
||||
|
|
|
@ -16,13 +16,12 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./legacy";
|
||||
import "./updater";
|
||||
|
||||
import { debounce } from "@utils/debounce";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import { Queue } from "@utils/Queue";
|
||||
import { BrowserWindow, desktopCapturer, ipcMain, shell } from "electron";
|
||||
import { BrowserWindow, ipcMain, shell } from "electron";
|
||||
import { mkdirSync, readFileSync, watch } from "fs";
|
||||
import { open, readFile, writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
@ -45,9 +44,6 @@ export function readSettings() {
|
|||
}
|
||||
}
|
||||
|
||||
// Fix for screensharing in Electron >= 17
|
||||
ipcMain.handle(IpcEvents.GET_DESKTOP_CAPTURE_SOURCES, (_, opts) => desktopCapturer.getSources(opts));
|
||||
|
||||
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
|
||||
|
||||
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
||||
|
@ -81,7 +77,7 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
|
|||
export function initIpc(mainWindow: BrowserWindow) {
|
||||
open(QUICKCSS_PATH, "a+").then(fd => {
|
||||
fd.close();
|
||||
watch(QUICKCSS_PATH, debounce(async () => {
|
||||
watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
|
||||
mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
|
||||
}, 50));
|
||||
});
|
||||
|
@ -95,7 +91,8 @@ ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
|||
webPreferences: {
|
||||
preload: join(__dirname, "preload.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false
|
||||
nodeIntegration: false,
|
||||
sandbox: false
|
||||
}
|
||||
});
|
||||
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
|
||||
|
|
|
@ -28,7 +28,9 @@ const VENCORD_SRC_DIR = join(__dirname, "..");
|
|||
|
||||
const execFile = promisify(cpExecFile);
|
||||
|
||||
const isFlatpak = Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
|
||||
const isFlatpak = process.platform === "linux" && Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
|
||||
|
||||
if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`;
|
||||
|
||||
function git(...args: string[]) {
|
||||
const opts = { cwd: VENCORD_SRC_DIR };
|
||||
|
@ -66,10 +68,10 @@ async function pull() {
|
|||
async function build() {
|
||||
const opts = { cwd: VENCORD_SRC_DIR };
|
||||
|
||||
let res;
|
||||
const command = isFlatpak ? "flatpak-spawn" : "node";
|
||||
const args = isFlatpak ? ["--host", "node", "scripts/build/build.mjs"] : ["scripts/build/build.mjs"];
|
||||
|
||||
if (isFlatpak) res = await execFile("flatpak-spawn", ["--host", "node", "scripts/build/build.mjs"], opts);
|
||||
else res = await execFile("node", ["scripts/build/build.mjs"], opts);
|
||||
const res = await execFile(command, args, opts);
|
||||
|
||||
return !res.stderr.includes("Build failed");
|
||||
}
|
||||
|
|
|
@ -37,10 +37,7 @@ async function githubGet(endpoint: string) {
|
|||
Accept: "application/vnd.github+json",
|
||||
// "All API requests MUST include a valid User-Agent header.
|
||||
// Requests with no User-Agent header will be rejected."
|
||||
"User-Agent": VENCORD_USER_AGENT,
|
||||
// todo: perhaps add support for (optional) api token?
|
||||
// unauthorised rate limit is 60 reqs/h
|
||||
// https://github.com/settings/tokens/new?description=Vencord%20Updater
|
||||
"User-Agent": VENCORD_USER_AGENT
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -52,7 +49,7 @@ async function calculateGitChanges() {
|
|||
const res = await githubGet(`/compare/${gitHash}...HEAD`);
|
||||
|
||||
const data = JSON.parse(res.toString("utf-8"));
|
||||
return data.commits.map(c => ({
|
||||
return data.commits.map((c: any) => ({
|
||||
// github api only sends the long sha
|
||||
hash: c.sha.slice(0, 7),
|
||||
author: c.author.login,
|
||||
|
|
3
src/modules.d.ts
vendored
3
src/modules.d.ts
vendored
|
@ -38,7 +38,8 @@ declare module "~fileContent/*" {
|
|||
export default content;
|
||||
}
|
||||
|
||||
declare module "*.css" { }
|
||||
declare module "*.css";
|
||||
|
||||
declare module "*.css?managed" {
|
||||
const name: string;
|
||||
export default name;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import { app, autoUpdater } from "electron";
|
||||
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
|
||||
import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
|
||||
import { basename, dirname, join } from "path";
|
||||
|
||||
const { setAppUserModelId } = app;
|
||||
|
@ -44,6 +44,7 @@ function isNewer($new: string, old: string) {
|
|||
}
|
||||
|
||||
function patchLatest() {
|
||||
try {
|
||||
const currentAppPath = dirname(process.execPath);
|
||||
const currentVersion = basename(currentAppPath);
|
||||
const discordPath = join(currentAppPath, "..");
|
||||
|
@ -56,46 +57,37 @@ function patchLatest() {
|
|||
|
||||
if (latestVersion === currentVersion) return;
|
||||
|
||||
const app = join(discordPath, latestVersion, "resources", "app");
|
||||
if (existsSync(app)) return;
|
||||
const resources = join(discordPath, latestVersion, "resources");
|
||||
const app = join(resources, "app.asar");
|
||||
const _app = join(resources, "_app.asar");
|
||||
|
||||
if (!existsSync(app) || statSync(app).isDirectory()) return;
|
||||
|
||||
console.info("[Vencord] Detected Host Update. Repatching...");
|
||||
|
||||
const patcherPath = join(__dirname, "patcher.js");
|
||||
renameSync(app, _app);
|
||||
mkdirSync(app);
|
||||
writeFileSync(join(app, "package.json"), JSON.stringify({
|
||||
name: "discord",
|
||||
main: "index.js"
|
||||
}));
|
||||
writeFileSync(join(app, "index.js"), `require(${JSON.stringify(patcherPath)});`);
|
||||
writeFileSync(join(app, "index.js"), `require(${JSON.stringify(join(__dirname, "patcher.js"))});`);
|
||||
} catch (err) {
|
||||
console.error("[Vencord] Failed to repatch latest host update", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we
|
||||
// need to reinject
|
||||
function patchUpdater() {
|
||||
const main = require.main!;
|
||||
const buildInfo = require(join(process.resourcesPath, "build_info.json"));
|
||||
|
||||
try {
|
||||
if (buildInfo?.newUpdater) {
|
||||
const autoStartScript = join(main.filename, "..", "autoStart", "win32.js");
|
||||
const autoStartScript = join(require.main!.filename, "..", "autoStart", "win32.js");
|
||||
const { update } = require(autoStartScript);
|
||||
|
||||
// New Updater Injection
|
||||
require.cache[autoStartScript]!.exports.update = function () {
|
||||
patchLatest();
|
||||
update.apply(this, arguments);
|
||||
};
|
||||
} else {
|
||||
const hostUpdaterScript = join(main.filename, "..", "hostUpdater.js");
|
||||
const { quitAndInstall } = require(hostUpdaterScript);
|
||||
|
||||
// Old Updater Injection
|
||||
require.cache[hostUpdaterScript]!.exports.quitAndInstall = function () {
|
||||
patchLatest();
|
||||
quitAndInstall.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// OpenAsar uses electrons autoUpdater on Windows
|
||||
const { quitAndInstall } = autoUpdater;
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
*/
|
||||
|
||||
import { onceDefined } from "@utils/onceDefined";
|
||||
import electron, { app, BrowserWindowConstructorOptions } from "electron";
|
||||
import { readFileSync } from "fs";
|
||||
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
import { initIpc } from "./ipcMain";
|
||||
|
@ -43,16 +42,48 @@ require.main!.filename = join(asarPath, discordPkg.main);
|
|||
app.setAppPath(asarPath);
|
||||
|
||||
if (!process.argv.includes("--vanilla")) {
|
||||
let settings: typeof import("@api/settings").Settings = {} as any;
|
||||
try {
|
||||
settings = JSON.parse(readSettings());
|
||||
} catch { }
|
||||
|
||||
// Repatch after host updates on Windows
|
||||
if (process.platform === "win32")
|
||||
if (process.platform === "win32") {
|
||||
require("./patchWin32Updater");
|
||||
|
||||
if (settings.winCtrlQ) {
|
||||
const originalBuild = Menu.buildFromTemplate;
|
||||
Menu.buildFromTemplate = function (template) {
|
||||
if (template[0]?.label === "&File") {
|
||||
const { submenu } = template[0];
|
||||
if (Array.isArray(submenu)) {
|
||||
submenu.push({
|
||||
label: "Quit (Hidden)",
|
||||
visible: false,
|
||||
acceleratorWorksWhenHidden: true,
|
||||
accelerator: "Control+Q",
|
||||
click: () => app.quit()
|
||||
});
|
||||
}
|
||||
}
|
||||
return originalBuild.call(this, template);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class BrowserWindow extends electron.BrowserWindow {
|
||||
constructor(options: BrowserWindowConstructorOptions) {
|
||||
if (options?.webPreferences?.preload && options.title) {
|
||||
const original = options.webPreferences.preload;
|
||||
options.webPreferences.preload = join(__dirname, "preload.js");
|
||||
options.webPreferences.sandbox = false;
|
||||
if (settings.frameless) {
|
||||
options.frame = false;
|
||||
}
|
||||
if (settings.transparent) {
|
||||
options.transparent = true;
|
||||
options.backgroundColor = "#00000000";
|
||||
}
|
||||
|
||||
process.env.DISCORD_PRELOAD = original;
|
||||
|
||||
|
@ -100,8 +131,7 @@ if (!process.argv.includes("--vanilla")) {
|
|||
});
|
||||
|
||||
try {
|
||||
const settings = JSON.parse(readSettings());
|
||||
if (settings.enableReactDevtools)
|
||||
if (settings?.enableReactDevtools)
|
||||
installExt("fmkadmapgofadopljbjfkapdkoienihi")
|
||||
.then(() => console.info("[Vencord] Installed React Developer Tools"))
|
||||
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
|
||||
|
@ -160,21 +190,4 @@ if (!process.argv.includes("--vanilla")) {
|
|||
}
|
||||
|
||||
console.log("[Vencord] Loading original Discord app.asar");
|
||||
// Legacy Vencord Injector requires "../app.asar". However, because we
|
||||
// restore the require.main above this is messed up, so monkey patch Module._load to
|
||||
// redirect such requires
|
||||
// FIXME: remove this eventually
|
||||
if (readFileSync(injectorPath, "utf-8").includes('require("../app.asar")')) {
|
||||
console.warn("[Vencord] [--> WARNING <--] You have a legacy Vencord install. Please reinject");
|
||||
const Module = require("module");
|
||||
const loadModule = Module._load;
|
||||
Module._load = function (path: string) {
|
||||
if (path === "../app.asar") {
|
||||
Module._load = loadModule;
|
||||
arguments[0] = require.main!.filename;
|
||||
}
|
||||
return loadModule.apply(this, arguments);
|
||||
};
|
||||
} else {
|
||||
require(require.main!.filename);
|
||||
}
|
||||
|
|
42
src/plugins/alwaysTrust.ts
Normal file
42
src/plugins/alwaysTrust.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "AlwaysTrust",
|
||||
description: "Removes the annoying untrusted domain and suspicious file popup",
|
||||
authors: [Devs.zt],
|
||||
patches: [
|
||||
{
|
||||
find: ".displayName=\"MaskedLinkStore\"",
|
||||
replacement: {
|
||||
match: /\.isTrustedDomain=function\(.\){return.+?};/,
|
||||
replace: ".isTrustedDomain=function(){return true};"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "\"github.com\":new RegExp(\"\\\\/releases\\\\S*\\\\/download\"),",
|
||||
replacement: {
|
||||
match: /const o=JSON.parse\('\[.+?'\)/,
|
||||
replace: "const o=[]"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
|
@ -36,7 +36,7 @@ export default definePlugin({
|
|||
replacement: {
|
||||
match: /uploadFiles:(.{1,2}),/,
|
||||
replace:
|
||||
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=Vencord.Plugins.plugins.AnonymiseFileNames.anonymise(f.filename)),$1(...args)),",
|
||||
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f.filename)),$1(...args)),",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -73,7 +73,7 @@ export default definePlugin({
|
|||
replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,`
|
||||
},
|
||||
{
|
||||
match: /spacing:(\d{1,2}),children:(.{1,40}(.{1,2})\.jsx.+(.{1,2})\.onClick.+\)})},/,
|
||||
match: /spacing:(\d{1,2}),children:(.{1,40}(\i)\.jsx.+?(\i)\.onClick.+?\)})},/,
|
||||
// if the badge provides it's own component, render that instead of an image
|
||||
// the badge also includes info about the user that has it (type BadgeUserArgs), which is why it's passed as props
|
||||
replace: (_, s, origBadgeComponent, React, badge) =>
|
||||
|
|
|
@ -50,10 +50,10 @@ export default definePlugin({
|
|||
},
|
||||
// Show plugin name instead of "Built-In"
|
||||
{
|
||||
find: "().source,children",
|
||||
find: ".source,children",
|
||||
replacement: {
|
||||
// ...children: p?.name
|
||||
match: /(?<=:(.{1,3})\.displayDescription\}.{0,200}\(\)\.source,children:)[^}]+/,
|
||||
match: /(?<=:(.{1,3})\.displayDescription\}.{0,200}\.source,children:)[^}]+/,
|
||||
replace: "$1.plugin||($&)"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,9 +25,9 @@ export default definePlugin({
|
|||
authors: [Devs.Cyn],
|
||||
patches: [
|
||||
{
|
||||
find: "_messageAttachmentToEmbedMedia",
|
||||
find: ".Messages.REMOVE_ATTACHMENT_BODY",
|
||||
replacement: {
|
||||
match: /(\(\)\.container\)?,children:)(\[[^\]]+\])(}\)\};return)/,
|
||||
match: /(.container\)?,children:)(\[[^\]]+\])(}\)\};return)/,
|
||||
replace: (_, pre, accessories, post) =>
|
||||
`${pre}Vencord.Api.MessageAccessories._modifyAccessories(${accessories},this.props)${post}`,
|
||||
},
|
||||
|
|
|
@ -27,7 +27,7 @@ export default definePlugin({
|
|||
{
|
||||
find: ".withMentionPrefix",
|
||||
replacement: {
|
||||
match: /(\(\).roleDot.{10,50}{children:.{1,2})}\)/,
|
||||
match: /(.roleDot.{10,50}{children:.{1,2})}\)/,
|
||||
replace: "$1.concat(Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0]))})"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,12 +22,17 @@ import definePlugin from "@utils/types";
|
|||
export default definePlugin({
|
||||
name: "MessagePopoverAPI",
|
||||
description: "API to add buttons to message popovers.",
|
||||
authors: [Devs.KingFish],
|
||||
authors: [Devs.KingFish, Devs.Ven],
|
||||
patches: [{
|
||||
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
|
||||
replacement: {
|
||||
match: /(message:(.).{0,100}Fragment,\{children:\[)(.{0,90}renderPopout:.{0,200}message_reaction_emoji_picker.+?return (.{1,3})\(.{0,30}"add-reaction")/,
|
||||
replace: "$1...Vencord.Api.MessagePopover._buildPopoverElements($2,$4),$3"
|
||||
// foo && !bar ? createElement(blah,...makeElement(addReactionData))
|
||||
match: /(\i&&!\i)\?\(0,\i\.jsxs?\)\(.{0,20}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/,
|
||||
replace: (m, bools, makeElement) => {
|
||||
const msg = m.match(/message:(.{1,3}),/)?.[1];
|
||||
if (!msg) throw new Error("Could not find message variable");
|
||||
return `...(${bools}?Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}):[]),${m}`;
|
||||
}
|
||||
}
|
||||
}],
|
||||
});
|
||||
|
|
|
@ -34,8 +34,8 @@ export default definePlugin({
|
|||
";if(Vencord.Api.Notices.currentNotice)return false$&"
|
||||
},
|
||||
{
|
||||
match: /(?<=NOTICE_DISMISS:function.+?){(?=if\(null==(.+?)\))/,
|
||||
replace: '{if($1?.id=="VencordNotice")return ($1=null,Vencord.Api.Notices.nextNotice(),true);'
|
||||
match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/,
|
||||
replace: 'if($1?.id=="VencordNotice")return($1=null,Vencord.Api.Notices.nextNotice(),true);'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ export default definePlugin({
|
|||
replacement: {
|
||||
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
|
||||
replace:
|
||||
"Vencord.Plugins.plugins.BetterGifAltText.altify(e);$1",
|
||||
"$self.altify(e);$1",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -39,7 +39,7 @@ export default definePlugin({
|
|||
replacement: {
|
||||
match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/,
|
||||
replace:
|
||||
"?($1.alt='GIF',Vencord.Plugins.plugins.BetterGifAltText.altify($1))",
|
||||
"?($1.alt='GIF',$self.altify($1))",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -33,7 +33,7 @@ export default definePlugin({
|
|||
find: "M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V16C20 18.2091 18.2091 20 16 20H4C1.79086 20 0 18.2091 0 16V4Z",
|
||||
replacement: {
|
||||
match: /viewBox:"0 0 20 20"/,
|
||||
replace: "$&,onClick:()=>Vencord.Plugins.plugins.BetterRoleDot.copyToClipBoard(e.color),style:{cursor:'pointer'}",
|
||||
replace: "$&,onClick:()=>$self.copyToClipBoard(e.color),style:{cursor:'pointer'}",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -41,7 +41,7 @@ export default definePlugin({
|
|||
all: true,
|
||||
predicate: () => Settings.plugins.BetterRoleDot.bothStyles,
|
||||
replacement: {
|
||||
match: /"(?:username|dot)"===\w\b/g,
|
||||
match: /"(?:username|dot)"===\w(?!\.\w)/g,
|
||||
replace: "true",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -43,12 +43,12 @@ export default definePlugin({
|
|||
|
||||
patches: [
|
||||
{
|
||||
find: "().embedWrapper,embed",
|
||||
find: ".embedWrapper,embed",
|
||||
replacement: [{
|
||||
match: /(\.renderEmbed=.+?(.)=.\.props)(.+?\(\)\.embedWrapper)/g,
|
||||
match: /(\.renderEmbed=.+?(.)=.\.props)(.+?\.embedWrapper)/g,
|
||||
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
||||
}, {
|
||||
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\(\)\.embedWrapper)/g,
|
||||
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g,
|
||||
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
||||
}]
|
||||
}
|
||||
|
|
|
@ -74,8 +74,8 @@ export default definePlugin({
|
|||
patches: [{
|
||||
find: ".renderConnectionStatus=",
|
||||
replacement: {
|
||||
match: /(?<=renderConnectionStatus=.+\(\)\.channel,children:)\w/,
|
||||
replace: "[$&, Vencord.Plugins.plugins.CallTimer.renderTimer(this.props.channel.id)]"
|
||||
match: /(?<=renderConnectionStatus=.+\.channel,children:)\w/,
|
||||
replace: "[$&, $self.renderTimer(this.props.channel.id)]"
|
||||
}
|
||||
}],
|
||||
renderTimer(channelId: string) {
|
||||
|
|
37
src/plugins/colorSighted.ts
Normal file
37
src/plugins/colorSighted.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "ColorSighted",
|
||||
description: "Removes the colorblind-friendly icons from statuses, just like 2015-2017 Discord",
|
||||
authors: [Devs.lewisakura],
|
||||
patches: [
|
||||
{
|
||||
find: "Masks.STATUS_ONLINE",
|
||||
replacement: {
|
||||
// we can use global replacement here - these are specific to the status icons and are used nowhere else,
|
||||
// so it keeps the patch and plugin small and simple
|
||||
match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g,
|
||||
replace: "Masks.STATUS_ONLINE"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
|
@ -99,7 +99,7 @@ export default definePlugin({
|
|||
const newName = video.name.replace(/\.mp4$/i, ".corrupt.mp4");
|
||||
const promptToUpload = findByCode("UPLOAD_FILE_LIMIT_ERROR");
|
||||
const file = new File([buf], newName, { type: "video/mp4" });
|
||||
setImmediate(() => promptToUpload([file], ctx.channel, DRAFT_TYPE));
|
||||
setTimeout(() => promptToUpload([file], ctx.channel, DRAFT_TYPE), 10);
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
|
251
src/plugins/customRPC.tsx
Normal file
251
src/plugins/customRPC.tsx
Normal file
|
@ -0,0 +1,251 @@
|
|||
/*
|
||||
* 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 { definePluginSettings } from "@api/settings";
|
||||
import { Link } from "@components/Link";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { useAwaiter } from "@utils/misc";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
|
||||
import {
|
||||
FluxDispatcher,
|
||||
Forms,
|
||||
GuildStore,
|
||||
React,
|
||||
SelectedChannelStore,
|
||||
SelectedGuildStore,
|
||||
UserStore
|
||||
} from "@webpack/common";
|
||||
|
||||
const ActivityComponent = findByCodeLazy("onOpenGameProfile");
|
||||
const ActivityClassName = findByPropsLazy("activity", "buttonColor");
|
||||
const Colors = findByPropsLazy("profileColors");
|
||||
|
||||
// START yoinked from lastfm.tsx
|
||||
const assetManager = mapMangledModuleLazy(
|
||||
"getAssetImage: size must === [number, number] for Twitch",
|
||||
{
|
||||
getAsset: filters.byCode("apply("),
|
||||
}
|
||||
);
|
||||
|
||||
async function getApplicationAsset(key: string): Promise<string> {
|
||||
return (await assetManager.getAsset(settings.store.appID, [key, undefined]))[0];
|
||||
}
|
||||
|
||||
interface ActivityAssets {
|
||||
large_image?: string;
|
||||
large_text?: string;
|
||||
small_image?: string;
|
||||
small_text?: string;
|
||||
}
|
||||
|
||||
interface Activity {
|
||||
state: string;
|
||||
details?: string;
|
||||
timestamps?: {
|
||||
start?: Number;
|
||||
end?: Number;
|
||||
};
|
||||
assets?: ActivityAssets;
|
||||
buttons?: Array<string>;
|
||||
name: string;
|
||||
application_id: string;
|
||||
metadata?: {
|
||||
button_urls?: Array<string>;
|
||||
};
|
||||
type: ActivityType;
|
||||
flags: Number;
|
||||
}
|
||||
|
||||
enum ActivityType {
|
||||
PLAYING = 0,
|
||||
LISTENING = 2,
|
||||
WATCHING = 3,
|
||||
COMPETING = 5
|
||||
}
|
||||
// END
|
||||
|
||||
const strOpt = (description: string) => ({
|
||||
type: OptionType.STRING,
|
||||
description,
|
||||
onChange: setRpc
|
||||
}) as const;
|
||||
|
||||
const numOpt = (description: string) => ({
|
||||
type: OptionType.NUMBER,
|
||||
description,
|
||||
onChange: setRpc
|
||||
}) as const;
|
||||
|
||||
const choice = (label: string, value: any, _default?: Boolean) => ({
|
||||
label,
|
||||
value,
|
||||
default: _default
|
||||
}) as const;
|
||||
|
||||
const choiceOpt = (description: string, options) => ({
|
||||
type: OptionType.SELECT,
|
||||
description,
|
||||
onChange: setRpc,
|
||||
options
|
||||
}) as const;
|
||||
|
||||
|
||||
const settings = definePluginSettings({
|
||||
appID: strOpt("The ID of the application for the rich presence."),
|
||||
appName: strOpt("The name of the presence."),
|
||||
details: strOpt("Line 1 of rich presence."),
|
||||
state: strOpt("Line 2 of rich presence."),
|
||||
type: choiceOpt("Type of presence", [
|
||||
choice("Playing", ActivityType.PLAYING, true),
|
||||
choice("Listening", ActivityType.LISTENING),
|
||||
choice("Watching", ActivityType.WATCHING),
|
||||
choice("Competing", ActivityType.COMPETING)
|
||||
]),
|
||||
startTime: numOpt("Unix Timestamp for beginning of activity."),
|
||||
endTime: numOpt("Unix Timestamp for end of activity."),
|
||||
imageBig: strOpt("Sets the big image to the specified image."),
|
||||
imageBigTooltip: strOpt("Sets the tooltip text for the big image."),
|
||||
imageSmall: strOpt("Sets the small image to the specified image."),
|
||||
imageSmallTooltip: strOpt("Sets the tooltip text for the small image."),
|
||||
buttonOneText: strOpt("The text for the first button"),
|
||||
buttonOneURL: strOpt("The URL for the first button"),
|
||||
buttonTwoText: strOpt("The text for the second button"),
|
||||
buttonTwoURL: strOpt("The URL for the second button")
|
||||
});
|
||||
|
||||
async function createActivity(): Promise<Activity | undefined> {
|
||||
const {
|
||||
appID,
|
||||
appName,
|
||||
details,
|
||||
state,
|
||||
type,
|
||||
startTime,
|
||||
endTime,
|
||||
imageBig,
|
||||
imageBigTooltip,
|
||||
imageSmall,
|
||||
imageSmallTooltip,
|
||||
buttonOneText,
|
||||
buttonOneURL,
|
||||
buttonTwoText,
|
||||
buttonTwoURL
|
||||
} = settings.store;
|
||||
|
||||
if (!appName) return;
|
||||
|
||||
const activity: Activity = {
|
||||
application_id: appID || "0",
|
||||
name: appName,
|
||||
state,
|
||||
details,
|
||||
type,
|
||||
flags: 1 << 0,
|
||||
};
|
||||
|
||||
if (startTime) {
|
||||
activity.timestamps = {
|
||||
start: startTime,
|
||||
};
|
||||
if (endTime) {
|
||||
activity.timestamps.end = endTime;
|
||||
}
|
||||
}
|
||||
|
||||
if (buttonOneText) {
|
||||
activity.buttons = [
|
||||
buttonOneText,
|
||||
buttonTwoText
|
||||
].filter(Boolean);
|
||||
|
||||
activity.metadata = {
|
||||
button_urls: [
|
||||
buttonOneURL,
|
||||
buttonTwoURL
|
||||
].filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
if (imageBig) {
|
||||
activity.assets = {
|
||||
large_image: await getApplicationAsset(imageBig),
|
||||
large_text: imageBigTooltip
|
||||
};
|
||||
}
|
||||
|
||||
if (imageSmall) {
|
||||
activity.assets = {
|
||||
...activity.assets,
|
||||
small_image: await getApplicationAsset(imageSmall),
|
||||
small_text: imageSmallTooltip
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
for (const k in activity) {
|
||||
if (k === "type") continue; // without type, the presence is considered invalid.
|
||||
const v = activity[k];
|
||||
if (!v || v.length === 0)
|
||||
delete activity[k];
|
||||
}
|
||||
|
||||
// WHAT DO YOU WANT FROM ME
|
||||
// eslint-disable-next-line consistent-return
|
||||
return activity;
|
||||
}
|
||||
|
||||
async function setRpc(disable?: Boolean) {
|
||||
const activity: Activity | undefined = await createActivity();
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
type: "LOCAL_ACTIVITY_UPDATE",
|
||||
activity: !disable ? activity : {}
|
||||
});
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "CustomRPC",
|
||||
description: "Allows you to set a custom rich presence.",
|
||||
authors: [Devs.captain],
|
||||
start: setRpc,
|
||||
stop: () => setRpc(true),
|
||||
settings,
|
||||
|
||||
settingsAboutComponent: () => {
|
||||
const activity = useAwaiter(createActivity);
|
||||
return (
|
||||
<>
|
||||
<Forms.FormTitle tag="h2">NOTE:</Forms.FormTitle>
|
||||
<Forms.FormText>
|
||||
You will need to <Link href="https://discord.com/developers/applications">create an
|
||||
application</Link> and
|
||||
get its ID to use this plugin.
|
||||
</Forms.FormText>
|
||||
<Forms.FormDivider />
|
||||
<div style={{ width: "284px" }} className={Colors.profileColors}>
|
||||
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
|
||||
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
|
||||
application={{ id: settings.store.appID }}
|
||||
user={UserStore.getCurrentUser()} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -57,7 +57,7 @@ async function doClone(guildId: string, id: string, name: string, isAnimated: bo
|
|||
reader.onload = () => {
|
||||
uploadEmoji({
|
||||
guildId,
|
||||
name,
|
||||
name: name.split("~")[0],
|
||||
image: reader.result
|
||||
}).then(() => {
|
||||
Toasts.show({
|
||||
|
@ -187,7 +187,7 @@ export default definePlugin({
|
|||
find: "open-native-link",
|
||||
replacement: {
|
||||
match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
|
||||
replace: "$&,Vencord.Plugins.plugins.EmoteCloner.makeMenu(arguments[2])"
|
||||
replace: "$&,$self.makeMenu(arguments[2])"
|
||||
},
|
||||
|
||||
},
|
||||
|
|
|
@ -22,11 +22,25 @@ import { Devs } from "@utils/constants";
|
|||
import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||
import { ChannelStore, UserStore } from "@webpack/common";
|
||||
import { ChannelStore, PermissionStore, UserStore } from "@webpack/common";
|
||||
|
||||
const DRAFT_TYPE = 0;
|
||||
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
|
||||
|
||||
const USE_EXTERNAL_EMOJIS = 1n << 18n;
|
||||
const USE_EXTERNAL_STICKERS = 1n << 37n;
|
||||
|
||||
enum EmojiIntentions {
|
||||
REACTION = 0,
|
||||
STATUS = 1,
|
||||
COMMUNITY_CONTENT = 2,
|
||||
CHAT = 3,
|
||||
GUILD_STICKER_RELATED_EMOJI = 4,
|
||||
GUILD_ROLE_BENEFIT_EMOJI = 5,
|
||||
COMMUNITY_CONTENT_ONLY = 6,
|
||||
SOUNDBOARD = 7
|
||||
}
|
||||
|
||||
interface BaseSticker {
|
||||
available: boolean;
|
||||
description: string;
|
||||
|
@ -58,26 +72,39 @@ migratePluginSettings("FakeNitro", "NitroBypass");
|
|||
|
||||
export default definePlugin({
|
||||
name: "FakeNitro",
|
||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity],
|
||||
description: "Allows you to stream in nitro quality and send fake emojis/stickers.",
|
||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain],
|
||||
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
|
||||
dependencies: ["MessageEventsAPI"],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "canUseAnimatedEmojis:function",
|
||||
find: ".PREMIUM_LOCKED;",
|
||||
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
||||
replacement: [
|
||||
"canUseAnimatedEmojis",
|
||||
"canUseEmojisEverywhere"
|
||||
].map(func => {
|
||||
return {
|
||||
match: new RegExp(`${func}:function\\(.+?\\{`),
|
||||
replace: "$&return true;"
|
||||
};
|
||||
})
|
||||
{
|
||||
match: /(?<=(?<intention>\i)=\i\.intention)/,
|
||||
replace: ",fakeNitroIntention=$<intention>"
|
||||
},
|
||||
{
|
||||
match: /(?<=\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i)(?=\))/g,
|
||||
replace: ",fakeNitroIntention"
|
||||
},
|
||||
{
|
||||
match: /(?<=&&!\i&&)!(?<canUseExternal>\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
|
||||
replace: `(!$<canUseExternal>&&![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "canUseAnimatedEmojis:function",
|
||||
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
||||
replacement: {
|
||||
match: /(?<=(?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\((?<user>\i))\){(?<premiumCheck>.+?\))/g,
|
||||
replace: `,fakeNitroIntention){$<premiumCheck>||fakeNitroIntention===undefined||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "canUseStickersEverywhere:function",
|
||||
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
|
||||
replacement: {
|
||||
match: /canUseStickersEverywhere:function\(.+?\{/,
|
||||
|
@ -93,7 +120,7 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
{
|
||||
find: "canUseAnimatedEmojis:function",
|
||||
find: "canStreamHighQuality:function",
|
||||
predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
|
||||
replacement: [
|
||||
"canUseHighVideoUploadQuality",
|
||||
|
@ -114,6 +141,13 @@ export default definePlugin({
|
|||
replace: ""
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "canUseClientThemes:function",
|
||||
replacement: {
|
||||
match: /(?<=canUseClientThemes:function\(\i\){)/,
|
||||
replace: "return true;"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
options: {
|
||||
|
@ -161,6 +195,22 @@ export default definePlugin({
|
|||
return (UserStore.getCurrentUser().premiumType ?? 0) > 1;
|
||||
},
|
||||
|
||||
hasPermissionToUseExternalEmojis(channelId: string) {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
|
||||
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
|
||||
|
||||
return PermissionStore.can(USE_EXTERNAL_EMOJIS, channel);
|
||||
},
|
||||
|
||||
hasPermissionToUseExternalStickers(channelId: string) {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
|
||||
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
|
||||
|
||||
return PermissionStore.can(USE_EXTERNAL_STICKERS, channel);
|
||||
},
|
||||
|
||||
getStickerLink(stickerId: string) {
|
||||
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.FakeNitro.stickerSize}`;
|
||||
},
|
||||
|
@ -245,7 +295,7 @@ export default definePlugin({
|
|||
if (!sticker)
|
||||
break stickerBypass;
|
||||
|
||||
if (sticker.available !== false && (this.canUseStickers || (sticker as GuildSticker)?.guild_id === guildId))
|
||||
if (sticker.available !== false && ((this.canUseStickers && this.hasPermissionToUseExternalStickers(channelId)) || (sticker as GuildSticker)?.guild_id === guildId))
|
||||
break stickerBypass;
|
||||
|
||||
let link = this.getStickerLink(sticker.id);
|
||||
|
@ -268,7 +318,7 @@ export default definePlugin({
|
|||
}
|
||||
}
|
||||
|
||||
if (!this.canUseEmotes && settings.enableEmojiBypass) {
|
||||
if ((!this.canUseEmotes || !this.hasPermissionToUseExternalEmojis(channelId)) && settings.enableEmojiBypass) {
|
||||
for (const emoji of messageObj.validNonShortcutEmojis) {
|
||||
if (!emoji.require_colons) continue;
|
||||
if (emoji.guildId === guildId && !emoji.animated) continue;
|
||||
|
@ -284,8 +334,9 @@ export default definePlugin({
|
|||
return { cancel: false };
|
||||
});
|
||||
|
||||
if (!this.canUseEmotes && settings.enableEmojiBypass) {
|
||||
this.preEdit = addPreEditListener((_, __, messageObj) => {
|
||||
this.preEdit = addPreEditListener((channelId, __, messageObj) => {
|
||||
if (this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId)) return;
|
||||
|
||||
const { guildId } = this;
|
||||
|
||||
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
|
||||
|
@ -299,7 +350,6 @@ export default definePlugin({
|
|||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
stop() {
|
||||
|
|
|
@ -18,10 +18,7 @@
|
|||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { waitFor } from "@webpack";
|
||||
|
||||
let GuildStore;
|
||||
waitFor(["getGuild"], m => GuildStore = m);
|
||||
import { GuildStore } from "@webpack/common";
|
||||
|
||||
export default definePlugin({
|
||||
name: "ForceOwnerCrown",
|
||||
|
@ -33,7 +30,7 @@ export default definePlugin({
|
|||
find: ".renderOwner=",
|
||||
replacement: {
|
||||
match: /isOwner;return null!=(\w+)?&&/g,
|
||||
replace: "isOwner;if(Vencord.Plugins.plugins.ForceOwnerCrown.isGuildOwner(this.props)){$1=true;}return null!=$1&&"
|
||||
replace: "isOwner;if($self.isGuildOwner(this.props)){$1=true;}return null!=$1&&"
|
||||
}
|
||||
},
|
||||
],
|
||||
|
|
|
@ -35,7 +35,7 @@ interface IgnoredActivity {
|
|||
}
|
||||
|
||||
const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn");
|
||||
const PreviewBadgeClasses = findByPropsLazy("previewBadge", "previewBadgeIcon");
|
||||
const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon");
|
||||
const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight");
|
||||
const RunningGameStore = findByPropsLazy("getRunningGames", "getGamesSeen");
|
||||
|
||||
|
@ -116,7 +116,7 @@ function ToggleActivityComponent({ activity }: { activity: IgnoredActivity; }) {
|
|||
function ToggleActivityComponentWithBackground({ activity }: { activity: IgnoredActivity; }) {
|
||||
return (
|
||||
<div
|
||||
className={`${PreviewBadgeClasses.previewBadge} ${BaseShapeRoundClasses.baseShapeRound}`}
|
||||
className={`${TryItOutClasses.tryItOutBadge} ${BaseShapeRoundClasses.baseShapeRound}`}
|
||||
style={{ padding: "0 2px" }}
|
||||
>
|
||||
<ToggleActivityComponent activity={activity} />
|
||||
|
@ -143,22 +143,22 @@ export default definePlugin({
|
|||
authors: [Devs.Nuckyz],
|
||||
description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.",
|
||||
patches: [{
|
||||
find: ".Messages.SETTINGS_GAMES_OVERLAY_ON",
|
||||
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
|
||||
replacement: {
|
||||
match: /(this.renderLastPlayed\(\)]}\),this.renderOverlayToggle\(\))/,
|
||||
replace: "$1,Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(this.props)"
|
||||
match: /var .=(?<props>.)\.overlay.+?"aria-label":.\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY.+?}}\)/,
|
||||
replace: "$&,$self.renderToggleGameActivityButton($<props>)"
|
||||
}
|
||||
}, {
|
||||
find: ".Messages.NEW,name",
|
||||
find: ".overlayBadge",
|
||||
replacement: {
|
||||
match: /\(\)\.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?<props>.)\.name}\):null/,
|
||||
replace: "$&,Vencord.Plugins.plugins.IgnoreActivities.renderToggleActivityButton($<props>)"
|
||||
match: /.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?<props>.)\.name}\):null/,
|
||||
replace: "$&,$self.renderToggleActivityButton($<props>)"
|
||||
}
|
||||
}, {
|
||||
find: '.displayName="LocalActivityStore"',
|
||||
replacement: {
|
||||
match: /((.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?;)/,
|
||||
replace: "$1$2=$2.filter(Vencord.Plugins.plugins.IgnoreActivities.isActivityEnabled);"
|
||||
match: /(?<activities>.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?\)\)/,
|
||||
replace: "$&;$<activities>=$<activities>.filter($self.isActivityNotIgnored);"
|
||||
}
|
||||
}],
|
||||
|
||||
|
@ -189,12 +189,10 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
|
||||
renderToggleGameActivityButton(props: { game: { id?: string; exePath: string; } | null; }) {
|
||||
if (!props.game) return (null);
|
||||
|
||||
renderToggleGameActivityButton(props: { id?: string; exePath: string; }) {
|
||||
return (
|
||||
<ErrorBoundary noop>
|
||||
<ToggleActivityComponent activity={{ id: props.game.id ?? props.game.exePath, type: ActivitiesTypes.Game }} />
|
||||
<ToggleActivityComponent activity={{ id: props.id ?? props.exePath, type: ActivitiesTypes.Game }} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
},
|
||||
|
@ -207,7 +205,7 @@ export default definePlugin({
|
|||
);
|
||||
},
|
||||
|
||||
isActivityEnabled(props: { type: number; application_id?: string; name?: string; }) {
|
||||
isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) {
|
||||
if (props.type === 0) {
|
||||
if (props.application_id !== undefined) return !ignoredActivitiesCache.has(props.application_id);
|
||||
else {
|
||||
|
|
|
@ -60,7 +60,16 @@ for (const p of pluginsValues) {
|
|||
});
|
||||
}
|
||||
|
||||
for (const p of pluginsValues)
|
||||
for (const p of pluginsValues) {
|
||||
if (p.settings) {
|
||||
p.settings.pluginName = p.name;
|
||||
p.options ??= {};
|
||||
for (const [name, def] of Object.entries(p.settings.def)) {
|
||||
const checks = p.settings.checks?.[name];
|
||||
p.options[name] = { ...def, ...checks };
|
||||
}
|
||||
}
|
||||
|
||||
if (p.patches && isPluginEnabled(p.name)) {
|
||||
for (const patch of p.patches) {
|
||||
patch.plugin = p.name;
|
||||
|
@ -69,6 +78,7 @@ for (const p of pluginsValues)
|
|||
patches.push(patch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins() {
|
||||
for (const name in Plugins)
|
||||
|
|
77
src/plugins/invisibleChat/components/DecryptionModal.tsx
Normal file
77
src/plugins/invisibleChat/components/DecryptionModal.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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,
|
||||
ModalRoot,
|
||||
openModal,
|
||||
} from "@utils/modal";
|
||||
import { Button, Forms, React, TextInput } from "@webpack/common";
|
||||
|
||||
import { decrypt } from "../index";
|
||||
|
||||
export function DecModal(props: any) {
|
||||
const secret: string = props?.message?.content;
|
||||
const [password, setPassword] = React.useState("password");
|
||||
|
||||
return (
|
||||
<ModalRoot {...props}>
|
||||
<ModalHeader>
|
||||
<Forms.FormTitle tag="h4">Decrypt Message</Forms.FormTitle>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Secret</Forms.FormTitle>
|
||||
<TextInput defaultValue={secret} disabled={true}></TextInput>
|
||||
<Forms.FormTitle tag="h5">Password</Forms.FormTitle>
|
||||
<TextInput
|
||||
style={{ marginBottom: "20px" }}
|
||||
onChange={setPassword}
|
||||
/>
|
||||
</ModalContent>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color={Button.Colors.GREEN}
|
||||
onClick={() => {
|
||||
const toSend = decrypt(secret, password, true);
|
||||
if (!toSend || !props?.message) return;
|
||||
// @ts-expect-error
|
||||
Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend);
|
||||
props.onClose();
|
||||
}}>
|
||||
Decrypt
|
||||
</Button>
|
||||
<Button
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.LINK}
|
||||
style={{ left: 15, position: "absolute" }}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
export function buildDecModal(msg: any): any {
|
||||
openModal((props: any) => <DecModal {...props} {...msg} />);
|
||||
}
|
116
src/plugins/invisibleChat/components/EncryptionModal.tsx
Normal file
116
src/plugins/invisibleChat/components/EncryptionModal.tsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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,
|
||||
openModal,
|
||||
} from "@utils/modal";
|
||||
import { findLazy } from "@webpack";
|
||||
import { Button, Forms, React, Switch, TextInput } from "@webpack/common";
|
||||
|
||||
import { encrypt } from "../index";
|
||||
|
||||
const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT);
|
||||
|
||||
function EncModal(props: ModalProps) {
|
||||
const [secret, setSecret] = React.useState("");
|
||||
const [cover, setCover] = React.useState("");
|
||||
const [password, setPassword] = React.useState("password");
|
||||
const [noCover, setNoCover] = React.useState(false);
|
||||
|
||||
const isValid = secret && (noCover || (cover && /\w \w/.test(cover)));
|
||||
|
||||
return (
|
||||
<ModalRoot {...props}>
|
||||
<ModalHeader>
|
||||
<Forms.FormTitle tag="h4">Encrypt Message</Forms.FormTitle>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Secret</Forms.FormTitle>
|
||||
<TextInput
|
||||
onChange={(e: string) => {
|
||||
setSecret(e);
|
||||
}}
|
||||
/>
|
||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Cover (2 or more Words!!)</Forms.FormTitle>
|
||||
<TextInput
|
||||
disabled={noCover}
|
||||
onChange={(e: string) => {
|
||||
setCover(e);
|
||||
}}
|
||||
/>
|
||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Password</Forms.FormTitle>
|
||||
<TextInput
|
||||
style={{ marginBottom: "20px" }}
|
||||
defaultValue={"password"}
|
||||
onChange={(e: string) => {
|
||||
setPassword(e);
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
value={noCover}
|
||||
onChange={(e: boolean) => {
|
||||
setNoCover(e);
|
||||
}}
|
||||
>
|
||||
Don't use a Cover
|
||||
</Switch>
|
||||
</ModalContent>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color={Button.Colors.GREEN}
|
||||
disabled={!isValid}
|
||||
onClick={() => {
|
||||
if (!isValid) return;
|
||||
const encrypted = encrypt(secret, password, noCover ? "d d" : cover);
|
||||
const toSend = noCover ? encrypted.replaceAll("d", "") : encrypted;
|
||||
if (!toSend) return;
|
||||
|
||||
ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", {
|
||||
rawText: `${toSend}`
|
||||
});
|
||||
|
||||
props.onClose();
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
<Button
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
look={Button.Looks.LINK}
|
||||
style={{ left: 15, position: "absolute" }}
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
export function buildEncModal(): any {
|
||||
openModal(props => <EncModal {...props} />);
|
||||
}
|
257
src/plugins/invisibleChat/index.tsx
Normal file
257
src/plugins/invisibleChat/index.tsx
Normal file
|
@ -0,0 +1,257 @@
|
|||
/*
|
||||
* 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 { addButton, removeButton } from "@api/MessagePopover";
|
||||
import { definePluginSettings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getStegCloak } from "@utils/dependencies";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Button, ButtonLooks, ButtonWrapperClasses, ChannelStore, FluxDispatcher, Tooltip } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
import { buildDecModal } from "./components/DecryptionModal";
|
||||
import { buildEncModal } from "./components/EncryptionModal";
|
||||
|
||||
let steggo: any;
|
||||
|
||||
function PopOverIcon() {
|
||||
return (
|
||||
|
||||
<svg
|
||||
fill="var(--header-secondary)"
|
||||
width={24} height={24}
|
||||
viewBox={"0 0 64 64"}
|
||||
>
|
||||
<path d="M 32 9 C 24.832 9 19 14.832 19 22 L 19 27.347656 C 16.670659 28.171862 15 30.388126 15 33 L 15 49 C 15 52.314 17.686 55 21 55 L 43 55 C 46.314 55 49 52.314 49 49 L 49 33 C 49 30.388126 47.329341 28.171862 45 27.347656 L 45 22 C 45 14.832 39.168 9 32 9 z M 32 13 C 36.963 13 41 17.038 41 22 L 41 27 L 23 27 L 23 22 C 23 17.038 27.037 13 32 13 z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function Indicator() {
|
||||
return (
|
||||
<Tooltip text="This message has a hidden message! (InvisibleChat)">
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
<img
|
||||
aria-label="Hidden Message Indicator (InvisibleChat)"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
src="https://github.com/SammCheese/invisible-chat/raw/NewReplugged/src/assets/lock.png"
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ transform: "translateY(4p)", paddingInline: 4 }}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
function ChatBarIcon() {
|
||||
return (
|
||||
<Tooltip text="Encrypt Message">
|
||||
{({ onMouseEnter, onMouseLeave }) => (
|
||||
// size="" = Button.Sizes.NONE
|
||||
/*
|
||||
many themes set "> button" to display: none, as the gift button is
|
||||
the only directly descending button (all the other elements are divs.)
|
||||
Thus, wrap in a div here to avoid getting hidden by that.
|
||||
flex is for some reason necessary as otherwise the button goes flying off
|
||||
*/
|
||||
<div style={{ display: "flex" }}>
|
||||
<Button
|
||||
aria-haspopup="dialog"
|
||||
aria-label="Encrypt Message"
|
||||
size=""
|
||||
look={ButtonLooks.BLANK}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
innerClassName={ButtonWrapperClasses.button}
|
||||
onClick={() => buildEncModal()}
|
||||
style={{ marginRight: "2px" }}
|
||||
>
|
||||
<div className={ButtonWrapperClasses.buttonWrapper}>
|
||||
<svg
|
||||
aria-hidden
|
||||
role="img"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox={"0 0 64 64"}
|
||||
style={{ scale: "1.1" }}
|
||||
>
|
||||
<path fill="currentColor" d="M 32 9 C 24.832 9 19 14.832 19 22 L 19 27.347656 C 16.670659 28.171862 15 30.388126 15 33 L 15 49 C 15 52.314 17.686 55 21 55 L 43 55 C 46.314 55 49 52.314 49 49 L 49 33 C 49 30.388126 47.329341 28.171862 45 27.347656 L 45 22 C 45 14.832 39.168 9 32 9 z M 32 13 C 36.963 13 41 17.038 41 22 L 41 27 L 23 27 L 23 22 C 23 17.038 27.037 13 32 13 z" />
|
||||
</svg>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Tooltip >
|
||||
);
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
savedPasswords: {
|
||||
type: OptionType.STRING,
|
||||
default: "password, Password",
|
||||
description: "Saved Passwords (Seperated with a , )"
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "InvisibleChat",
|
||||
description: "Encrypt your Messages in a non-suspicious way! This plugin makes requests to >>https://embed.sammcheese.net<< to provide embeds to decrypted links!",
|
||||
authors: [Devs.SammCheese],
|
||||
patches: [
|
||||
{
|
||||
// Indicator
|
||||
find: ".Messages.MESSAGE_EDITED,",
|
||||
replacement: {
|
||||
match: /var .,.,.=(.)\.className,.=.\.message,.=.\.children,.=.\.content,.=.\.onUpdate/gm,
|
||||
replace: "try {$1 && $self.INV_REGEX.test($1.content[0]) ? $1.content.push($self.indicator()) : null } catch {};$&"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".activeCommandOption",
|
||||
replacement: {
|
||||
match: /.=.\.activeCommand,.=.\.activeCommandOption,.{1,133}(.)=\[\];/,
|
||||
replace: "$&;$1.push($self.chatBarIcon());",
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
EMBED_API_URL: "https://embed.sammcheese.net",
|
||||
INV_REGEX: new RegExp(/( \u200c|\u200d |[\u2060-\u2064])[^\u200b]/),
|
||||
URL_REGEX: new RegExp(
|
||||
/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/,
|
||||
),
|
||||
settings,
|
||||
async start() {
|
||||
const { default: StegCloak } = await getStegCloak();
|
||||
steggo = new StegCloak(true, false);
|
||||
|
||||
addButton("invDecrypt", message => {
|
||||
return this.INV_REGEX.test(message?.content)
|
||||
? {
|
||||
label: "Decrypt Message",
|
||||
icon: this.popOverIcon,
|
||||
message: message,
|
||||
channel: ChannelStore.getChannel(message.channel_id),
|
||||
onClick: async () => {
|
||||
await iteratePasswords(message).then((res: string | false) => {
|
||||
if (res) return void this.buildEmbed(message, res);
|
||||
return void buildDecModal({ message });
|
||||
});
|
||||
}
|
||||
}
|
||||
: null;
|
||||
});
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeButton("invDecrypt");
|
||||
},
|
||||
|
||||
// Gets the Embed of a Link
|
||||
async getEmbed(url: URL): Promise<Object | {}> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const options: RequestInit = {
|
||||
signal: controller.signal,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
}),
|
||||
};
|
||||
|
||||
// AWS hosted url to discord embed object
|
||||
const rawRes = await fetch(this.EMBED_API_URL, options);
|
||||
clearTimeout(timeout);
|
||||
|
||||
return await rawRes.json();
|
||||
},
|
||||
|
||||
async buildEmbed(message: any, revealed: string): Promise<void> {
|
||||
const urlCheck = revealed.match(this.URL_REGEX);
|
||||
|
||||
message.embeds.push({
|
||||
type: "rich",
|
||||
title: "Decrypted Message",
|
||||
color: "0x45f5f5",
|
||||
description: revealed,
|
||||
footer: {
|
||||
text: "Made with ❤️ by c0dine and Sammy!",
|
||||
},
|
||||
});
|
||||
|
||||
if (urlCheck?.length)
|
||||
message.embeds.push(await this.getEmbed(new URL(urlCheck[0])));
|
||||
|
||||
this.updateMessage(message);
|
||||
},
|
||||
|
||||
updateMessage: (message: any) => {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "MESSAGE_UPDATE",
|
||||
message,
|
||||
});
|
||||
},
|
||||
|
||||
chatBarIcon: ErrorBoundary.wrap(ChatBarIcon, { noop: true }),
|
||||
popOverIcon: () => <PopOverIcon />,
|
||||
indicator: ErrorBoundary.wrap(Indicator, { noop: true })
|
||||
});
|
||||
|
||||
export function encrypt(secret: string, password: string, cover: string): string {
|
||||
return steggo.hide(secret + "\u200b", password, cover);
|
||||
}
|
||||
|
||||
export function decrypt(secret: string, password: string, removeIndicator: boolean): string {
|
||||
const decrypted = steggo.reveal(secret, password);
|
||||
return removeIndicator ? decrypted.replace("\u200b", "") : decrypted;
|
||||
}
|
||||
|
||||
export function isCorrectPassword(result: string): boolean {
|
||||
return result.endsWith("\u200b");
|
||||
}
|
||||
|
||||
export async function iteratePasswords(message: Message): Promise<string | false> {
|
||||
const passwords = settings.store.savedPasswords.split(",").map(s => s.trim());
|
||||
|
||||
if (!message?.content || !passwords?.length) return false;
|
||||
|
||||
let { content } = message;
|
||||
|
||||
// we use an extra variable so we dont have to edit the message content directly
|
||||
if (/^\W/.test(message.content)) content = `d ${message.content}d`;
|
||||
|
||||
for (let i = 0; i < passwords.length; i++) {
|
||||
const result = decrypt(content, passwords[i], false);
|
||||
if (isCorrectPassword(result)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
|
@ -68,7 +68,7 @@ export default definePlugin({
|
|||
find: ".LOADING_DID_YOU_KNOW",
|
||||
replacement: {
|
||||
match: /\._loadingText=.+?random\(.+?;/s,
|
||||
replace: "._loadingText=Vencord.Plugins.plugins.LoadingQuotes.quote;",
|
||||
replace: "._loadingText=$self.quote;",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -56,7 +56,7 @@ function MemberCount() {
|
|||
<div {...props}>
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: "var(--status-green-600)",
|
||||
backgroundColor: "var(--green-360)",
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
borderRadius: "50%",
|
||||
|
@ -64,7 +64,7 @@ function MemberCount() {
|
|||
marginRight: "0.5em"
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: "var(--status-green-600)" }}>{online}</span>
|
||||
<span style={{ color: "var(--green-360)" }}>{online}</span>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
@ -76,13 +76,13 @@ function MemberCount() {
|
|||
width: "6px",
|
||||
height: "6px",
|
||||
borderRadius: "50%",
|
||||
border: "3px solid var(--status-grey-500)",
|
||||
border: "3px solid var(--primary-400)",
|
||||
display: "inline-block",
|
||||
marginRight: "0.5em",
|
||||
marginLeft: "1em"
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: "var(--status-grey-500)" }}>{total}</span>
|
||||
<span style={{ color: "var(--primary-400)" }}>{total}</span>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
@ -99,7 +99,7 @@ export default definePlugin({
|
|||
find: ".isSidebarVisible,",
|
||||
replacement: {
|
||||
match: /(var (.)=.\.className.+?children):\[(.\.useMemo[^}]+"aria-multiselectable")/,
|
||||
replace: "$1:[$2.startsWith('members')?Vencord.Plugins.plugins.MemberCount.render():null,$3"
|
||||
replace: "$1:[$2.startsWith('members')?$self.render():null,$3"
|
||||
}
|
||||
}],
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
* 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
|
||||
|
@ -19,7 +19,7 @@
|
|||
import { addClickListener, removeClickListener } from "@api/MessageEvents";
|
||||
import { migratePluginSettings } from "@api/settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findLazy } from "@webpack";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
|
@ -35,6 +35,19 @@ export default definePlugin({
|
|||
authors: [Devs.Ven],
|
||||
dependencies: ["MessageEventsAPI"],
|
||||
|
||||
options: {
|
||||
enableDeleteOnClick: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable delete on click",
|
||||
default: true
|
||||
},
|
||||
enableDoubleClickToEdit: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable double click to edit",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
|
||||
const PermissionStore = findByPropsLazy("can", "initialize");
|
||||
|
@ -47,11 +60,11 @@ export default definePlugin({
|
|||
this.onClick = addClickListener((msg, chan, event) => {
|
||||
const isMe = msg.author.id === UserStore.getCurrentUser().id;
|
||||
if (!isDeletePressed) {
|
||||
if (isMe && event.detail >= 2 && !EditStore.isEditing(chan.id, msg.id)) {
|
||||
if (Vencord.Settings.plugins.MessageClickActions.enableDoubleClickToEdit && (isMe && event.detail >= 2 && !EditStore.isEditing(chan.id, msg.id))) {
|
||||
MessageActions.startEditMessage(chan.id, msg.id, msg.content);
|
||||
event.preventDefault();
|
||||
}
|
||||
} else if (isMe || PermissionStore.can(Permissions.MANAGE_MESSAGES, chan)) {
|
||||
} else if (Vencord.Settings.plugins.MessageClickActions.enableDeleteOnClick && (isMe || PermissionStore.can(Permissions.MANAGE_MESSAGES, chan))) {
|
||||
MessageActions.deleteMessage(chan.id, msg.id);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ let AutomodEmbed: React.ComponentType<any>,
|
|||
Endpoints: Record<string, any>;
|
||||
|
||||
waitFor(["mle_AutomodEmbed"], m => (AutomodEmbed = m.mle_AutomodEmbed));
|
||||
waitFor(filters.byCode("().inlineMediaEmbed"), m => Embed = m);
|
||||
waitFor(filters.byCode(".inlineMediaEmbed"), m => Embed = m);
|
||||
waitFor(m => m.type?.toString()?.includes('["message","compact","className",'), m => ChannelMessage = m);
|
||||
waitFor(["MESSAGE_CREATE_ATTACHMENT_UPLOAD"], _ => Endpoints = _);
|
||||
const SearchResultClasses = findByPropsLazy("message", "searchResult");
|
||||
|
@ -139,6 +139,16 @@ interface MessageEmbedProps {
|
|||
guildID: string;
|
||||
}
|
||||
|
||||
function withEmbeddedBy(message: Message, embeddedBy: string[]) {
|
||||
return new Proxy(message, {
|
||||
get(_, prop) {
|
||||
if (prop === "vencordEmbeddedBy") return embeddedBy;
|
||||
// @ts-ignore ts so bad
|
||||
return Reflect.get(...arguments);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "MessageLinkEmbeds",
|
||||
description: "Adds a preview to messages that link another message",
|
||||
|
@ -146,14 +156,13 @@ export default definePlugin({
|
|||
dependencies: ["MessageAccessoriesAPI"],
|
||||
patches: [
|
||||
{
|
||||
find: "().embedCard",
|
||||
find: ".embedCard",
|
||||
replacement: [{
|
||||
match: /{"use strict";(.{0,10})\(\)=>(.{1,2})}\);/,
|
||||
replace: '{"use strict";$1()=>$2,me:()=>messageEmbed});'
|
||||
}, {
|
||||
match: /function (.{1,2})\((.{1,2})\){var (.{1,2})=.{1,2}\.message,(.{1,2})=.{1,2}\.channel(.{0,300})\(\)\.embedCard(.{0,500})}\)}/,
|
||||
replace: "function $1($2){var $3=$2.message,$4=$2.channel$5().embedCard$6})}\
|
||||
var messageEmbed={mle_AutomodEmbed:$1};"
|
||||
match: /function (.{1,2})\(.{1,2}\){var .{1,2}=.{1,2}\.message,.{1,2}=.{1,2}\.channel.{0,300}\.embedCard.{0,500}}\)}/,
|
||||
replace: "$&;var messageEmbed={mle_AutomodEmbed:$1};"
|
||||
}]
|
||||
}
|
||||
],
|
||||
|
@ -195,19 +204,24 @@ var messageEmbed={mle_AutomodEmbed:$1};"
|
|||
|
||||
messageEmbedAccessory(props) {
|
||||
const { message }: { message: Message; } = props;
|
||||
// @ts-ignore
|
||||
const embeddedBy: string[] = message.vencordEmbeddedBy ?? [];
|
||||
|
||||
const accessories = [] as (JSX.Element | null)[];
|
||||
|
||||
let match = null as RegExpMatchArray | null;
|
||||
while ((match = this.messageLinkRegex.exec(message.content!)) !== null) {
|
||||
const [_, guildID, channelID, messageID] = match;
|
||||
if (embeddedBy.includes(messageID)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const linkedChannel = ChannelStore.getChannel(channelID);
|
||||
if (!linkedChannel || (guildID !== "@me" && !PermissionStore.can(1024n /* view channel */, linkedChannel))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let linkedMessage = messageCache[messageID]?.message as Message;
|
||||
let linkedMessage = messageCache[messageID]?.message;
|
||||
if (!linkedMessage) {
|
||||
linkedMessage ??= MessageStore.getMessage(channelID, messageID);
|
||||
if (linkedMessage) messageCache[messageID] = { message: linkedMessage, fetched: true };
|
||||
|
@ -224,7 +238,7 @@ var messageEmbed={mle_AutomodEmbed:$1};"
|
|||
}
|
||||
}
|
||||
const messageProps: MessageEmbedProps = {
|
||||
message: linkedMessage,
|
||||
message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]),
|
||||
channel: linkedChannel,
|
||||
guildID
|
||||
};
|
||||
|
|
3
src/plugins/messageLogger/deleteStyleOverlay.css
Normal file
3
src/plugins/messageLogger/deleteStyleOverlay.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.messagelogger-deleted {
|
||||
background-color: rgba(240 71 71 / 15%);
|
||||
}
|
3
src/plugins/messageLogger/deleteStyleText.css
Normal file
3
src/plugins/messageLogger/deleteStyleText.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.messagelogger-deleted div {
|
||||
color: #f04747;
|
||||
}
|
|
@ -19,20 +19,23 @@
|
|||
import "./messageLogger.css";
|
||||
|
||||
import { Settings } from "@api/settings";
|
||||
import { disableStyle, enableStyle } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import Logger from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Parser, UserStore } from "@webpack/common";
|
||||
import { moment, Parser, Timestamp, UserStore } from "@webpack/common";
|
||||
|
||||
function addDeleteStyleClass() {
|
||||
import overlayStyle from "./deleteStyleOverlay.css?managed";
|
||||
import textStyle from "./deleteStyleText.css?managed";
|
||||
|
||||
function addDeleteStyle() {
|
||||
if (Settings.plugins.MessageLogger.deleteStyle === "text") {
|
||||
document.body.classList.remove("messagelogger-red-overlay");
|
||||
document.body.classList.add("messagelogger-red-text");
|
||||
enableStyle(textStyle);
|
||||
disableStyle(overlayStyle);
|
||||
} else {
|
||||
document.body.classList.remove("messagelogger-red-text");
|
||||
document.body.classList.add("messagelogger-red-overlay");
|
||||
disableStyle(textStyle);
|
||||
enableStyle(overlayStyle);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,28 +44,21 @@ export default definePlugin({
|
|||
description: "Temporarily logs deleted and edited messages.",
|
||||
authors: [Devs.rushii, Devs.Ven],
|
||||
|
||||
timestampModule: null as any,
|
||||
moment: null as Function | null,
|
||||
|
||||
start() {
|
||||
this.moment = findByPropsLazy("relativeTimeRounding", "relativeTimeThreshold");
|
||||
this.timestampModule = findByPropsLazy("messageLogger_TimestampComponent");
|
||||
|
||||
addDeleteStyleClass();
|
||||
addDeleteStyle();
|
||||
},
|
||||
|
||||
stop() {
|
||||
document.querySelectorAll(".messageLogger-deleted").forEach(e => e.remove());
|
||||
document.querySelectorAll(".messageLogger-edited").forEach(e => e.remove());
|
||||
document.querySelectorAll(".messagelogger-deleted").forEach(e => e.remove());
|
||||
document.querySelectorAll(".messagelogger-edited").forEach(e => e.remove());
|
||||
document.body.classList.remove("messagelogger-red-overlay");
|
||||
document.body.classList.remove("messagelogger-red-text");
|
||||
},
|
||||
|
||||
renderEdit(edit: { timestamp: any, content: string; }) {
|
||||
const Timestamp = this.timestampModule.messageLogger_TimestampComponent;
|
||||
return (
|
||||
<ErrorBoundary noop>
|
||||
<div className="messageLogger-edited">
|
||||
<div className="messagelogger-edited">
|
||||
{Parser.parse(edit.content)}
|
||||
<Timestamp
|
||||
timestamp={edit.timestamp}
|
||||
|
@ -78,7 +74,7 @@ export default definePlugin({
|
|||
|
||||
makeEdit(newMessage: any, oldMessage: any): any {
|
||||
return {
|
||||
timestamp: this.moment?.call(newMessage.edited_timestamp),
|
||||
timestamp: moment?.call(newMessage.edited_timestamp),
|
||||
content: oldMessage.content
|
||||
};
|
||||
},
|
||||
|
@ -92,7 +88,7 @@ export default definePlugin({
|
|||
{ label: "Red text", value: "text", default: true },
|
||||
{ label: "Red overlay", value: "overlay" }
|
||||
],
|
||||
onChange: () => addDeleteStyleClass()
|
||||
onChange: () => addDeleteStyle()
|
||||
},
|
||||
ignoreBots: {
|
||||
type: OptionType.BOOLEAN,
|
||||
|
@ -155,7 +151,7 @@ export default definePlugin({
|
|||
replace:
|
||||
"MESSAGE_DELETE:function($1){" +
|
||||
" var cache = $2getOrCreate($1.channelId);" +
|
||||
" cache = Vencord.Plugins.plugins.MessageLogger.handleDelete(cache, $1, false);" +
|
||||
" cache = $self.handleDelete(cache, $1, false);" +
|
||||
" $2commit(cache);" +
|
||||
"},"
|
||||
},
|
||||
|
@ -165,7 +161,7 @@ export default definePlugin({
|
|||
replace:
|
||||
"MESSAGE_DELETE_BULK:function($1){" +
|
||||
" var cache = $2getOrCreate($1.channelId);" +
|
||||
" cache = Vencord.Plugins.plugins.MessageLogger.handleDelete(cache, $1, true);" +
|
||||
" cache = $self.handleDelete(cache, $1, true);" +
|
||||
" $2commit(cache);" +
|
||||
"},"
|
||||
},
|
||||
|
@ -175,7 +171,7 @@ export default definePlugin({
|
|||
replace: "$1" +
|
||||
".update($3,m =>" +
|
||||
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
|
||||
" m.set('editHistory',[...(m.editHistory || []), Vencord.Plugins.plugins.MessageLogger.makeEdit($2.message, m)]) :" +
|
||||
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
|
||||
" m" +
|
||||
")" +
|
||||
".update($3"
|
||||
|
@ -259,8 +255,8 @@ export default definePlugin({
|
|||
replace: "$1,deleted=$2.attachment?.deleted,"
|
||||
},
|
||||
{
|
||||
match: /(hiddenSpoilers:\w,className:)/,
|
||||
replace: "$1 (deleted ? 'messageLogger-deleted-attachment ' : '') +"
|
||||
match: /\["className","attachment","inlineMedia".+?className:/,
|
||||
replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -276,9 +272,9 @@ export default definePlugin({
|
|||
replace: "var $1=$2.id,deleted=$2.message.deleted,"
|
||||
},
|
||||
{
|
||||
// Append messageLogger-deleted to classNames if deleted
|
||||
// Append messagelogger-deleted to classNames if deleted
|
||||
match: /\)\("li",\{(.+?),className:/,
|
||||
replace: ")(\"li\",{$1,className:(deleted ? \"messageLogger-deleted \" : \"\")+"
|
||||
replace: ")(\"li\",{$1,className:(deleted ? \"messagelogger-deleted \" : \"\")+"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -291,7 +287,7 @@ export default definePlugin({
|
|||
{
|
||||
// Render editHistory in the deepest div for message content
|
||||
match: /(\)\("div",\{id:.+?children:\[)/,
|
||||
replace: "$1 (arguments[0].message.editHistory.length > 0 ? arguments[0].message.editHistory.map(edit => Vencord.Plugins.plugins.MessageLogger.renderEdit(edit)) : null), "
|
||||
replace: "$1 (arguments[0].message.editHistory.length > 0 ? arguments[0].message.editHistory.map(edit => $self.renderEdit(edit)) : null), "
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -312,17 +308,6 @@ export default definePlugin({
|
|||
]
|
||||
},
|
||||
|
||||
{
|
||||
// Message "(edited)" timestamp component
|
||||
// Module 23552
|
||||
find: "Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format",
|
||||
replacement: {
|
||||
// Re-export the timestamp component under a findable name
|
||||
match: /{(\w{1,2}:\(\)=>(\w{1,2}))}/,
|
||||
replace: "{$1,messageLogger_TimestampComponent:()=>$2}"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
// Message context base menu
|
||||
// Module 600300
|
||||
|
|
|
@ -1,27 +1,20 @@
|
|||
.messagelogger-red-overlay .messageLogger-deleted {
|
||||
background-color: rgba(240, 71, 71, 0.15);
|
||||
}
|
||||
.messagelogger-red-text .messageLogger-deleted div {
|
||||
color: #f04747;
|
||||
}
|
||||
|
||||
.messageLogger-deleted [class^="buttons"] {
|
||||
.messagelogger-deleted [class^="buttons"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.messageLogger-deleted-attachment {
|
||||
.messagelogger-deleted-attachment {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.messageLogger-deleted-attachment:hover {
|
||||
.messagelogger-deleted-attachment:hover {
|
||||
filter: grayscale(0);
|
||||
transition: 250ms filter linear;
|
||||
}
|
||||
|
||||
.theme-dark .messageLogger-edited {
|
||||
.theme-dark .messagelogger-edited {
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
.theme-light .messageLogger-edited {
|
||||
.theme-light .messagelogger-edited {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ export default definePlugin({
|
|||
replacement: [
|
||||
{
|
||||
match: /(?<=MESSAGE_CREATE:function\((\w)\){var \w=\w\.channelId,\w=\w\.message,\w=\w\.isPushNotification,\w=\w\.\w\.getOrCreate\(\w\));/,
|
||||
replace: ";if(Vencord.Plugins.plugins.NoBlockedMessages.isBlocked(n))return;"
|
||||
replace: ";if($self.isBlocked(n))return;"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ export default definePlugin({
|
|||
replacement: {
|
||||
match: /CREATE_PENDING_REPLY:function\((.{1,2})\){/,
|
||||
replace:
|
||||
"CREATE_PENDING_REPLY:function($1){$1.shouldMention=Vencord.Plugins.plugins.NoReplyMention.shouldMention($1);",
|
||||
"CREATE_PENDING_REPLY:function($1){$1.shouldMention=$self.shouldMention($1);",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -26,7 +26,7 @@ export default definePlugin({
|
|||
authors: [Devs.Ven, Devs.adryd],
|
||||
|
||||
start() {
|
||||
fetch("https://raw.githubusercontent.com/adryd325/oneko.js/14bab15a755d0e35cd4ae19c931d96d306f99f42/oneko.js")
|
||||
fetch("https://raw.githubusercontent.com/adryd325/oneko.js/5977144dce83e4d71af1de005d16e38eebeb7b72/oneko.js")
|
||||
.then(x => x.text())
|
||||
.then(s => s.replace("./oneko.gif", "https://raw.githubusercontent.com/adryd325/oneko.js/14bab15a755d0e35cd4ae19c931d96d306f99f42/oneko.gif"))
|
||||
.then(eval);
|
||||
|
|
|
@ -175,8 +175,8 @@ export default definePlugin({
|
|||
gif.finish();
|
||||
const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" });
|
||||
// Immediately after the command finishes, Discord clears all input, including pending attachments.
|
||||
// Thus, setImmediate is needed to make this execute after Discord cleared the input
|
||||
setImmediate(() => promptToUpload([file], cmdCtx.channel, DRAFT_TYPE));
|
||||
// Thus, setTimeout is needed to make this execute after Discord cleared the input
|
||||
setTimeout(() => promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10);
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
|
@ -24,7 +24,7 @@ export default definePlugin({
|
|||
description: "Doesn't show the small guild icons in folders",
|
||||
authors: [Devs.botato],
|
||||
patches: [{
|
||||
find: "().expandedFolderIconWrapper",
|
||||
find: ".expandedFolderIconWrapper",
|
||||
replacement: [{
|
||||
match: /\(\w\|\|\w\)&&(\(.{0,40}\(.{1,3}\.animated)/,
|
||||
replace: "$1",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue