Compare commits
2 Commits
devbuild
...
features/c
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c3da99eeee | ||
![]() |
0e7bd87cee |
@ -2,26 +2,7 @@
|
|||||||
"root": true,
|
"root": true,
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"ignorePatterns": ["dist", "browser"],
|
"ignorePatterns": ["dist", "browser"],
|
||||||
"plugins": [
|
"plugins": ["header", "simple-import-sort", "unused-imports"],
|
||||||
"@typescript-eslint",
|
|
||||||
"header",
|
|
||||||
"simple-import-sort",
|
|
||||||
"unused-imports",
|
|
||||||
"path-alias"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"import/resolver": {
|
|
||||||
"alias": {
|
|
||||||
"map": [
|
|
||||||
["@webpack", "./src/webpack"],
|
|
||||||
["@webpack/common", "./src/webpack/common"],
|
|
||||||
["@utils", "./src/utils"],
|
|
||||||
["@api", "./src/api"],
|
|
||||||
["@components", "./src/components"]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rules": {
|
"rules": {
|
||||||
// Since it's only been a month and Vencord has already been stolen
|
// Since it's only been a month and Vencord has already been stolen
|
||||||
// by random skids who rebranded it to "AlphaCord" and erased all license
|
// by random skids who rebranded it to "AlphaCord" and erased all license
|
||||||
@ -107,8 +88,6 @@
|
|||||||
"simple-import-sort/imports": "error",
|
"simple-import-sort/imports": "error",
|
||||||
"simple-import-sort/exports": "error",
|
"simple-import-sort/exports": "error",
|
||||||
|
|
||||||
"unused-imports/no-unused-imports": "error",
|
"unused-imports/no-unused-imports": "error"
|
||||||
|
|
||||||
"path-alias/no-relative": "error"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
.github/FUNDING.yml
vendored
13
.github/FUNDING.yml
vendored
@ -1,13 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: Vendicated
|
|
||||||
patreon: Aliucord
|
|
||||||
open_collective: # Replace with a single Open Collective username
|
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
otechie: # Replace with a single Otechie username
|
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
58
.github/workflows/build.yml
vendored
58
.github/workflows/build.yml
vendored
@ -1,15 +1,8 @@
|
|||||||
name: Build DevBuild
|
name: Build latest
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
|
||||||
- .github/workflows/build.yml
|
|
||||||
- src/**
|
|
||||||
- browser/**
|
|
||||||
- scripts/build/**
|
|
||||||
- package.json
|
|
||||||
- pnpm-lock.yaml
|
|
||||||
env:
|
env:
|
||||||
FORCE_COLOR: true
|
FORCE_COLOR: true
|
||||||
|
|
||||||
@ -22,43 +15,42 @@ jobs:
|
|||||||
|
|
||||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
- name: Use Node.js 19
|
- name: Use Node.js 18
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 19
|
node-version: 18
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build web
|
- name: Build web
|
||||||
run: pnpm buildWeb --standalone
|
run: pnpm buildWeb
|
||||||
|
|
||||||
- name: 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
|
- name: Build
|
||||||
run: pnpm build --standalone
|
run: pnpm build --standalone
|
||||||
|
|
||||||
- name: Rename extensions for more user friendliness
|
|
||||||
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
|
|
||||||
|
|
||||||
- name: Get some values needed for the release
|
- name: Get some values needed for the release
|
||||||
id: release_values
|
id: vars
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||||
|
|
||||||
- name: Upload Devbuild
|
- uses: dev-drprasad/delete-tag-and-release@085c6969f18bad0de1b9f3fe6692a3cd01f64fe5 # v0.2.0
|
||||||
run: |
|
with:
|
||||||
gh release upload devbuild --clobber dist/*
|
delete_release: true
|
||||||
gh release edit devbuild --title "DevBuild $RELEASE_TAG"
|
tag_name: devbuild
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
RELEASE_TAG: ${{ env.release_tag }}
|
|
||||||
|
- name: Create the release
|
||||||
|
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: devbuild
|
||||||
|
name: Dev Build ${{ steps.vars.outputs.sha_short }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
files: |
|
||||||
|
dist/*
|
||||||
|
43
.github/workflows/reportBrokenPlugins.yml
vendored
43
.github/workflows/reportBrokenPlugins.yml
vendored
@ -1,43 +0,0 @@
|
|||||||
name: Test Patches
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
# Every day at midnight
|
|
||||||
- cron: 0 0 * * *
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
TestPlugins:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
|
||||||
|
|
||||||
- name: Use Node.js 19
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 19
|
|
||||||
cache: "pnpm"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
pnpm add puppeteer
|
|
||||||
|
|
||||||
sudo apt-get install -y chromium-browser
|
|
||||||
|
|
||||||
- name: Build web
|
|
||||||
run: pnpm buildWeb --standalone
|
|
||||||
|
|
||||||
- name: Create Report
|
|
||||||
timeout-minutes: 10
|
|
||||||
run: |
|
|
||||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
|
||||||
export CHROMIUM_BIN=$(which chromium-browser)
|
|
||||||
|
|
||||||
esbuild test/generateReport.ts > dist/report.mjs
|
|
||||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
|
||||||
env:
|
|
||||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
|
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@ -23,8 +23,5 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Lint & Test if desktop version compiles
|
- name: Lint & Test if it compiles
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|
||||||
- name: Lint & Test if web version compiles
|
|
||||||
run: pnpm testWeb
|
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -18,6 +18,3 @@ lerna-debug.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
src/userplugins
|
src/userplugins
|
||||||
|
|
||||||
ExtensionCache/
|
|
||||||
settings/
|
|
||||||
|
10
.vscode/extensions.json
vendored
10
.vscode/extensions.json
vendored
@ -1,11 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [ "EditorConfig.EditorConfig" ]
|
||||||
"EditorConfig.EditorConfig",
|
|
||||||
"pmneo.tsimporter",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"formulahendry.auto-rename-tag",
|
|
||||||
"GregorBiswanger.json2ts",
|
|
||||||
"eamodio.gitlens",
|
|
||||||
"kamikillerto.vscode-colorize"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
37
.vscode/launch.json
vendored
37
.vscode/launch.json
vendored
@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
// this allows you to debug Vencord from VSCode.
|
|
||||||
// How to use:
|
|
||||||
// You need to run Discord via the command line to pass some flags to it.
|
|
||||||
// If you want to debug the main (node.js) process (preload.ts, ipcMain/*, patcher.ts),
|
|
||||||
// add the --inspect flag
|
|
||||||
// To debug the renderer (99% of Vencord), add the --remote-debugging-port=9223 flag
|
|
||||||
//
|
|
||||||
// Now launch the desired configuration in VSCode and start Discord with the flags.
|
|
||||||
// For example, to debug both process, run Electron: All then launch Discord with
|
|
||||||
// discord --remote-debugging-port=9223 --inspect
|
|
||||||
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Electron: Main",
|
|
||||||
"type": "node",
|
|
||||||
"request": "attach",
|
|
||||||
"port": 9229,
|
|
||||||
"timeout": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Electron: Renderer",
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "attach",
|
|
||||||
"port": 9223,
|
|
||||||
"timeout": 30000,
|
|
||||||
"webRoot": "${workspaceFolder}/src"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"compounds": [
|
|
||||||
{
|
|
||||||
"name": "Electron: All",
|
|
||||||
"configurations": ["Electron: Main", "Electron: Renderer"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
27
README.md
27
README.md
@ -4,26 +4,21 @@ A Discord client mod that does things differently
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Super easy to install, no git or node or anything else required
|
- Works on Discord's latest update that breaks all other mods
|
||||||
- Many plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
- Browser Support (experimental): Run Vencord in your Browser instead of the desktop app
|
||||||
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, custom slash commands, ShowHiddenChannels
|
- Custom Css and Themes: Manually edit `%appdata%/Vencord/settings/quickCss.css` / `~/.config/Vencord/settings/quickCss.css` with your favourite editor and the client will automatically apply your changes. To import BetterDiscord themes, just add `@import url(theUrl)` on the top of this file. (Make sure the url is a github raw URL or similar and only contains plain text, and NOT a nice looking website)
|
||||||
- Browser Support: Run Vencord in your Browser via extension or UserScript
|
- Many Useful™ plugins - [List](https://github.com/Vendicated/Vencord/tree/main/src/plugins)
|
||||||
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
- Experiments
|
||||||
- Works in all Electron versions (Confirmed working on versions 13-23)
|
- Proper context isolation -> Works in newer Electron versions (Confirmed working on versions 13-22)
|
||||||
|
- Inline patches: Patch Discord's code with regex replacements! See [the experiments plugin](src/plugins/experiments.ts) for an example. While being more complex, this is more powerful than monkey patching since you can patch only small parts of functions instead of fully replacing them, access non exported/local variables and even replace constants (like in the aforementioned experiments patch!)
|
||||||
|
|
||||||
## Installing / Uninstalling
|
## Installing / Uninstalling
|
||||||
|
|
||||||
If you're just a normal user, use [our simple gui installer!](https://github.com/Vendicated/VencordInstaller#usage)
|
Read [Megu's Installation Guide!](docs/1_INSTALLING.md)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
## Installing on Browser
|
## Installing on Browser
|
||||||
|
|
||||||
Install the browser extension for [](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip), [](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Firefox.xpi) or [UserScript](https://github.com/Vendicated/Vencord/releases/download/devbuild/Vencord.user.js). Please note that they aren't automatically updated for now, so you will regularely have to reinstall it.
|
Run the same commands as in the regular install method. Now run
|
||||||
|
|
||||||
|
|
||||||
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
|
```sh
|
||||||
pnpm buildWeb
|
pnpm buildWeb
|
||||||
@ -33,11 +28,7 @@ You will find the built extension at dist/extension.zip. Now just install this e
|
|||||||
|
|
||||||
## Installing Plugins
|
## 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!
|
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.
|
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!
|
Don't forget to rebuild!
|
||||||
|
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as DataStore from "../src/api/DataStore";
|
|
||||||
import IpcEvents from "../src/utils/IpcEvents";
|
import IpcEvents from "../src/utils/IpcEvents";
|
||||||
|
import * as DataStore from "../src/api/DataStore";
|
||||||
|
|
||||||
// Discord deletes this so need to store in variable
|
// Discord deletes this so need to store in variable
|
||||||
const { localStorage } = window;
|
const { localStorage } = window;
|
||||||
|
@ -1,48 +1,24 @@
|
|||||||
/*
|
if (typeof browser === "undefined") {
|
||||||
* Vencord, a modification for Discord's desktop app
|
var browser = chrome;
|
||||||
* 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) {
|
browser.webRequest.onHeadersReceived.addListener(({ responseHeaders, url }) => {
|
||||||
if (details.type === "stylesheet") {
|
const cspIdx = responseHeaders.findIndex(h => h.name === "content-security-policy");
|
||||||
details.responseHeaders = details.responseHeaders.filter(it => it.name.toLowerCase() !== 'content-type');
|
if (cspIdx !== -1)
|
||||||
details.responseHeaders.push({ name: "Content-Type", value: "text/css" });
|
responseHeaders.splice(cspIdx, 1);
|
||||||
|
|
||||||
|
if (url.endsWith(".css")) {
|
||||||
|
const contentType = responseHeaders.find(h => h.name === "content-type");
|
||||||
|
if (contentType)
|
||||||
|
contentType.value = "text/css";
|
||||||
|
else
|
||||||
|
responseHeaders.push({
|
||||||
|
name: "content-type",
|
||||||
|
value: "text/json"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return { responseHeaders: details.responseHeaders };
|
|
||||||
}
|
|
||||||
|
|
||||||
var cspHeaders = [
|
|
||||||
"content-security-policy",
|
|
||||||
"content-security-policy-report-only",
|
|
||||||
];
|
|
||||||
|
|
||||||
function removeCSPHeaders(details) {
|
|
||||||
return {
|
return {
|
||||||
responseHeaders: details.responseHeaders.filter(header =>
|
responseHeaders
|
||||||
!cspHeaders.includes(header.name.toLowerCase()))
|
|
||||||
};
|
};
|
||||||
}
|
}, { urls: ["*://*.discord.com/*"] }, ["blocking", "responseHeaders"]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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"]
|
|
||||||
);
|
|
||||||
|
@ -1,25 +1,32 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Vencord Web",
|
"name": "Vencord Web",
|
||||||
"description": "The Vencord Client Mod for Discord Web.",
|
"description": "Yeee",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "Vendicated",
|
"author": "Vendicated",
|
||||||
"homepage_url": "https://github.com/Vendicated/Vencord",
|
"homepage_url": "https://github.com/Vendicated/Vencord",
|
||||||
"permissions": [
|
"background": {
|
||||||
"webRequest",
|
"scripts": [
|
||||||
"webRequestBlocking",
|
"background.js"
|
||||||
"*://*.discord.com/*",
|
]
|
||||||
"https://raw.githubusercontent.com/*"
|
},
|
||||||
],
|
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"matches": ["*://*.discord.com/*"],
|
"matches": [
|
||||||
"js": ["content.js"]
|
"*://*.discord.com/*"
|
||||||
|
],
|
||||||
|
"js": [
|
||||||
|
"content.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"web_accessible_resources": ["dist/Vencord.js"],
|
"permissions": [
|
||||||
"background": {
|
"*://*.discord.com/*",
|
||||||
"scripts": ["background.js"]
|
"webRequest",
|
||||||
}
|
"webRequestBlocking"
|
||||||
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
"dist/Vencord.js"
|
||||||
|
]
|
||||||
}
|
}
|
@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 3,
|
|
||||||
"name": "Vencord Web",
|
|
||||||
"description": "Yeee",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Vendicated",
|
|
||||||
"homepage_url": "https://github.com/Vendicated/Vencord",
|
|
||||||
|
|
||||||
"host_permissions": [
|
|
||||||
"*://*.discord.com/*",
|
|
||||||
"https://raw.githubusercontent.com/*"
|
|
||||||
],
|
|
||||||
|
|
||||||
"permissions": ["declarativeNetRequest"],
|
|
||||||
|
|
||||||
"content_scripts": [
|
|
||||||
{
|
|
||||||
"run_at": "document_start",
|
|
||||||
"matches": ["*://*.discord.com/*"],
|
|
||||||
"js": ["content.js"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"web_accessible_resources": [
|
|
||||||
{
|
|
||||||
"resources": ["dist/Vencord.js"],
|
|
||||||
"matches": ["*://*.discord.com/*"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"declarative_net_request": {
|
|
||||||
"rule_resources": [
|
|
||||||
{
|
|
||||||
"id": "modifyResponseHeaders",
|
|
||||||
"enabled": true,
|
|
||||||
"path": "modifyResponseHeaders.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"action": {
|
|
||||||
"type": "modifyHeaders",
|
|
||||||
"responseHeaders": [
|
|
||||||
{
|
|
||||||
"header": "content-security-policy",
|
|
||||||
"operation": "remove"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"header": "content-security-policy-report-only",
|
|
||||||
"operation": "remove"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"condition": {
|
|
||||||
"resourceTypes": ["main_frame"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"action": {
|
|
||||||
"type": "modifyHeaders",
|
|
||||||
"responseHeaders": [
|
|
||||||
{
|
|
||||||
"header": "content-type",
|
|
||||||
"operation": "set",
|
|
||||||
"value": "text/css"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"condition": {
|
|
||||||
"resourceTypes": ["stylesheet"],
|
|
||||||
"urlFilter": "https://raw.githubusercontent.com/*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,6 +1,3 @@
|
|||||||
> **Warning**
|
|
||||||
> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead.
|
|
||||||
|
|
||||||
# Installation Guide
|
# Installation Guide
|
||||||
|
|
||||||
Welcome to Megu's Installation Guide! In this file, you will learn about how to download, install, and uninstall Vencord!
|
Welcome to Megu's Installation Guide! In this file, you will learn about how to download, install, and uninstall Vencord!
|
||||||
|
@ -15,7 +15,7 @@ You don't need to run `pnpm build` every time you make a change. Instead, use `p
|
|||||||
3. In `index.ts`, copy-paste the following template code:
|
3. In `index.ts`, copy-paste the following template code:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "../../utils/types";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Epic Plugin",
|
name: "Epic Plugin",
|
||||||
|
44
package.json
44
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.0.1",
|
"version": "1.0.0",
|
||||||
"description": "A Discord client mod that does things differently",
|
"description": "A Discord client mod that does things differently",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
@ -24,52 +24,28 @@
|
|||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
"lint:fix": "pnpm lint --fix",
|
"lint:fix": "pnpm lint --fix",
|
||||||
"test": "pnpm lint && pnpm build && pnpm testTsc",
|
"test": "pnpm lint && pnpm build && pnpm testTsc",
|
||||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
|
||||||
"testTsc": "tsc --noEmit",
|
"testTsc": "tsc --noEmit",
|
||||||
"uninject": "node scripts/patcher/uninstall.js",
|
"uninject": "node scripts/patcher/uninstall.js",
|
||||||
"watch": "node scripts/build/build.mjs --watch"
|
"watch": "node scripts/build/build.mjs --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"console-menu": "^0.1.0",
|
||||||
"fflate": "^0.7.4"
|
"fflate": "^0.7.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/diff": "^5.0.2",
|
"@types/node": "^18.7.13",
|
||||||
"@types/node": "^18.11.9",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react": "^18.0.25",
|
|
||||||
"@types/react-dom": "^18.0.9",
|
|
||||||
"@types/yazl": "^2.4.2",
|
"@types/yazl": "^2.4.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
"@typescript-eslint/parser": "^5.39.0",
|
||||||
"@typescript-eslint/parser": "^5.44.0",
|
|
||||||
"@vap/core": "0.0.12",
|
|
||||||
"@vap/shiki": "0.10.3",
|
|
||||||
"console-menu": "^0.1.0",
|
|
||||||
"diff": "^5.1.0",
|
|
||||||
"discord-types": "^1.3.26",
|
"discord-types": "^1.3.26",
|
||||||
"esbuild": "^0.15.16",
|
"esbuild": "^0.15.5",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.24.0",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
|
||||||
"eslint-plugin-header": "^3.1.1",
|
"eslint-plugin-header": "^3.1.1",
|
||||||
"eslint-plugin-path-alias": "^1.0.0",
|
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"highlight.js": "10.6.0",
|
|
||||||
"moment": "^2.29.4",
|
|
||||||
"puppeteer-core": "^19.3.0",
|
|
||||||
"standalone-electron-types": "^1.0.0",
|
"standalone-electron-types": "^1.0.0",
|
||||||
"type-fest": "^3.3.0",
|
"type-fest": "^3.1.0",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.8.4"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@7.13.4",
|
"packageManager": "pnpm@7.13.4"
|
||||||
"pnpm": {
|
|
||||||
"patchedDependencies": {
|
|
||||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"webExt": {
|
|
||||||
"artifactsDir": "./dist",
|
|
||||||
"build": {
|
|
||||||
"overwriteDest": true
|
|
||||||
},
|
|
||||||
"sourceDir": "./dist/extension-v2-unpacked"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
diff --git a/lib/rules/no-relative.js b/lib/rules/no-relative.js
|
|
||||||
index 71594c83f1f4f733ffcc6047d7f7084348335dbe..d8623d87c89499c442171db3272cba07c9efabbe 100644
|
|
||||||
--- a/lib/rules/no-relative.js
|
|
||||||
+++ b/lib/rules/no-relative.js
|
|
||||||
@@ -41,7 +41,7 @@ module.exports = {
|
|
||||||
ImportDeclaration(node) {
|
|
||||||
const importPath = node.source.value;
|
|
||||||
|
|
||||||
- if (!/^(\.?\.\/)/.test(importPath)) {
|
|
||||||
+ if (!/^(\.\.\/)/.test(importPath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
1238
pnpm-lock.yaml
generated
1238
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -19,17 +19,22 @@
|
|||||||
|
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
|
|
||||||
import { commonOpts, globPlugins, isStandalone, watch } from "./common.mjs";
|
import { commonOpts, gitHash, globPlugins, isStandalone } from "./common.mjs";
|
||||||
|
|
||||||
const defines = {
|
const defines = {
|
||||||
IS_STANDALONE: isStandalone,
|
IS_STANDALONE: isStandalone
|
||||||
IS_DEV: JSON.stringify(watch)
|
|
||||||
};
|
};
|
||||||
if (defines.IS_STANDALONE === "false")
|
if (defines.IS_STANDALONE === "false")
|
||||||
// If this is a local build (not standalone), optimise
|
// If this is a local build (not standalone), optimise
|
||||||
// for the specific platform we're on
|
// for the specific platform we're on
|
||||||
defines["process.platform"] = JSON.stringify(process.platform);
|
defines["process.platform"] = JSON.stringify(process.platform);
|
||||||
|
|
||||||
|
const header = `
|
||||||
|
// Vencord ${gitHash}
|
||||||
|
// Standalone: ${defines.IS_STANDALONE}
|
||||||
|
// Platform: ${defines["process.platform"] || "Universal"}
|
||||||
|
`.trim();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.BuildOptions}
|
* @type {esbuild.BuildOptions}
|
||||||
*/
|
*/
|
||||||
@ -42,25 +47,25 @@ const nodeCommonOpts = {
|
|||||||
bundle: true,
|
bundle: true,
|
||||||
external: ["electron", ...commonOpts.external],
|
external: ["electron", ...commonOpts.external],
|
||||||
define: defines,
|
define: defines,
|
||||||
|
banner: {
|
||||||
|
js: header
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
|
|
||||||
const sourcemap = watch ? "inline" : "external";
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...nodeCommonOpts,
|
...nodeCommonOpts,
|
||||||
entryPoints: ["src/preload.ts"],
|
entryPoints: ["src/preload.ts"],
|
||||||
outfile: "dist/preload.js",
|
outfile: "dist/preload.js",
|
||||||
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
|
footer: { js: "//# sourceURL=VencordPreload\n//# sourceMappingURL=vencord://preload.js.map" },
|
||||||
sourcemap,
|
sourcemap: "external",
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...nodeCommonOpts,
|
...nodeCommonOpts,
|
||||||
entryPoints: ["src/patcher.ts"],
|
entryPoints: ["src/patcher.ts"],
|
||||||
outfile: "dist/patcher.js",
|
outfile: "dist/patcher.js",
|
||||||
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
|
footer: { js: "//# sourceURL=VencordPatcher\n//# sourceMappingURL=vencord://patcher.js.map" },
|
||||||
sourcemap,
|
sourcemap: "external",
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOpts,
|
...commonOpts,
|
||||||
@ -68,16 +73,16 @@ await Promise.all([
|
|||||||
outfile: "dist/renderer.js",
|
outfile: "dist/renderer.js",
|
||||||
format: "iife",
|
format: "iife",
|
||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
footer: { js: "//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
|
footer: { js: "//# sourceURL=VencordRenderer\n//# sourceMappingURL=vencord://renderer.js.map" },
|
||||||
globalName: "Vencord",
|
globalName: "Vencord",
|
||||||
sourcemap,
|
sourcemap: "external",
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins,
|
globPlugins,
|
||||||
...commonOpts.plugins
|
...commonOpts.plugins
|
||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
...defines,
|
IS_WEB: "false",
|
||||||
IS_WEB: false
|
IS_STANDALONE: isStandalone
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
]).catch(err => {
|
]).catch(err => {
|
||||||
|
@ -20,13 +20,13 @@
|
|||||||
|
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
import { zip } from "fflate";
|
import { zip } from "fflate";
|
||||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
import { readFile } from "fs/promises";
|
import { readFile } from "fs/promises";
|
||||||
import { join, resolve } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
// wtf is this assert syntax
|
// wtf is this assert syntax
|
||||||
import PackageJSON from "../../package.json" assert { type: "json" };
|
import PackageJSON from "../../package.json" assert { type: "json" };
|
||||||
import { commonOpts, fileIncludePlugin, gitHashPlugin, gitRemotePlugin, globPlugins, watch } from "./common.mjs";
|
import { commonOpts, fileIncludePlugin, gitHashPlugin, gitRemotePlugin, globPlugins } from "./common.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.BuildOptions}
|
* @type {esbuild.BuildOptions}
|
||||||
@ -46,8 +46,7 @@ const commonOptions = {
|
|||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
define: {
|
define: {
|
||||||
IS_WEB: "true",
|
IS_WEB: "true",
|
||||||
IS_STANDALONE: "true",
|
IS_STANDALONE: "true"
|
||||||
IS_DEV: JSON.stringify(watch)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -62,7 +61,7 @@ await Promise.all(
|
|||||||
...commonOptions,
|
...commonOptions,
|
||||||
outfile: "dist/Vencord.user.js",
|
outfile: "dist/Vencord.user.js",
|
||||||
banner: {
|
banner: {
|
||||||
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${PackageJSON.version}.${new Date().getTime()}`)
|
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", PackageJSON.version)
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
|
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
|
||||||
@ -72,39 +71,20 @@ await Promise.all(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
async function buildPluginZip(target, files, shouldZip) {
|
zip({
|
||||||
const entries = {
|
dist: {
|
||||||
"dist/Vencord.js": readFileSync("dist/browser.js"),
|
"Vencord.js": readFileSync("dist/browser.js")
|
||||||
...Object.fromEntries(await Promise.all(files.map(async f => [
|
},
|
||||||
(f.startsWith("manifest") ? "manifest.json" : f),
|
...Object.fromEntries(await Promise.all(["background.js", "content.js", "manifest.json"].map(async f => [
|
||||||
await readFile(join("browser", f))
|
f,
|
||||||
]))),
|
await readFile(join("browser", f))
|
||||||
};
|
]))),
|
||||||
|
}, {}, (err, data) => {
|
||||||
if (shouldZip) {
|
if (err) {
|
||||||
zip(entries, {}, (err, data) => {
|
console.error(err);
|
||||||
if (err) {
|
process.exitCode = 1;
|
||||||
console.error(err);
|
|
||||||
process.exitCode = 1;
|
|
||||||
} else {
|
|
||||||
writeFileSync("dist/" + target, data);
|
|
||||||
console.info("Extension written to dist/" + target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
if (existsSync(target))
|
writeFileSync("dist/extension.zip", data);
|
||||||
rmSync(target, { recursive: true });
|
console.info("Extension written to dist/extension.zip");
|
||||||
for (const entry in entries) {
|
|
||||||
const destination = "dist/" + target + "/" + entry;
|
|
||||||
const parentDirectory = resolve(destination, "..");
|
|
||||||
mkdirSync(parentDirectory, { recursive: true });
|
|
||||||
writeFileSync(destination, entries[entry]);
|
|
||||||
}
|
|
||||||
console.info("Unpacked Extension written to dist/" + target);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
await buildPluginZip("extension-v3.zip", ["modifyResponseHeaders.json", "content.js", "manifestv3.json"], true);
|
|
||||||
await buildPluginZip("extension-v2.zip", ["background.js", "content.js", "manifestv2.json"], true);
|
|
||||||
await buildPluginZip("extension-v2-unpacked", ["background.js", "content.js", "manifestv2.json"], false);
|
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { exec, execSync } from "child_process";
|
import { exec, execSync } from "child_process";
|
||||||
|
import esbuild from "esbuild";
|
||||||
import { existsSync } from "fs";
|
import { existsSync } from "fs";
|
||||||
import { readdir, readFile } from "fs/promises";
|
import { readdir, readFile } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
@ -24,14 +25,6 @@ import { promisify } from "util";
|
|||||||
|
|
||||||
export const watch = process.argv.includes("--watch");
|
export const watch = process.argv.includes("--watch");
|
||||||
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
||||||
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
|
||||||
export const banner = {
|
|
||||||
js: `
|
|
||||||
// Vencord ${gitHash}
|
|
||||||
// Standalone: ${isStandalone}
|
|
||||||
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
|
|
||||||
`.trim()
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
|
||||||
/**
|
/**
|
||||||
@ -86,6 +79,7 @@ export const globPlugins = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.Plugin}
|
* @type {esbuild.Plugin}
|
||||||
*/
|
*/
|
||||||
@ -148,7 +142,7 @@ export const fileIncludePlugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").BuildOptions}
|
* @type {esbuild.BuildOptions}
|
||||||
*/
|
*/
|
||||||
export const commonOpts = {
|
export const commonOpts = {
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
@ -157,12 +151,6 @@ export const commonOpts = {
|
|||||||
minify: !watch,
|
minify: !watch,
|
||||||
sourcemap: watch ? "inline" : "",
|
sourcemap: watch ? "inline" : "",
|
||||||
legalComments: "linked",
|
legalComments: "linked",
|
||||||
banner,
|
|
||||||
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin],
|
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin],
|
||||||
external: ["~plugins", "~git-hash", "~git-remote"],
|
external: ["~plugins", "~git-hash", "~git-remote"]
|
||||||
inject: ["./scripts/build/inject/react.mjs"],
|
|
||||||
jsxFactory: "VencordCreateElement",
|
|
||||||
jsxFragment: "VencordFragment",
|
|
||||||
// Work around https://github.com/evanw/esbuild/issues/2460
|
|
||||||
tsconfig: "./scripts/build/tsconfig.esbuild.json"
|
|
||||||
};
|
};
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const VencordFragment = Symbol.for("react.fragment");
|
|
||||||
export let VencordCreateElement =
|
|
||||||
(...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);
|
|
@ -1,7 +0,0 @@
|
|||||||
// Work around https://github.com/evanw/esbuild/issues/2460
|
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "react"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// A script to automatically generate a list of all plugins.
|
|
||||||
// Just copy paste the entire file into a running Vencord install and it will prompt you
|
|
||||||
// to save the file
|
|
||||||
|
|
||||||
// eslint-disable-next-line spaced-comment
|
|
||||||
/// <reference types="../src/modules"/>
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
/**
|
|
||||||
* @type {typeof import("~plugins").default}
|
|
||||||
*/
|
|
||||||
const Plugins = Vencord.Plugins.plugins;
|
|
||||||
|
|
||||||
const header = `
|
|
||||||
<!-- This file is auto generated, do not edit -->
|
|
||||||
|
|
||||||
# Vencord Plugins
|
|
||||||
`;
|
|
||||||
|
|
||||||
let tableOfContents = "\n\n";
|
|
||||||
|
|
||||||
let list = "\n\n";
|
|
||||||
|
|
||||||
for (const p of Object.values(Plugins).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
||||||
tableOfContents += `- [${p.name}](#${p.name.replaceAll(" ", "-")})\n`;
|
|
||||||
|
|
||||||
list += `## ${p.name}
|
|
||||||
|
|
||||||
${p.description}
|
|
||||||
|
|
||||||
**Authors**: ${p.authors.map(a => a.name).join(", ")}
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (p.commands?.length) {
|
|
||||||
list += "\n\n#### Commands\n";
|
|
||||||
for (const cmd of p.commands) {
|
|
||||||
list += `${cmd.name} - ${cmd.description}\n\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
list += "\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
copy(header + tableOfContents + list);
|
|
||||||
})();
|
|
@ -59,7 +59,6 @@ const LINUX_DISCORD_DIRS = [
|
|||||||
"/usr/lib64",
|
"/usr/lib64",
|
||||||
"/opt",
|
"/opt",
|
||||||
`${process.env.HOME}/.local/share`,
|
`${process.env.HOME}/.local/share`,
|
||||||
`${process.env.HOME}/.dvm`,
|
|
||||||
"/var/lib/flatpak/app",
|
"/var/lib/flatpak/app",
|
||||||
`${process.env.HOME}/.local/share/flatpak/app`,
|
`${process.env.HOME}/.local/share/flatpak/app`,
|
||||||
];
|
];
|
||||||
|
@ -39,7 +39,6 @@ const {
|
|||||||
getDarwinDirs,
|
getDarwinDirs,
|
||||||
getLinuxDirs,
|
getLinuxDirs,
|
||||||
ENTRYPOINT,
|
ENTRYPOINT,
|
||||||
question
|
|
||||||
} = require("./common");
|
} = require("./common");
|
||||||
|
|
||||||
switch (process.platform) {
|
switch (process.platform) {
|
||||||
@ -63,14 +62,15 @@ async function install(installations) {
|
|||||||
// Attempt to give flatpak perms
|
// Attempt to give flatpak perms
|
||||||
if (selected.isFlatpak) {
|
if (selected.isFlatpak) {
|
||||||
try {
|
try {
|
||||||
|
const { branch } = selected;
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const globalCmd = `flatpak override ${selected.branch} --filesystem=${cwd}`;
|
const globalCmd = `flatpak override ${branch} --filesystem=${cwd}`;
|
||||||
const userCmd = `flatpak override --user ${selected.branch} --filesystem=${cwd}`;
|
const userCmd = `flatpak override --user ${branch} --filesystem=${cwd}`;
|
||||||
const cmd = selected.location.startsWith("/home")
|
const cmd = selected.location.startsWith("/home")
|
||||||
? userCmd
|
? userCmd
|
||||||
: globalCmd;
|
: globalCmd;
|
||||||
execSync(cmd);
|
execSync(cmd);
|
||||||
console.log("Gave write perms to Discord Flatpak.");
|
console.log("Successfully gave write perms to Discord Flatpak.");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Failed to give write perms to Discord Flatpak.");
|
console.log("Failed to give write perms to Discord Flatpak.");
|
||||||
console.log(
|
console.log(
|
||||||
@ -79,29 +79,6 @@ async function install(installations) {
|
|||||||
);
|
);
|
||||||
process.exit(1);
|
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`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const version of selected.versions) {
|
for (const version of selected.versions) {
|
||||||
|
@ -18,19 +18,20 @@
|
|||||||
|
|
||||||
export * as Api from "./api";
|
export * as Api from "./api";
|
||||||
export * as Plugins from "./plugins";
|
export * as Plugins from "./plugins";
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
||||||
export * as Util from "./utils";
|
export * as Util from "./utils";
|
||||||
export * as QuickCss from "./utils/quickCss";
|
export * as QuickCss from "./utils/quickCss";
|
||||||
export * as Updater from "./utils/updater";
|
export * as Updater from "./utils/updater";
|
||||||
export * as Webpack from "./webpack";
|
export * as Webpack from "./webpack";
|
||||||
export { PlainSettings, Settings };
|
|
||||||
|
|
||||||
import "./utils/quickCss";
|
|
||||||
import "./webpack/patchWebpack";
|
|
||||||
|
|
||||||
import { popNotice, showNotice } from "./api/Notices";
|
import { popNotice, showNotice } from "./api/Notices";
|
||||||
import { PlainSettings, Settings } from "./api/settings";
|
import { PlainSettings,Settings } from "./api/settings";
|
||||||
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
import { startAllPlugins } from "./plugins";
|
||||||
|
|
||||||
|
export { PlainSettings,Settings };
|
||||||
|
|
||||||
|
import "./webpack/patchWebpack";
|
||||||
|
import "./utils/quickCss";
|
||||||
|
|
||||||
import { checkForUpdates, UpdateLogger } from "./utils/updater";
|
import { checkForUpdates, UpdateLogger } from "./utils/updater";
|
||||||
import { onceReady } from "./webpack";
|
import { onceReady } from "./webpack";
|
||||||
import { Router } from "./webpack/common";
|
import { Router } from "./webpack/common";
|
||||||
@ -60,19 +61,6 @@ async function init() {
|
|||||||
UpdateLogger.error("Failed to check for updates", err);
|
UpdateLogger.error("Failed to check for updates", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_DEV) {
|
|
||||||
const pendingPatches = patches.filter(p => !p.all && p.predicate?.() !== false);
|
|
||||||
if (pendingPatches.length)
|
|
||||||
PMLogger.warn(
|
|
||||||
"Webpack has finished initialising, but some patches haven't been applied yet.",
|
|
||||||
"This might be expected since some Modules are lazy loaded, but please verify",
|
|
||||||
"that all plugins are working as intended.",
|
|
||||||
"You are seeing this warning because this is a Development build of Vencord.",
|
|
||||||
"\nThe following patches have not been applied:",
|
|
||||||
"\n\n" + pendingPatches.map(p => `${p.plugin}: ${p.find}`).join("\n")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
@ -16,9 +16,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import IPC_EVENTS from "@utils/IpcEvents";
|
|
||||||
import { IpcRenderer, ipcRenderer } from "electron";
|
import { IpcRenderer, ipcRenderer } from "electron";
|
||||||
|
|
||||||
|
import IPC_EVENTS from "./utils/IpcEvents";
|
||||||
|
|
||||||
function assertEventAllowed(event: string) {
|
function assertEventAllowed(event: string) {
|
||||||
if (!(event in IPC_EVENTS)) throw new Error(`Event ${event} not allowed.`);
|
if (!(event in IPC_EVENTS)) throw new Error(`Event ${event} not allowed.`);
|
||||||
}
|
}
|
||||||
|
@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { User } from "discord-types/general";
|
|
||||||
import { HTMLProps } from "react";
|
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
|
||||||
|
|
||||||
export enum BadgePosition {
|
|
||||||
START,
|
|
||||||
END
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProfileBadge {
|
|
||||||
/** The tooltip to show on hover */
|
|
||||||
tooltip: string;
|
|
||||||
/** The custom image to use */
|
|
||||||
image?: string;
|
|
||||||
/** Action to perform when you click the badge */
|
|
||||||
onClick?(): void;
|
|
||||||
/** Should the user display this badge? */
|
|
||||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
|
||||||
/** Optional props (e.g. style) for the badge */
|
|
||||||
props?: HTMLProps<HTMLImageElement>;
|
|
||||||
/** Insert at start or end? */
|
|
||||||
position?: BadgePosition;
|
|
||||||
|
|
||||||
/** The badge name to display. Discord uses this, but we don't. */
|
|
||||||
key?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Badges = new Set<ProfileBadge>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a new badge with the Badges API
|
|
||||||
* @param badge The badge to register
|
|
||||||
*/
|
|
||||||
export function addBadge(badge: ProfileBadge) {
|
|
||||||
Badges.add(badge);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregister a badge from the Badges API
|
|
||||||
* @param badge The badge to remove
|
|
||||||
*/
|
|
||||||
export function removeBadge(badge: ProfileBadge) {
|
|
||||||
return Badges.delete(badge);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject badges into the profile badges array.
|
|
||||||
* You probably don't need to use this.
|
|
||||||
*/
|
|
||||||
export function inject(badgeArray: ProfileBadge[], args: BadgeUserArgs) {
|
|
||||||
for (const badge of Badges) {
|
|
||||||
if (!badge.shouldShow || badge.shouldShow(args)) {
|
|
||||||
badge.position === BadgePosition.START
|
|
||||||
? badgeArray.unshift(badge)
|
|
||||||
: badgeArray.push(badge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id);
|
|
||||||
|
|
||||||
return badgeArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BadgeUserArgs {
|
|
||||||
user: User;
|
|
||||||
profile: Profile;
|
|
||||||
premiumSince: Date;
|
|
||||||
premiumGuildSince?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConnectedAccount {
|
|
||||||
type: string;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
verified: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Profile {
|
|
||||||
connectedAccounts: ConnectedAccount[];
|
|
||||||
premiumType: number;
|
|
||||||
premiumSince: string;
|
|
||||||
premiumGuildSince?: any;
|
|
||||||
lastFetched: number;
|
|
||||||
profileFetchFailed: boolean;
|
|
||||||
application?: any;
|
|
||||||
}
|
|
@ -16,15 +16,15 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mergeDefaults } from "@utils/misc";
|
|
||||||
import { findByCodeLazy, findByPropsLazy, waitFor } from "@webpack";
|
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
import type { PartialDeep } from "type-fest";
|
import type { PartialDeep } from "type-fest";
|
||||||
|
|
||||||
|
import { lazyWebpack, mergeDefaults } from "../../utils/misc";
|
||||||
|
import { filters, waitFor } from "../../webpack";
|
||||||
import { Argument } from "./types";
|
import { Argument } from "./types";
|
||||||
|
|
||||||
const createBotMessage = findByCodeLazy('username:"Clyde"');
|
const createBotMessage = lazyWebpack(filters.byCode('username:"Clyde"'));
|
||||||
const MessageSender = findByPropsLazy("receiveMessage");
|
const MessageSender = lazyWebpack(filters.byProps(["receiveMessage"]));
|
||||||
|
|
||||||
let SnowflakeUtils: any;
|
let SnowflakeUtils: any;
|
||||||
waitFor("fromTimestamp", m => SnowflakeUtils = m);
|
waitFor("fromTimestamp", m => SnowflakeUtils = m);
|
||||||
|
@ -16,10 +16,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { makeCodeblock } from "@utils/misc";
|
import { makeCodeblock } from "../../utils/misc";
|
||||||
|
import { generateId, sendBotMessage } from "./commandHelpers";
|
||||||
import { sendBotMessage } from "./commandHelpers";
|
import { ApplicationCommandInputType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
|
||||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
|
|
||||||
|
|
||||||
export * from "./commandHelpers";
|
export * from "./commandHelpers";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
@ -80,12 +79,7 @@ export const _handleCommand = function (cmd: Command, args: Argument[], ctx: Com
|
|||||||
}
|
}
|
||||||
} as never;
|
} as never;
|
||||||
|
|
||||||
|
function modifyOpt(opt: Option | Command) {
|
||||||
/**
|
|
||||||
* Prepare a Command Option for Discord by filling missing fields
|
|
||||||
* @param opt
|
|
||||||
*/
|
|
||||||
export function prepareOption<O extends Option | Command>(opt: O): O {
|
|
||||||
opt.displayName ||= opt.name;
|
opt.displayName ||= opt.name;
|
||||||
opt.displayDescription ||= opt.description;
|
opt.displayDescription ||= opt.description;
|
||||||
opt.options?.forEach((opt, i, opts) => {
|
opt.options?.forEach((opt, i, opts) => {
|
||||||
@ -94,36 +88,11 @@ export function prepareOption<O extends Option | Command>(opt: O): O {
|
|||||||
else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;
|
else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;
|
||||||
opt.choices?.forEach(x => x.displayName ||= x.name);
|
opt.choices?.forEach(x => x.displayName ||= x.name);
|
||||||
|
|
||||||
prepareOption(opts[i]);
|
modifyOpt(opts[i]);
|
||||||
});
|
|
||||||
return opt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Yes, Discord registers individual commands for each subcommand
|
|
||||||
// TODO: This probably doesn't support nested subcommands. If that is ever needed,
|
|
||||||
// investigate
|
|
||||||
function registerSubCommands(cmd: Command, plugin: string) {
|
|
||||||
cmd.options?.forEach(o => {
|
|
||||||
if (o.type !== ApplicationCommandOptionType.SUB_COMMAND)
|
|
||||||
throw new Error("When specifying sub-command options, all options must be sub-commands.");
|
|
||||||
const subCmd = {
|
|
||||||
...cmd,
|
|
||||||
...o,
|
|
||||||
type: ApplicationCommandType.CHAT_INPUT,
|
|
||||||
name: `${cmd.name} ${o.name}`,
|
|
||||||
displayName: `${cmd.name} ${o.name}`,
|
|
||||||
subCommandPath: [{
|
|
||||||
name: o.name,
|
|
||||||
type: o.type,
|
|
||||||
displayName: o.name
|
|
||||||
}],
|
|
||||||
rootCommand: cmd
|
|
||||||
};
|
|
||||||
registerCommand(subCmd as any, plugin);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerCommand<C extends Command>(command: C, plugin: string) {
|
export function registerCommand(command: Command, plugin: string) {
|
||||||
if (!BUILT_IN) {
|
if (!BUILT_IN) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[CommandsAPI]",
|
"[CommandsAPI]",
|
||||||
@ -137,19 +106,13 @@ export function registerCommand<C extends Command>(command: C, plugin: string) {
|
|||||||
throw new Error(`Command '${command.name}' already exists.`);
|
throw new Error(`Command '${command.name}' already exists.`);
|
||||||
|
|
||||||
command.isVencordCommand = true;
|
command.isVencordCommand = true;
|
||||||
command.id ??= `-${BUILT_IN.length + 1}`;
|
command.id ??= generateId();
|
||||||
command.applicationId ??= "-1"; // BUILT_IN;
|
command.applicationId ??= "-1"; // BUILT_IN;
|
||||||
command.type ??= ApplicationCommandType.CHAT_INPUT;
|
command.type ??= ApplicationCommandType.CHAT_INPUT;
|
||||||
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
|
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
|
||||||
command.plugin ||= plugin;
|
command.plugin ||= plugin;
|
||||||
|
|
||||||
prepareOption(command);
|
modifyOpt(command);
|
||||||
|
|
||||||
if (command.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND) {
|
|
||||||
registerSubCommands(command, plugin);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
commands[command.name] = command;
|
commands[command.name] = command;
|
||||||
BUILT_IN.push(command);
|
BUILT_IN.push(command);
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,6 @@ export interface Argument {
|
|||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
focused: undefined;
|
focused: undefined;
|
||||||
options: Argument[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Command {
|
export interface Command {
|
||||||
|
@ -16,9 +16,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Logger from "@utils/Logger";
|
import type { Channel,Message } from "discord-types/general";
|
||||||
import { MessageStore } from "@webpack/common";
|
|
||||||
import type { Channel, Message } from "discord-types/general";
|
import Logger from "../utils/logger";
|
||||||
|
|
||||||
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
|
||||||
|
|
||||||
@ -37,37 +37,25 @@ export interface MessageObject {
|
|||||||
validNonShortcutEmojis: Emoji[];
|
validNonShortcutEmojis: Emoji[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageExtra {
|
export type SendListener = (channelId: string, messageObj: MessageObject, extra: any) => void;
|
||||||
stickerIds?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => void | { cancel: boolean; };
|
|
||||||
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
|
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
|
||||||
|
|
||||||
const sendListeners = new Set<SendListener>();
|
const sendListeners = new Set<SendListener>();
|
||||||
const editListeners = new Set<EditListener>();
|
const editListeners = new Set<EditListener>();
|
||||||
|
|
||||||
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
|
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: any) {
|
||||||
for (const listener of sendListeners) {
|
for (const listener of sendListeners) {
|
||||||
try {
|
try {
|
||||||
const result = listener(channelId, messageObj, extra);
|
listener(channelId, messageObj, extra);
|
||||||
if (result && result.cancel === true) {
|
} catch (e) { MessageEventsLogger.error(`MessageSendHandler: Listener encoutered an unknown error. (${e})`); }
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
MessageEventsLogger.error("MessageSendHandler: Listener encountered an unknown error\n", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
|
export function _handlePreEdit(channeld: string, messageId: string, messageObj: MessageObject) {
|
||||||
for (const listener of editListeners) {
|
for (const listener of editListeners) {
|
||||||
try {
|
try {
|
||||||
listener(channelId, messageId, messageObj);
|
listener(channeld, messageId, messageObj);
|
||||||
} catch (e) {
|
} catch (e) { MessageEventsLogger.error(`MessageEditHandler: Listener encoutered an unknown error. (${e})`); }
|
||||||
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,14 +87,10 @@ type ClickListener = (message: Message, channel: Channel, event: MouseEvent) =>
|
|||||||
const listeners = new Set<ClickListener>();
|
const listeners = new Set<ClickListener>();
|
||||||
|
|
||||||
export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
|
export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
|
||||||
// message object may be outdated, so (try to) fetch latest one
|
|
||||||
message = MessageStore.getMessage(channel.id, message.id) ?? message;
|
|
||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
try {
|
try {
|
||||||
listener(message, channel, event);
|
listener(message, channel, event);
|
||||||
} catch (e) {
|
} catch (e) { MessageEventsLogger.error(`MessageClickHandler: Listener encoutered an unknown error. (${e})`); }
|
||||||
MessageEventsLogger.error("MessageClickHandler: Listener encountered an unknown error\n", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import { Channel, Message } from "discord-types/general";
|
|
||||||
import type { MouseEventHandler } from "react";
|
|
||||||
|
|
||||||
const logger = new Logger("MessagePopover");
|
|
||||||
|
|
||||||
export interface ButtonItem {
|
|
||||||
key?: string,
|
|
||||||
label: string,
|
|
||||||
icon: React.ComponentType<any>,
|
|
||||||
message: Message,
|
|
||||||
channel: Channel,
|
|
||||||
onClick?: MouseEventHandler<HTMLButtonElement>,
|
|
||||||
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type getButtonItem = (message: Message) => ButtonItem | null;
|
|
||||||
|
|
||||||
export const buttons = new Map<string, getButtonItem>();
|
|
||||||
|
|
||||||
export function addButton(
|
|
||||||
identifier: string,
|
|
||||||
item: getButtonItem,
|
|
||||||
) {
|
|
||||||
buttons.set(identifier, item);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeButton(identifier: string) {
|
|
||||||
buttons.delete(identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function _buildPopoverElements(
|
|
||||||
msg: Message,
|
|
||||||
makeButton: (item: ButtonItem) => React.ComponentType
|
|
||||||
) {
|
|
||||||
const items = [] as React.ComponentType[];
|
|
||||||
|
|
||||||
for (const [identifier, getItem] of buttons.entries()) {
|
|
||||||
try {
|
|
||||||
const item = getItem(msg);
|
|
||||||
if (item) {
|
|
||||||
item.key ??= identifier;
|
|
||||||
items.push(makeButton(item));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`[${identifier}]`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { waitFor } from "@webpack";
|
import { waitFor } from "../webpack";
|
||||||
|
|
||||||
let NoticesModule: any;
|
let NoticesModule: any;
|
||||||
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
|
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
|
|
||||||
const logger = new Logger("ServerListAPI");
|
|
||||||
|
|
||||||
export enum ServerListRenderPosition {
|
|
||||||
Above,
|
|
||||||
In,
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderFunctionsAbove = new Set<Function>();
|
|
||||||
const renderFunctionsIn = new Set<Function>();
|
|
||||||
|
|
||||||
function getRenderFunctions(position: ServerListRenderPosition) {
|
|
||||||
return position === ServerListRenderPosition.Above ? renderFunctionsAbove : renderFunctionsIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addServerListElement(position: ServerListRenderPosition, renderFunction: Function) {
|
|
||||||
getRenderFunctions(position).add(renderFunction);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeServerListElement(position: ServerListRenderPosition, renderFunction: Function) {
|
|
||||||
getRenderFunctions(position).delete(renderFunction);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const renderAll = (position: ServerListRenderPosition) => {
|
|
||||||
const ret: Array<JSX.Element> = [];
|
|
||||||
|
|
||||||
for (const renderFunction of getRenderFunctions(position)) {
|
|
||||||
try {
|
|
||||||
ret.unshift(renderFunction());
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Failed to render server list element:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
};
|
|
@ -16,14 +16,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as $Badges from "./Badges";
|
|
||||||
import * as $Commands from "./Commands";
|
import * as $Commands from "./Commands";
|
||||||
import * as $DataStore from "./DataStore";
|
import * as $DataStore from "./DataStore";
|
||||||
import * as $MessageAccessories from "./MessageAccessories";
|
import * as $MessageAccessories from "./MessageAccessories";
|
||||||
import * as $MessageEventsAPI from "./MessageEvents";
|
import * as $MessageEventsAPI from "./MessageEvents";
|
||||||
import * as $MessagePopover from "./MessagePopover";
|
|
||||||
import * as $Notices from "./Notices";
|
import * as $Notices from "./Notices";
|
||||||
import * as $ServerList from "./ServerList";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An API allowing you to listen to Message Clicks or run your own logic
|
* An API allowing you to listen to Message Clicks or run your own logic
|
||||||
@ -60,17 +57,5 @@ const DataStore = $DataStore;
|
|||||||
* An API allowing you to add custom components as message accessories
|
* An API allowing you to add custom components as message accessories
|
||||||
*/
|
*/
|
||||||
const MessageAccessories = $MessageAccessories;
|
const MessageAccessories = $MessageAccessories;
|
||||||
/**
|
|
||||||
* An API allowing you to add custom buttons in the message popover
|
|
||||||
*/
|
|
||||||
const MessagePopover = $MessagePopover;
|
|
||||||
/**
|
|
||||||
* An API allowing you to add badges to user profiles
|
|
||||||
*/
|
|
||||||
const Badges = $Badges;
|
|
||||||
/**
|
|
||||||
* An API allowing you to add custom elements to the server list
|
|
||||||
*/
|
|
||||||
const ServerList = $ServerList;
|
|
||||||
|
|
||||||
export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, MessagePopover, Notices, ServerList };
|
export { Commands,DataStore, MessageAccessories, MessageEvents, Notices };
|
||||||
|
@ -16,20 +16,17 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import { mergeDefaults } from "@utils/misc";
|
|
||||||
import { OptionType } from "@utils/types";
|
|
||||||
import { React } from "@webpack/common";
|
|
||||||
|
|
||||||
import plugins from "~plugins";
|
import plugins from "~plugins";
|
||||||
|
|
||||||
const logger = new Logger("Settings");
|
import IpcEvents from "../utils/IpcEvents";
|
||||||
|
import { mergeDefaults } from "../utils/misc";
|
||||||
|
import { OptionType } from "../utils/types";
|
||||||
|
import { React } from "../webpack/common";
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
notifyAboutUpdates: boolean;
|
notifyAboutUpdates: boolean;
|
||||||
useQuickCss: boolean;
|
useQuickCss: boolean;
|
||||||
enableReactDevtools: boolean;
|
enableReactDevtools: boolean;
|
||||||
themeLinks: string[];
|
|
||||||
plugins: {
|
plugins: {
|
||||||
[plugin: string]: {
|
[plugin: string]: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -41,38 +38,35 @@ export interface Settings {
|
|||||||
const DefaultSettings: Settings = {
|
const DefaultSettings: Settings = {
|
||||||
notifyAboutUpdates: true,
|
notifyAboutUpdates: true,
|
||||||
useQuickCss: true,
|
useQuickCss: true,
|
||||||
themeLinks: [],
|
|
||||||
enableReactDevtools: false,
|
enableReactDevtools: false,
|
||||||
plugins: {}
|
plugins: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const plugin in plugins) {
|
||||||
|
DefaultSettings.plugins[plugin] = {
|
||||||
|
enabled: plugins[plugin].required ?? false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings;
|
var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings;
|
||||||
mergeDefaults(settings, DefaultSettings);
|
mergeDefaults(settings, DefaultSettings);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error("Corrupt settings file. ", err);
|
||||||
var settings = mergeDefaults({} as Settings, DefaultSettings);
|
var settings = mergeDefaults({} as Settings, DefaultSettings);
|
||||||
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
|
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
|
||||||
const subscriptions = new Set<SubscriptionCallback>();
|
const subscriptions = new Set<SubscriptionCallback>();
|
||||||
|
|
||||||
const proxyCache = {} as Record<string, any>;
|
|
||||||
|
|
||||||
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
|
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
|
||||||
function makeProxy(settings: any, root = settings, path = ""): Settings {
|
function makeProxy(settings: Settings, root = settings, path = ""): Settings {
|
||||||
return proxyCache[path] ??= new Proxy(settings, {
|
return new Proxy(settings, {
|
||||||
get(target, p: string) {
|
get(target, p: string) {
|
||||||
const v = target[p];
|
const v = target[p];
|
||||||
|
|
||||||
// using "in" is important in the following cases to properly handle falsy or nullish values
|
// using "in" is important in the following cases to properly handle falsy or nullish values
|
||||||
if (!(p in target)) {
|
if (!(p in target)) {
|
||||||
// Return empty for plugins with no settings
|
|
||||||
if (path === "plugins" && p in plugins)
|
|
||||||
return target[p] = makeProxy({
|
|
||||||
enabled: plugins[p].required ?? false
|
|
||||||
}, root, `plugins.${p}`);
|
|
||||||
|
|
||||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||||
// the default value.
|
// the default value.
|
||||||
if (path.startsWith("plugins.")) {
|
if (path.startsWith("plugins.")) {
|
||||||
@ -82,13 +76,9 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
|||||||
if (!setting) return v;
|
if (!setting) return v;
|
||||||
if ("default" in setting)
|
if ("default" in setting)
|
||||||
// normal setting with a default value
|
// normal setting with a default value
|
||||||
return (target[p] = setting.default);
|
return setting.default;
|
||||||
if (setting.type === OptionType.SELECT) {
|
if (setting.type === OptionType.SELECT)
|
||||||
const def = setting.options.find(o => o.default);
|
return setting.options.find(o => o.default)?.value;
|
||||||
if (def)
|
|
||||||
target[p] = def.value;
|
|
||||||
return def?.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
@ -141,19 +131,14 @@ export const Settings = makeProxy(settings);
|
|||||||
* Settings hook for React components. Returns a smart settings
|
* Settings hook for React components. Returns a smart settings
|
||||||
* object that automagically triggers a rerender if any properties
|
* object that automagically triggers a rerender if any properties
|
||||||
* are altered
|
* are altered
|
||||||
* @param paths An optional list of paths to whitelist for rerenders
|
|
||||||
* @returns Settings
|
* @returns Settings
|
||||||
*/
|
*/
|
||||||
export function useSettings(paths?: string[]) {
|
export function useSettings() {
|
||||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||||
|
|
||||||
const onUpdate: SubscriptionCallback = paths
|
|
||||||
? (value, path) => paths.includes(path) && forceUpdate()
|
|
||||||
: forceUpdate;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
subscriptions.add(onUpdate);
|
subscriptions.add(forceUpdate);
|
||||||
return () => void subscriptions.delete(onUpdate);
|
return () => void subscriptions.delete(forceUpdate);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return Settings;
|
return Settings;
|
||||||
@ -180,21 +165,3 @@ export function addSettingsListener(path: string, onUpdate: (newValue: any, path
|
|||||||
(onUpdate as SubscriptionCallback)._path = path;
|
(onUpdate as SubscriptionCallback)._path = path;
|
||||||
subscriptions.add(onUpdate);
|
subscriptions.add(onUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
|
||||||
const { plugins } = settings;
|
|
||||||
if (name in plugins) return;
|
|
||||||
|
|
||||||
for (const oldName of oldNames) {
|
|
||||||
if (oldName in plugins) {
|
|
||||||
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
|
|
||||||
plugins[name] = plugins[oldName];
|
|
||||||
delete plugins[oldName];
|
|
||||||
VencordNative.ipc.invoke(
|
|
||||||
IpcEvents.SET_SETTINGS,
|
|
||||||
JSON.stringify(settings, null, 4)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { React, TextInput } from "@webpack/common";
|
|
||||||
|
|
||||||
// TODO: Refactor settings to use this as well
|
|
||||||
interface TextInputProps {
|
|
||||||
/**
|
|
||||||
* WARNING: Changing this between renders will have no effect!
|
|
||||||
*/
|
|
||||||
value: string;
|
|
||||||
/**
|
|
||||||
* This will only be called if the new value passed validate()
|
|
||||||
*/
|
|
||||||
onChange(newValue: string): void;
|
|
||||||
/**
|
|
||||||
* Optionally validate the user input
|
|
||||||
* Return true if the input is valid
|
|
||||||
* Otherwise, return a string containing the reason for this input being invalid
|
|
||||||
*/
|
|
||||||
validate(v: string): true | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A very simple wrapper around Discord's TextInput that validates input and shows
|
|
||||||
* the user an error message and only calls your onChange when the input is valid
|
|
||||||
*/
|
|
||||||
export function CheckedTextInput({ value: initialValue, onChange, validate }: TextInputProps) {
|
|
||||||
const [value, setValue] = React.useState(initialValue);
|
|
||||||
const [error, setError] = React.useState<string>();
|
|
||||||
|
|
||||||
function handleChange(v: string) {
|
|
||||||
setValue(v);
|
|
||||||
const res = validate(v);
|
|
||||||
if (res === true) {
|
|
||||||
setError(void 0);
|
|
||||||
onChange(v);
|
|
||||||
} else {
|
|
||||||
setError(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
|
||||||
import { Button } from "@webpack/common";
|
|
||||||
|
|
||||||
import { Heart } from "./Heart";
|
|
||||||
|
|
||||||
export default function DonateButton(props: any) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
{...props}
|
|
||||||
look={Button.Looks.LINK}
|
|
||||||
color={Button.Colors.TRANSPARENT}
|
|
||||||
onClick={() =>
|
|
||||||
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/sponsors/Vendicated")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Heart />
|
|
||||||
Donate
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
@ -16,20 +16,13 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Logger from "@utils/Logger";
|
import Logger from "../utils/logger";
|
||||||
import { LazyComponent } from "@utils/misc";
|
import { Margins, React } from "../webpack/common";
|
||||||
import { Margins, React } from "@webpack/common";
|
|
||||||
|
|
||||||
import { ErrorCard } from "./ErrorCard";
|
import { ErrorCard } from "./ErrorCard";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Render nothing if an error occurs */
|
|
||||||
noop?: boolean;
|
|
||||||
/** Fallback component to render if an error occurs */
|
|
||||||
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
||||||
/** called when an error occurs */
|
|
||||||
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
||||||
/** Custom error message */
|
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,77 +32,68 @@ const logger = new Logger("React ErrorBoundary", color);
|
|||||||
|
|
||||||
const NO_ERROR = {};
|
const NO_ERROR = {};
|
||||||
|
|
||||||
// We might want to import this in a place where React isn't ready yet.
|
export default class ErrorBoundary extends React.Component<React.PropsWithChildren<Props>> {
|
||||||
// Thus, wrap in a LazyComponent
|
static wrap<T = any>(Component: React.ComponentType<T>): (props: T) => React.ReactElement {
|
||||||
const ErrorBoundary = LazyComponent(() => {
|
return props => (
|
||||||
return class ErrorBoundary extends React.PureComponent<React.PropsWithChildren<Props>> {
|
<ErrorBoundary>
|
||||||
state = {
|
<Component {...props as any/* I hate react typings ??? */} />
|
||||||
error: NO_ERROR as any,
|
</ErrorBoundary>
|
||||||
stack: "",
|
);
|
||||||
message: ""
|
}
|
||||||
};
|
|
||||||
|
|
||||||
static getDerivedStateFromError(error: any) {
|
state = {
|
||||||
let stack = error?.stack ?? "";
|
error: NO_ERROR as any,
|
||||||
let message = error?.message || String(error);
|
stack: "",
|
||||||
|
message: ""
|
||||||
|
};
|
||||||
|
|
||||||
if (error instanceof Error && stack) {
|
static getDerivedStateFromError(error: any) {
|
||||||
const eolIdx = stack.indexOf("\n");
|
let stack = error?.stack ?? "";
|
||||||
if (eolIdx !== -1) {
|
let message = error?.message || String(error);
|
||||||
message = stack.slice(0, eolIdx);
|
|
||||||
stack = stack.slice(eolIdx + 1).replace(/https:\/\/\S+\/assets\//g, "");
|
if (error instanceof Error && stack) {
|
||||||
}
|
const eolIdx = stack.indexOf("\n");
|
||||||
|
if (eolIdx !== -1) {
|
||||||
|
message = stack.slice(0, eolIdx);
|
||||||
|
stack = stack.slice(eolIdx + 1).replace(/https:\/\/\S+\/assets\//g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { error, stack, message };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
return { error, stack, message };
|
||||||
this.props.onError?.(error, errorInfo);
|
}
|
||||||
logger.error("A component threw an Error\n", error);
|
|
||||||
logger.error("Component Stack", errorInfo.componentStack);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
if (this.state.error === NO_ERROR) return this.props.children;
|
this.props.onError?.(error, errorInfo);
|
||||||
|
logger.error("A component threw an Error\n", error);
|
||||||
|
logger.error("Component Stack", errorInfo.componentStack);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.noop) return null;
|
render() {
|
||||||
|
if (this.state.error === NO_ERROR) return this.props.children;
|
||||||
|
|
||||||
if (this.props.fallback)
|
if (this.props.fallback)
|
||||||
return <this.props.fallback
|
return <this.props.fallback
|
||||||
children={this.props.children}
|
children={this.props.children}
|
||||||
{...this.state}
|
{...this.state}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorCard style={{
|
<ErrorCard style={{
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}>
|
}}>
|
||||||
<h1>Oh no!</h1>
|
<h1>Oh no!</h1>
|
||||||
<p>{msg}</p>
|
<p>{msg}</p>
|
||||||
<code>
|
<code>
|
||||||
{this.state.message}
|
{this.state.message}
|
||||||
{!!this.state.stack && (
|
{!!this.state.stack && (
|
||||||
<pre className={Margins.marginTop8}>
|
<pre className={Margins.marginTop8}>
|
||||||
{this.state.stack}
|
{this.state.stack}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
</code>
|
</code>
|
||||||
</ErrorCard>
|
</ErrorCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}) as
|
|
||||||
React.ComponentType<React.PropsWithChildren<Props>> & {
|
|
||||||
wrap<T extends JSX.IntrinsicAttributes = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
|
|
||||||
};
|
|
||||||
|
|
||||||
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
|
||||||
<ErrorBoundary {...errorBoundaryProps}>
|
|
||||||
<Component {...props} />
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ErrorBoundary;
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Card } from "@webpack/common";
|
import { Card } from "../webpack/common";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { React } from "@webpack/common";
|
import type { React } from "../webpack/common";
|
||||||
|
|
||||||
export function Flex(props: React.PropsWithChildren<{
|
export function Flex(props: React.PropsWithChildren<{
|
||||||
flexDirection?: React.CSSProperties["flexDirection"];
|
flexDirection?: React.CSSProperties["flexDirection"];
|
||||||
@ -24,11 +24,9 @@ export function Flex(props: React.PropsWithChildren<{
|
|||||||
className?: string;
|
className?: string;
|
||||||
} & React.HTMLProps<HTMLDivElement>>) {
|
} & React.HTMLProps<HTMLDivElement>>) {
|
||||||
props.style ??= {};
|
props.style ??= {};
|
||||||
props.style.display = "flex";
|
|
||||||
// TODO(ven): Remove me, what was I thinking??
|
|
||||||
props.style.gap ??= "1em";
|
|
||||||
props.style.flexDirection ||= props.flexDirection;
|
props.style.flexDirection ||= props.flexDirection;
|
||||||
delete props.flexDirection;
|
props.style.gap ??= "1em";
|
||||||
|
props.style.display = "flex";
|
||||||
return (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function Heart() {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
width="16"
|
|
||||||
style={{ marginRight: "0.5em", transform: "translateY(2px)" }}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="#db61a2"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
@ -16,20 +16,21 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { React } from "@webpack/common";
|
import { React } from "../webpack/common";
|
||||||
|
|
||||||
interface Props extends React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> {
|
interface Props {
|
||||||
|
href: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Link(props: React.PropsWithChildren<Props>) {
|
export function Link(props: React.PropsWithChildren<Props>) {
|
||||||
if (props.disabled) {
|
if (props.disabled) {
|
||||||
props.style ??= {};
|
props.style ??= {};
|
||||||
props.style.pointerEvents = "none";
|
props.style.pointerEvents = "none";
|
||||||
props["aria-disabled"] = true;
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<a role="link" target="_blank" {...props}>
|
<a href={props.href} target="_blank" style={props.style}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
@ -16,16 +16,16 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { debounce } from "@utils/debounce";
|
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
|
||||||
import { Queue } from "@utils/Queue";
|
|
||||||
import { find } from "@webpack";
|
|
||||||
|
|
||||||
import monacoHtml from "~fileContent/monacoWin.html";
|
import monacoHtml from "~fileContent/monacoWin.html";
|
||||||
|
|
||||||
|
import { IpcEvents } from "../utils";
|
||||||
|
import { debounce } from "../utils/debounce";
|
||||||
|
import { Queue } from "../utils/Queue";
|
||||||
|
import { find } from "../webpack/webpack";
|
||||||
|
|
||||||
const queue = new Queue();
|
const queue = new Queue();
|
||||||
const setCss = debounce((css: string) => {
|
const setCss = debounce((css: string) => {
|
||||||
queue.push(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css));
|
queue.add(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css));
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function launchMonacoEditor() {
|
export async function launchMonacoEditor() {
|
||||||
@ -33,12 +33,10 @@ export async function launchMonacoEditor() {
|
|||||||
|
|
||||||
win.setCss = setCss;
|
win.setCss = setCss;
|
||||||
win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
|
win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
|
||||||
win.getTheme = () =>
|
win.getTheme = () => find(m => m.ProtoClass?.typeName.endsWith("PreloadedUserSettings"))
|
||||||
find(m =>
|
.getCurrentValue().appearance.theme === 1
|
||||||
m.ProtoClass?.typeName.endsWith("PreloadedUserSettings")
|
? "vs-dark"
|
||||||
)?.getCurrentValue()?.appearance?.theme === 2
|
: "vs-light";
|
||||||
? "vs-light"
|
|
||||||
: "vs-dark";
|
|
||||||
|
|
||||||
win.document.write(monacoHtml);
|
win.document.write(monacoHtml);
|
||||||
}
|
}
|
||||||
|
@ -1,297 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { debounce } from "@utils/debounce";
|
|
||||||
import { makeCodeblock } from "@utils/misc";
|
|
||||||
import { search } from "@webpack";
|
|
||||||
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
|
||||||
|
|
||||||
import { CheckedTextInput } from "./CheckedTextInput";
|
|
||||||
import ErrorBoundary from "./ErrorBoundary";
|
|
||||||
|
|
||||||
// Do not include diff in non dev builds (side effects import)
|
|
||||||
if (IS_DEV) {
|
|
||||||
var differ = require("diff") as typeof import("diff");
|
|
||||||
}
|
|
||||||
|
|
||||||
const findCandidates = debounce(function ({ find, setModule, setError }) {
|
|
||||||
const candidates = search(find);
|
|
||||||
const keys = Object.keys(candidates);
|
|
||||||
const len = keys.length;
|
|
||||||
if (len === 0)
|
|
||||||
setError("No match. Perhaps that module is lazy loaded?");
|
|
||||||
else if (len !== 1)
|
|
||||||
setError("Multiple matches. Please refine your filter");
|
|
||||||
else
|
|
||||||
setModule([keys[0], candidates[keys[0]]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
function ReplacementComponent({ module, match, replacement, setReplacementError }) {
|
|
||||||
const [id, fact] = module;
|
|
||||||
const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
|
|
||||||
|
|
||||||
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
|
||||||
const src: string = fact.toString().replaceAll("\n", "");
|
|
||||||
try {
|
|
||||||
var patched = src.replace(match, replacement);
|
|
||||||
setReplacementError(void 0);
|
|
||||||
} catch (e) {
|
|
||||||
setReplacementError((e as Error).message);
|
|
||||||
return ["", [], []];
|
|
||||||
}
|
|
||||||
const m = src.match(match);
|
|
||||||
return [patched, m, makeDiff(src, patched, m)];
|
|
||||||
}, [id, match, replacement]);
|
|
||||||
|
|
||||||
function makeDiff(original: string, patched: string, match: RegExpMatchArray | null) {
|
|
||||||
if (!match || original === patched) return null;
|
|
||||||
|
|
||||||
const changeSize = patched.length - original.length;
|
|
||||||
|
|
||||||
// Use 200 surrounding characters of context
|
|
||||||
const start = Math.max(0, match.index! - 200);
|
|
||||||
const end = Math.min(original.length, match.index! + match[0].length + 200);
|
|
||||||
// (changeSize may be negative)
|
|
||||||
const endPatched = end + changeSize;
|
|
||||||
|
|
||||||
const context = original.slice(start, end);
|
|
||||||
const patchedContext = patched.slice(start, endPatched);
|
|
||||||
|
|
||||||
return differ.diffWordsWithSpace(context, patchedContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMatch() {
|
|
||||||
if (!matchResult)
|
|
||||||
return <Forms.FormText>Regex doesn't match!</Forms.FormText>;
|
|
||||||
|
|
||||||
const fullMatch = matchResult[0] ? makeCodeblock(matchResult[0], "js") : "";
|
|
||||||
const groups = matchResult.length > 1
|
|
||||||
? makeCodeblock(matchResult.slice(1).map((g, i) => `Group ${i + 1}: ${g}`).join("\n"), "yml")
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div style={{ userSelect: "text" }}>{Parser.parse(fullMatch)}</div>
|
|
||||||
<div style={{ userSelect: "text" }}>{Parser.parse(groups)}</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDiff() {
|
|
||||||
return diff?.map(p => {
|
|
||||||
const color = p.added ? "lime" : p.removed ? "red" : "grey";
|
|
||||||
return <div style={{ color, userSelect: "text" }}>{p.value}</div>;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle>Module {id}</Forms.FormTitle>
|
|
||||||
|
|
||||||
{!!matchResult?.[0]?.length && (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle>Match</Forms.FormTitle>
|
|
||||||
{renderMatch()}
|
|
||||||
</>)
|
|
||||||
}
|
|
||||||
|
|
||||||
{!!diff?.length && (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle>Diff</Forms.FormTitle>
|
|
||||||
{renderDiff()}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!!diff?.length && (
|
|
||||||
<Button className={Margins.marginTop20} onClick={() => {
|
|
||||||
try {
|
|
||||||
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
|
||||||
setCompileResult([true, "Compiled successfully"]);
|
|
||||||
} catch (err) {
|
|
||||||
setCompileResult([false, (err as Error).message]);
|
|
||||||
}
|
|
||||||
}}>Compile</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{compileResult &&
|
|
||||||
<Forms.FormText style={{ color: compileResult[0] ? "var(--text-positive)" : "var(--text-danger)" }}>
|
|
||||||
{compileResult[1]}
|
|
||||||
</Forms.FormText>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
|
||||||
const [isFunc, setIsFunc] = React.useState(false);
|
|
||||||
const [error, setError] = React.useState<string>();
|
|
||||||
|
|
||||||
function onChange(v: string) {
|
|
||||||
setError(void 0);
|
|
||||||
|
|
||||||
if (isFunc) {
|
|
||||||
try {
|
|
||||||
const func = (0, eval)(v);
|
|
||||||
if (typeof func === "function")
|
|
||||||
setReplacement(() => func);
|
|
||||||
else
|
|
||||||
setError("Replacement must be a function");
|
|
||||||
} catch (e) {
|
|
||||||
setReplacement(v);
|
|
||||||
setError((e as Error).message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setReplacement(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(
|
|
||||||
() => void (isFunc ? onChange(replacement) : setError(void 0)),
|
|
||||||
[isFunc]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle>replacement</Forms.FormTitle>
|
|
||||||
<TextInput
|
|
||||||
value={replacement?.toString()}
|
|
||||||
onChange={onChange}
|
|
||||||
error={error ?? replacementError}
|
|
||||||
/>
|
|
||||||
{!isFunc && (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
|
|
||||||
{Object.entries({
|
|
||||||
"$$": "Insert a $",
|
|
||||||
"$&": "Insert the entire match",
|
|
||||||
"$`": "Insert the substring before the match",
|
|
||||||
"$'": "Insert the substring after the match",
|
|
||||||
"$n": "Insert the nth capturing group ($1, $2...)"
|
|
||||||
}).map(([placeholder, desc]) => (
|
|
||||||
<Forms.FormText key={placeholder}>
|
|
||||||
{Parser.parse("`" + placeholder + "`")}: {desc}
|
|
||||||
</Forms.FormText>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Switch
|
|
||||||
className={Margins.marginTop8}
|
|
||||||
value={isFunc}
|
|
||||||
onChange={setIsFunc}
|
|
||||||
note="'replacement' will be evaled if this is toggled"
|
|
||||||
hideBorder={true}
|
|
||||||
>
|
|
||||||
Treat as Function
|
|
||||||
</Switch>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PatchHelper() {
|
|
||||||
const [find, setFind] = React.useState<string>("");
|
|
||||||
const [match, setMatch] = React.useState<string>("");
|
|
||||||
const [replacement, setReplacement] = React.useState<string | Function>("");
|
|
||||||
|
|
||||||
const [replacementError, setReplacementError] = React.useState<string>();
|
|
||||||
|
|
||||||
const [module, setModule] = React.useState<[number, Function]>();
|
|
||||||
const [findError, setFindError] = React.useState<string>();
|
|
||||||
|
|
||||||
const code = React.useMemo(() => {
|
|
||||||
return `
|
|
||||||
{
|
|
||||||
find: ${JSON.stringify(find)},
|
|
||||||
replacement: {
|
|
||||||
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
|
||||||
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`.trim();
|
|
||||||
}, [find, match, replacement]);
|
|
||||||
|
|
||||||
function onFindChange(v: string) {
|
|
||||||
setFindError(void 0);
|
|
||||||
setFind(v);
|
|
||||||
if (v.length) {
|
|
||||||
findCandidates({ find: v, setModule, setError: setFindError });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMatchChange(v: string) {
|
|
||||||
try {
|
|
||||||
new RegExp(v);
|
|
||||||
setFindError(void 0);
|
|
||||||
setMatch(v);
|
|
||||||
} catch (e: any) {
|
|
||||||
setFindError((e as Error).message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Forms.FormSection>
|
|
||||||
<Text variant="heading-md/normal" tag="h2" className={Margins.marginBottom8}>Patch Helper</Text>
|
|
||||||
<Forms.FormTitle>find</Forms.FormTitle>
|
|
||||||
<TextInput
|
|
||||||
type="text"
|
|
||||||
value={find}
|
|
||||||
onChange={onFindChange}
|
|
||||||
error={findError}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Forms.FormTitle>match</Forms.FormTitle>
|
|
||||||
<CheckedTextInput
|
|
||||||
value={match}
|
|
||||||
onChange={onMatchChange}
|
|
||||||
validate={v => {
|
|
||||||
try {
|
|
||||||
return (new RegExp(v), true);
|
|
||||||
} catch (e) {
|
|
||||||
return (e as Error).message;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ReplacementInput
|
|
||||||
replacement={replacement}
|
|
||||||
setReplacement={setReplacement}
|
|
||||||
replacementError={replacementError}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Forms.FormDivider />
|
|
||||||
{module && (
|
|
||||||
<ReplacementComponent
|
|
||||||
module={module}
|
|
||||||
match={new RegExp(match)}
|
|
||||||
replacement={replacement}
|
|
||||||
setReplacementError={setReplacementError}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!!(find && match && replacement) && (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle className={Margins.marginTop20}>Code</Forms.FormTitle>
|
|
||||||
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
|
||||||
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Forms.FormSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IS_DEV ? ErrorBoundary.wrap(PatchHelper) : null;
|
|
@ -16,31 +16,29 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { generateId } from "@api/Commands";
|
import { Forms } from "@components";
|
||||||
import { useSettings } from "@api/settings";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Flex } from "@components/Flex";
|
|
||||||
import { LazyComponent } from "@utils/misc";
|
|
||||||
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
|
||||||
import { proxyLazy } from "@utils/proxyLazy";
|
|
||||||
import { OptionType, Plugin } from "@utils/types";
|
|
||||||
import { findByCode, findByPropsLazy } from "@webpack";
|
|
||||||
import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
import { Constructor } from "type-fest";
|
import { Constructor } from "type-fest";
|
||||||
|
|
||||||
|
import { generateId } from "../../api/Commands";
|
||||||
|
import { useSettings } from "../../api/settings";
|
||||||
|
import { lazyWebpack, proxyLazy } from "../../utils";
|
||||||
|
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "../../utils/modal";
|
||||||
|
import { OptionType, Plugin } from "../../utils/types";
|
||||||
|
import { filters } from "../../webpack";
|
||||||
|
import { Button, FluxDispatcher, React, Text, Tooltip, UserStore, UserUtils } from "../../webpack/common";
|
||||||
|
import ErrorBoundary from "../ErrorBoundary";
|
||||||
|
import { Flex } from "../Flex";
|
||||||
import {
|
import {
|
||||||
ISettingElementProps,
|
|
||||||
SettingBooleanComponent,
|
SettingBooleanComponent,
|
||||||
SettingCustomComponent,
|
SettingInputComponent,
|
||||||
SettingNumericComponent,
|
SettingNumericComponent,
|
||||||
SettingSelectComponent,
|
SettingSelectComponent,
|
||||||
SettingSliderComponent,
|
SettingSliderComponent
|
||||||
SettingTextComponent
|
|
||||||
} from "./components";
|
} from "./components";
|
||||||
|
|
||||||
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
const UserSummaryItem = lazyWebpack(filters.byCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
|
||||||
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
const AvatarStyles = lazyWebpack(filters.byProps(["moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"]));
|
||||||
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
|
||||||
|
|
||||||
interface PluginModalProps extends ModalProps {
|
interface PluginModalProps extends ModalProps {
|
||||||
@ -62,16 +60,6 @@ function makeDummyUser(user: { name: string, id: BigInt; }) {
|
|||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any>>> = {
|
|
||||||
[OptionType.STRING]: SettingTextComponent,
|
|
||||||
[OptionType.NUMBER]: SettingNumericComponent,
|
|
||||||
[OptionType.BIGINT]: SettingNumericComponent,
|
|
||||||
[OptionType.BOOLEAN]: SettingBooleanComponent,
|
|
||||||
[OptionType.SELECT]: SettingSelectComponent,
|
|
||||||
[OptionType.SLIDER]: SettingSliderComponent,
|
|
||||||
[OptionType.COMPONENT]: SettingCustomComponent
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
||||||
const [authors, setAuthors] = React.useState<Partial<User>[]>([]);
|
const [authors, setAuthors] = React.useState<Partial<User>[]>([]);
|
||||||
|
|
||||||
@ -80,35 +68,23 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
const [tempSettings, setTempSettings] = React.useState<Record<string, any>>({});
|
const [tempSettings, setTempSettings] = React.useState<Record<string, any>>({});
|
||||||
|
|
||||||
const [errors, setErrors] = React.useState<Record<string, boolean>>({});
|
const [errors, setErrors] = React.useState<Record<string, boolean>>({});
|
||||||
const [saveError, setSaveError] = React.useState<string | null>(null);
|
|
||||||
|
|
||||||
const canSubmit = () => Object.values(errors).every(e => !e);
|
const canSubmit = () => Object.values(errors).every(e => !e);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
for (const user of plugin.authors.slice(0, 6)) {
|
for (const user of plugin.authors.slice(0, 6)) {
|
||||||
const author = user.id
|
const author = user.id ? await UserUtils.fetchUser(`${user.id}`).catch(() => null) : makeDummyUser(user);
|
||||||
? await UserUtils.fetchUser(`${user.id}`).catch(() => makeDummyUser(user))
|
setAuthors(a => [...a, author || makeDummyUser(user)]);
|
||||||
: makeDummyUser(user);
|
|
||||||
setAuthors(a => [...a, author]);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function saveAndClose() {
|
function saveAndClose() {
|
||||||
if (!plugin.options) {
|
if (!plugin.options) {
|
||||||
onClose();
|
onClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plugin.beforeSave) {
|
|
||||||
const result = await Promise.resolve(plugin.beforeSave(tempSettings));
|
|
||||||
if (result !== true) {
|
|
||||||
setSaveError(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let restartNeeded = false;
|
let restartNeeded = false;
|
||||||
for (const [key, value] of Object.entries(tempSettings)) {
|
for (const [key, value] of Object.entries(tempSettings)) {
|
||||||
const option = plugin.options[key];
|
const option = plugin.options[key];
|
||||||
@ -125,8 +101,9 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
const options: JSX.Element[] = [];
|
||||||
function onChange(newValue: any) {
|
for (const [key, setting] of Object.entries(plugin.options)) {
|
||||||
|
function onChange(newValue) {
|
||||||
setTempSettings(s => ({ ...s, [key]: newValue }));
|
setTempSettings(s => ({ ...s, [key]: newValue }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,19 +111,31 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
setErrors(e => ({ ...e, [key]: hasError }));
|
setErrors(e => ({ ...e, [key]: hasError }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const Component = Components[setting.type];
|
const props = { onChange, pluginSettings, id: key, onError };
|
||||||
return (
|
switch (setting.type) {
|
||||||
<Component
|
case OptionType.SELECT: {
|
||||||
id={key}
|
options.push(<SettingSelectComponent key={key} option={setting} {...props} />);
|
||||||
key={key}
|
break;
|
||||||
option={setting}
|
}
|
||||||
onChange={onChange}
|
case OptionType.STRING: {
|
||||||
onError={onError}
|
options.push(<SettingInputComponent key={key} option={setting} {...props} />);
|
||||||
pluginSettings={pluginSettings}
|
break;
|
||||||
/>
|
}
|
||||||
);
|
case OptionType.NUMBER:
|
||||||
});
|
case OptionType.BIGINT: {
|
||||||
|
options.push(<SettingNumericComponent key={key} option={setting} {...props} />);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OptionType.BOOLEAN: {
|
||||||
|
options.push(<SettingBooleanComponent key={key} option={setting} {...props} />);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OptionType.SLIDER: {
|
||||||
|
options.push(<SettingSliderComponent key={key} option={setting} {...props} />);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
|
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,31 +196,28 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Flex flexDirection="column" style={{ width: "100%" }}>
|
<Flex>
|
||||||
<Flex style={{ marginLeft: "auto" }}>
|
<Button
|
||||||
<Button
|
onClick={onClose}
|
||||||
onClick={onClose}
|
size={Button.Sizes.SMALL}
|
||||||
size={Button.Sizes.SMALL}
|
color={Button.Colors.RED}
|
||||||
color={Button.Colors.RED}
|
>
|
||||||
>
|
Exit Without Saving
|
||||||
Cancel
|
</Button>
|
||||||
</Button>
|
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
|
||||||
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
|
{({ onMouseEnter, onMouseLeave }) => (
|
||||||
{({ onMouseEnter, onMouseLeave }) => (
|
<Button
|
||||||
<Button
|
size={Button.Sizes.SMALL}
|
||||||
size={Button.Sizes.SMALL}
|
color={Button.Colors.BRAND}
|
||||||
color={Button.Colors.BRAND}
|
onClick={saveAndClose}
|
||||||
onClick={saveAndClose}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseLeave={onMouseLeave}
|
||||||
onMouseLeave={onMouseLeave}
|
disabled={!canSubmit()}
|
||||||
disabled={!canSubmit()}
|
>
|
||||||
>
|
Save & Exit
|
||||||
Save & Close
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
)}
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</Flex>
|
|
||||||
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
@ -16,9 +16,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PluginOptionBoolean } from "@utils/types";
|
import { Forms } from "@components";
|
||||||
import { Forms, React, Select } from "@webpack/common";
|
|
||||||
|
|
||||||
|
import { PluginOptionBoolean } from "../../../utils/types";
|
||||||
|
import { React, Select } from "../../../webpack/common";
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function SettingBooleanComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
|
export function SettingBooleanComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { PluginOptionComponent } from "@utils/types";
|
|
||||||
|
|
||||||
import { ISettingElementProps } from ".";
|
|
||||||
|
|
||||||
export function SettingCustomComponent({ option, onChange, onError }: ISettingElementProps<PluginOptionComponent>) {
|
|
||||||
return option.component({ setValue: onChange, setError: onError, option });
|
|
||||||
}
|
|
@ -16,9 +16,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { OptionType, PluginOptionNumber } from "@utils/types";
|
import { Forms } from "@components";
|
||||||
import { Forms, React, TextInput } from "@webpack/common";
|
|
||||||
|
|
||||||
|
import { OptionType, PluginOptionNumber } from "../../../utils/types";
|
||||||
|
import { React, TextInput } from "../../../webpack/common";
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
|
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||||
|
@ -16,9 +16,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PluginOptionSelect } from "@utils/types";
|
import { FormSection, FormText, FormTitle } from "@components/Forms";
|
||||||
import { Forms, React, Select } from "@webpack/common";
|
import Select from "@components/Select";
|
||||||
|
|
||||||
|
import { PluginOptionSelect } from "../../../utils/types";
|
||||||
|
import { React } from "../../../webpack/common";
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function SettingSelectComponent({ option, pluginSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
|
export function SettingSelectComponent({ option, pluginSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
|
||||||
@ -42,8 +44,8 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection>
|
<FormSection>
|
||||||
<Forms.FormTitle>{option.description}</Forms.FormTitle>
|
<FormTitle>{option.description}</FormTitle>
|
||||||
<Select
|
<Select
|
||||||
isDisabled={option.disabled?.() ?? false}
|
isDisabled={option.disabled?.() ?? false}
|
||||||
options={option.options}
|
options={option.options}
|
||||||
@ -55,7 +57,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
|
|||||||
serialize={v => String(v)}
|
serialize={v => String(v)}
|
||||||
{...option.componentProps}
|
{...option.componentProps}
|
||||||
/>
|
/>
|
||||||
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
|
{error && <FormText style={{ color: "var(--text-danger)" }}>{error}</FormText>}
|
||||||
</Forms.FormSection>
|
</FormSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PluginOptionSlider } from "@utils/types";
|
import { Forms } from "@components";
|
||||||
import { Forms, React, Slider } from "@webpack/common";
|
|
||||||
|
|
||||||
|
import { PluginOptionSlider } from "../../../utils/types";
|
||||||
|
import { React, Slider } from "../../../webpack/common";
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function makeRange(start: number, end: number, step = 1) {
|
export function makeRange(start: number, end: number, step = 1) {
|
||||||
|
@ -16,12 +16,13 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PluginOptionString } from "@utils/types";
|
import { Forms } from "@components";
|
||||||
import { Forms, React, TextInput } from "@webpack/common";
|
|
||||||
|
|
||||||
|
import { PluginOptionString } from "../../../utils/types";
|
||||||
|
import { React, TextInput } from "../../../webpack/common";
|
||||||
import { ISettingElementProps } from ".";
|
import { ISettingElementProps } from ".";
|
||||||
|
|
||||||
export function SettingTextComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
|
export function SettingInputComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
|
||||||
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
|
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PluginOptionBase } from "@utils/types";
|
import { PluginOptionBase } from "../../../utils/types";
|
||||||
|
|
||||||
export interface ISettingElementProps<T extends PluginOptionBase> {
|
export interface ISettingElementProps<T extends PluginOptionBase> {
|
||||||
option: T;
|
option: T;
|
||||||
@ -30,9 +30,7 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export * from "./SettingBooleanComponent";
|
export * from "./SettingBooleanComponent";
|
||||||
export * from "./SettingCustomComponent";
|
|
||||||
export * from "./SettingNumericComponent";
|
export * from "./SettingNumericComponent";
|
||||||
export * from "./SettingSelectComponent";
|
export * from "./SettingSelectComponent";
|
||||||
export * from "./SettingSliderComponent";
|
export * from "./SettingSliderComponent";
|
||||||
export * from "./SettingTextComponent";
|
export * from "./SettingTextComponent";
|
||||||
|
|
||||||
|
@ -16,32 +16,32 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { showNotice } from "@api/Notices";
|
import { FormDivider, FormSection, FormText, FormTitle } from "@components/Forms";
|
||||||
import { Settings, useSettings } from "@api/settings";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { ErrorCard } from "@components/ErrorCard";
|
|
||||||
import { Flex } from "@components/Flex";
|
|
||||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
|
||||||
import { ChangeList } from "@utils/ChangeList";
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import { classes, LazyComponent } 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 Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
|
import { showNotice } from "../../api/Notices";
|
||||||
|
import { Settings, useSettings } from "../../api/settings";
|
||||||
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
||||||
|
import { Logger, Modals } from "../../utils";
|
||||||
|
import { ChangeList } from "../../utils/ChangeList";
|
||||||
|
import { classes, lazyWebpack } from "../../utils/misc";
|
||||||
|
import { Plugin } from "../../utils/types";
|
||||||
|
import { filters } from "../../webpack";
|
||||||
|
import { Alerts, Button, Margins, Parser, React, Switch, Text, TextInput, Toasts, Tooltip } from "../../webpack/common";
|
||||||
|
import ErrorBoundary from "../ErrorBoundary";
|
||||||
|
import { ErrorCard } from "../ErrorCard";
|
||||||
|
import { Flex } from "../Flex";
|
||||||
import PluginModal from "./PluginModal";
|
import PluginModal from "./PluginModal";
|
||||||
import * as styles from "./styles";
|
import * as styles from "./styles";
|
||||||
|
|
||||||
const logger = new Logger("PluginSettings", "#a6d189");
|
const logger = new Logger("PluginSettings", "#a6d189");
|
||||||
|
|
||||||
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
|
const Select = lazyWebpack(filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
|
||||||
|
const InputStyles = lazyWebpack(filters.byProps(["inputDefault", "inputWrapper"]));
|
||||||
|
|
||||||
const CogWheel = LazyComponent(() => findByCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
|
const CogWheel = lazyWebpack(filters.byCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
|
||||||
const InfoIcon = LazyComponent(() => findByCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
|
const InfoIcon = lazyWebpack(filters.byCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
|
||||||
|
|
||||||
function showErrorToast(message: string) {
|
function showErrorToast(message: string) {
|
||||||
Toasts.show({
|
Toasts.show({
|
||||||
@ -91,7 +91,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openModal() {
|
function openModal() {
|
||||||
openModalLazy(async () => {
|
Modals.openModalLazy(async () => {
|
||||||
return modalProps => {
|
return modalProps => {
|
||||||
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
|
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
|
||||||
};
|
};
|
||||||
@ -147,19 +147,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
|
|||||||
onChange={toggleEnabled}
|
onChange={toggleEnabled}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={isEnabled()}
|
value={isEnabled()}
|
||||||
note={<Text variant="text-md/normal" style={{
|
note={<Text variant="text-md/normal" style={{ height: 40, overflow: "hidden" }}>{plugin.description}</Text>}
|
||||||
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}
|
hideBorder={true}
|
||||||
>
|
>
|
||||||
<Flex style={{ marginTop: "auto", width: "100%", height: "100%", alignItems: "center" }}>
|
<Flex style={{ marginTop: "auto", width: "100%", height: "100%", alignItems: "center" }}>
|
||||||
@ -222,6 +210,11 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||||||
return o;
|
return o;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
function hasDependents(plugin: Plugin) {
|
||||||
|
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
|
||||||
|
return !!enabledDependants?.length;
|
||||||
|
}
|
||||||
|
|
||||||
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
|
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
|
||||||
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
||||||
|
|
||||||
@ -244,10 +237,10 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection>
|
<FormSection tag="h1" title="Vencord">
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
<FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
Filters
|
Plugins
|
||||||
</Forms.FormTitle>
|
</FormTitle>
|
||||||
|
|
||||||
<ReloadRequiredCard plugins={[...changes.getChanges()]} style={{ marginBottom: 16 }} />
|
<ReloadRequiredCard plugins={[...changes.getChanges()]} style={{ marginBottom: 16 }} />
|
||||||
|
|
||||||
@ -261,7 +254,7 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||||||
{ label: "Show Enabled", value: "enabled" },
|
{ label: "Show Enabled", value: "enabled" },
|
||||||
{ label: "Show Disabled", value: "disabled" }
|
{ label: "Show Disabled", value: "disabled" }
|
||||||
]}
|
]}
|
||||||
serialize={String}
|
serialize={v => String(v)}
|
||||||
select={onStatusChange}
|
select={onStatusChange}
|
||||||
isSelected={v => v === searchValue.status}
|
isSelected={v => v === searchValue.status}
|
||||||
closeOnSelect={true}
|
closeOnSelect={true}
|
||||||
@ -269,8 +262,6 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
|
|
||||||
|
|
||||||
<div style={styles.PluginsGrid}>
|
<div style={styles.PluginsGrid}>
|
||||||
{sortedPlugins?.length ? sortedPlugins
|
{sortedPlugins?.length ? sortedPlugins
|
||||||
.filter(a => !a.required && !dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
.filter(a => !a.required && !dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
||||||
@ -281,16 +272,15 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||||||
onRestartNeeded={name => changes.add(name)}
|
onRestartNeeded={name => changes.add(name)}
|
||||||
disabled={plugin.required || !!dependency}
|
disabled={plugin.required || !!dependency}
|
||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
key={plugin.name}
|
|
||||||
/>;
|
/>;
|
||||||
})
|
})
|
||||||
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<Forms.FormDivider />
|
<FormDivider />
|
||||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
<FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||||
Required Plugins
|
Required Plugins
|
||||||
</Forms.FormTitle>
|
</FormTitle>
|
||||||
<div style={styles.PluginsGrid}>
|
<div style={styles.PluginsGrid}>
|
||||||
{sortedPlugins?.length ? sortedPlugins
|
{sortedPlugins?.length ? sortedPlugins
|
||||||
.filter(a => a.required || dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
.filter(a => a.required || dependencyCheck(a.name, depMap).length && pluginFilter(a))
|
||||||
@ -300,12 +290,12 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||||||
const tooltipText = plugin.required
|
const tooltipText = plugin.required
|
||||||
? "This plugin is required for Vencord to function."
|
? "This plugin is required for Vencord to function."
|
||||||
: makeDependencyList(dependencyCheck(plugin.name, depMap));
|
: makeDependencyList(dependencyCheck(plugin.name, depMap));
|
||||||
return <Tooltip text={tooltipText} key={plugin.name}>
|
return <Tooltip text={tooltipText}>
|
||||||
{({ onMouseLeave, onMouseEnter }) => (
|
{({ onMouseLeave, onMouseEnter }) => (
|
||||||
<PluginCard
|
<PluginCard
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onRestartNeeded={name => changes.handleChange(name)}
|
onRestartNeeded={name => changes.add(name)}
|
||||||
disabled={plugin.required || !!dependency}
|
disabled={plugin.required || !!dependency}
|
||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
/>
|
/>
|
||||||
@ -315,18 +305,15 @@ export default ErrorBoundary.wrap(function Settings() {
|
|||||||
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Forms.FormSection >
|
</FormSection>
|
||||||
);
|
);
|
||||||
}, {
|
|
||||||
message: "Failed to render the Plugin Settings. If this persists, try using the installer to reinstall!",
|
|
||||||
onError: handleComponentFailed,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function makeDependencyList(deps: string[]) {
|
function makeDependencyList(deps: string[]) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Forms.FormText>This plugin is required by:</Forms.FormText>
|
<FormText>This plugin is required by:</FormText>
|
||||||
{deps.map((dep: string) => <Forms.FormText style={{ margin: "0 auto" }}>{dep}</Forms.FormText>)}
|
{deps.map((dep: string) => <FormText style={{ margin: "0 auto" }}>{dep}</FormText>)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
121
src/components/Settings.tsx
Normal file
121
src/components/Settings.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FormDivider, FormSection, FormText, FormTitle } from "@components/Forms";
|
||||||
|
|
||||||
|
import { useSettings } from "../api/settings";
|
||||||
|
import { ChangeList } from "../utils/ChangeList";
|
||||||
|
import IpcEvents from "../utils/IpcEvents";
|
||||||
|
import { useAwaiter } from "../utils/misc";
|
||||||
|
import { Alerts, Button, Margins, Parser, React, Switch } from "../webpack/common";
|
||||||
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
|
import { Flex } from "./Flex";
|
||||||
|
import { launchMonacoEditor } from "./Monaco";
|
||||||
|
|
||||||
|
export default ErrorBoundary.wrap(function Settings() {
|
||||||
|
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading...");
|
||||||
|
const settings = useSettings();
|
||||||
|
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => void (changes.hasChanges && Alerts.show({
|
||||||
|
title: "Restart required",
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<p>The following plugins require a restart:</p>
|
||||||
|
<div>{changes.map((s, i) => (
|
||||||
|
<>
|
||||||
|
{i > 0 && ", "}
|
||||||
|
{Parser.parse("`" + s + "`")}
|
||||||
|
</>
|
||||||
|
))}</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
confirmText: "Restart now",
|
||||||
|
cancelText: "Later!",
|
||||||
|
onConfirm: () => location.reload()
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormSection tag="h1" title="Vencord">
|
||||||
|
<FormTitle tag="h5">
|
||||||
|
Settings
|
||||||
|
</FormTitle>
|
||||||
|
|
||||||
|
<FormText>
|
||||||
|
Settings Directory: <code style={{ userSelect: "text", cursor: "text" }}>{settingsDir}</code>
|
||||||
|
</FormText>
|
||||||
|
|
||||||
|
{!IS_WEB && <Flex className={Margins.marginBottom20} style={{ marginTop: 8 }}>
|
||||||
|
<Button
|
||||||
|
onClick={() => window.DiscordNative.app.relaunch()}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
color={Button.Colors.GREEN}
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={settingsDirPending}
|
||||||
|
>
|
||||||
|
Launch Directory
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_MONACO_EDITOR)}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={settingsDir === "Loading..."}
|
||||||
|
>
|
||||||
|
Open QuickCSS File
|
||||||
|
</Button>
|
||||||
|
</Flex>}
|
||||||
|
|
||||||
|
{IS_WEB && <Button
|
||||||
|
onClick={launchMonacoEditor}
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
disabled={settingsDir === "Loading..."}
|
||||||
|
>
|
||||||
|
Open QuickCSS File
|
||||||
|
</Button>}
|
||||||
|
|
||||||
|
<FormDivider />
|
||||||
|
<Switch
|
||||||
|
value={settings.useQuickCss}
|
||||||
|
onChange={(v: boolean) => settings.useQuickCss = v}
|
||||||
|
note="Loads styles from your QuickCss file"
|
||||||
|
>
|
||||||
|
Use QuickCss
|
||||||
|
</Switch>
|
||||||
|
{!IS_WEB && <Switch
|
||||||
|
value={settings.enableReactDevtools}
|
||||||
|
onChange={(v: boolean) => settings.enableReactDevtools = v}
|
||||||
|
note="Requires a full restart"
|
||||||
|
>
|
||||||
|
Enable React Developer Tools
|
||||||
|
</Switch>}
|
||||||
|
{!IS_WEB && <Switch
|
||||||
|
value={settings.notifyAboutUpdates}
|
||||||
|
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||||
|
note="Shows a Toast on StartUp"
|
||||||
|
>
|
||||||
|
Get notified about new Updates
|
||||||
|
</Switch>}
|
||||||
|
</FormSection>
|
||||||
|
);
|
||||||
|
});
|
@ -16,17 +16,18 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import { Forms } from "@components";
|
||||||
import { ErrorCard } from "@components/ErrorCard";
|
|
||||||
import { Flex } from "@components/Flex";
|
|
||||||
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 gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
|
import { classes, useAwaiter } from "../utils/misc";
|
||||||
|
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "../utils/updater";
|
||||||
|
import { Alerts, Button, Card, Margins, Parser, React, Toasts } from "../webpack/common";
|
||||||
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
|
import { ErrorCard } from "./ErrorCard";
|
||||||
|
import { Flex } from "./Flex";
|
||||||
|
import { Link } from "./Link";
|
||||||
|
|
||||||
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
||||||
return async () => {
|
return async () => {
|
||||||
dispatcher(true);
|
dispatcher(true);
|
||||||
@ -179,7 +180,7 @@ function Newer(props: CommonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Updater() {
|
function Updater() {
|
||||||
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
const [repo, err, repoPending] = useAwaiter(getRepo, "Loading...");
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (err)
|
if (err)
|
||||||
@ -192,7 +193,7 @@ function Updater() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection>
|
<Forms.FormSection tag="h1" title="Vencord Updater">
|
||||||
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
|
||||||
|
|
||||||
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
|
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
|
||||||
@ -210,7 +211,4 @@ function Updater() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IS_WEB ? null : ErrorBoundary.wrap(Updater, {
|
export default IS_WEB ? null : ErrorBoundary.wrap(Updater);
|
||||||
message: "Failed to render the Updater. If this persists, try using the installer to reinstall!",
|
|
||||||
onError: handleComponentFailed,
|
|
||||||
});
|
|
@ -1,69 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Flex } from "@components/Flex";
|
|
||||||
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",
|
|
||||||
}}>
|
|
||||||
<Flex flexDirection="column">
|
|
||||||
<strong>Warning</strong>
|
|
||||||
<span>Importing a settings file will overwrite your current settings.</span>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
|
||||||
You can import and export your Vencord settings as a JSON file.
|
|
||||||
This allows you to easily transfer your settings to another device,
|
|
||||||
or recover your settings after reinstalling Vencord or Discord.
|
|
||||||
</Text>
|
|
||||||
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
|
||||||
Settings Export contains:
|
|
||||||
<ul>
|
|
||||||
<li>— Custom QuickCSS</li>
|
|
||||||
<li>— Plugin Settings</li>
|
|
||||||
</ul>
|
|
||||||
</Text>
|
|
||||||
<Flex>
|
|
||||||
<Button
|
|
||||||
onClick={uploadSettingsBackup}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
>
|
|
||||||
Import Settings
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={downloadSettingsBackup}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
>
|
|
||||||
Export Settings
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</Forms.FormSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ErrorBoundary.wrap(BackupRestoreTab);
|
|
@ -1,135 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useSettings } from "@api/settings";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Link } from "@components/Link";
|
|
||||||
import { useAwaiter } from "@utils/misc";
|
|
||||||
import { findLazy } from "@webpack";
|
|
||||||
import { Card, Forms, Margins, React, TextArea } from "@webpack/common";
|
|
||||||
|
|
||||||
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
|
||||||
|
|
||||||
function Validator({ link }: { link: string; }) {
|
|
||||||
const [res, err, pending] = useAwaiter(() => fetch(link).then(res => {
|
|
||||||
if (res.status > 300) throw `${res.status} ${res.statusText}`;
|
|
||||||
const contentType = res.headers.get("Content-Type");
|
|
||||||
if (!contentType?.startsWith("text/css") && !contentType?.startsWith("text/plain"))
|
|
||||||
throw "Not a CSS file. Remember to use the raw link!";
|
|
||||||
|
|
||||||
return "Okay!";
|
|
||||||
}));
|
|
||||||
|
|
||||||
const text = pending
|
|
||||||
? "Checking..."
|
|
||||||
: err
|
|
||||||
? `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
||||||
: "Valid!";
|
|
||||||
|
|
||||||
return <Forms.FormText style={{
|
|
||||||
color: pending ? "var(--text-muted)" : err ? "var(--text-danger)" : "var(--text-positive)"
|
|
||||||
}}>{text}</Forms.FormText>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
|
||||||
if (!themeLinks.length) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle className={Margins.marginTop20} tag="h5">Validator</Forms.FormTitle>
|
|
||||||
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
|
||||||
<div>
|
|
||||||
{themeLinks.map(link => (
|
|
||||||
<Card style={{
|
|
||||||
padding: ".5em",
|
|
||||||
marginBottom: ".5em"
|
|
||||||
}} key={link}>
|
|
||||||
<Forms.FormTitle tag="h5" style={{
|
|
||||||
overflowWrap: "break-word"
|
|
||||||
}}>
|
|
||||||
{link}
|
|
||||||
</Forms.FormTitle>
|
|
||||||
<Validator link={link} />
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ErrorBoundary.wrap(function () {
|
|
||||||
const settings = useSettings();
|
|
||||||
const ref = React.useRef<HTMLTextAreaElement>();
|
|
||||||
|
|
||||||
function onBlur() {
|
|
||||||
settings.themeLinks = [...new Set(
|
|
||||||
ref.current!.value
|
|
||||||
.trim()
|
|
||||||
.split(/\n+/)
|
|
||||||
.map(s => s.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card style={{
|
|
||||||
padding: "1em",
|
|
||||||
marginBottom: "1em",
|
|
||||||
marginTop: "1em"
|
|
||||||
}}>
|
|
||||||
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
|
|
||||||
<Forms.FormText>One link per line</Forms.FormText>
|
|
||||||
<Forms.FormText>Be careful to use the raw links or github.io links!</Forms.FormText>
|
|
||||||
<Forms.FormDivider />
|
|
||||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
|
||||||
<div>
|
|
||||||
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
|
||||||
BetterDiscord Themes
|
|
||||||
</Link>
|
|
||||||
<Link href="https://github.com/search?q=discord+theme">Github</Link>
|
|
||||||
</div>
|
|
||||||
<Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText>
|
|
||||||
<Forms.FormText>In the GitHub repository of your theme, find X.theme.css / X.css, click on it, then click the "Raw" button</Forms.FormText>
|
|
||||||
<Forms.FormText>
|
|
||||||
If the theme has configuration that requires you to edit the file:
|
|
||||||
<ul>
|
|
||||||
<li>• Make a github account</li>
|
|
||||||
<li>• Click the fork button on the top right</li>
|
|
||||||
<li>• Edit the file</li>
|
|
||||||
<li>• Use the link to your own repository instead</li>
|
|
||||||
</ul>
|
|
||||||
</Forms.FormText>
|
|
||||||
</Card>
|
|
||||||
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
|
|
||||||
<TextArea
|
|
||||||
style={{
|
|
||||||
padding: ".5em",
|
|
||||||
border: "1px solid var(--background-modifier-accent)"
|
|
||||||
}}
|
|
||||||
ref={ref}
|
|
||||||
defaultValue={settings.themeLinks.join("\n")}
|
|
||||||
className={TextAreaProps.textarea}
|
|
||||||
placeholder="Theme Links"
|
|
||||||
spellCheck={false}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
<Validators themeLinks={settings.themeLinks} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,149 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import { useSettings } from "@api/settings";
|
|
||||||
import DonateButton from "@components/DonateButton";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
|
||||||
import { useAwaiter } from "@utils/misc";
|
|
||||||
import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common";
|
|
||||||
|
|
||||||
const st = (style: string) => `vcSettings${style}`;
|
|
||||||
|
|
||||||
function VencordSettings() {
|
|
||||||
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
|
|
||||||
fallbackValue: "Loading..."
|
|
||||||
});
|
|
||||||
const settings = useSettings();
|
|
||||||
|
|
||||||
const [donateImage] = React.useState(
|
|
||||||
Math.random() > 0.5
|
|
||||||
? "https://cdn.discordapp.com/emojis/1026533090627174460.png"
|
|
||||||
: "https://media.discordapp.net/stickers/1039992459209490513.png"
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<DonateCard image={donateImage} />
|
|
||||||
<Forms.FormSection title="Quick Actions">
|
|
||||||
<Card className={st("QuickActionCard")}>
|
|
||||||
{IS_WEB ? (
|
|
||||||
<Button
|
|
||||||
onClick={() => require("../Monaco").launchMonacoEditor()}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
disabled={settingsDir === "Loading..."}>
|
|
||||||
Open QuickCSS File
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<React.Fragment>
|
|
||||||
<Button
|
|
||||||
onClick={() => window.DiscordNative.app.relaunch()}
|
|
||||||
size={Button.Sizes.SMALL}>
|
|
||||||
Restart Client
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_MONACO_EDITOR)}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
disabled={settingsDir === "Loading..."}>
|
|
||||||
Open QuickCSS File
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
disabled={settingsDirPending}>
|
|
||||||
Open Settings Folder
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/Vendicated/Vencord")}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
disabled={settingsDirPending}>
|
|
||||||
Open in GitHub
|
|
||||||
</Button>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Forms.FormSection>
|
|
||||||
|
|
||||||
<Forms.FormDivider />
|
|
||||||
|
|
||||||
<Forms.FormSection title="Settings">
|
|
||||||
<Forms.FormText className={Margins.marginBottom20}>
|
|
||||||
Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
|
|
||||||
</Forms.FormText>
|
|
||||||
<Switch
|
|
||||||
value={settings.useQuickCss}
|
|
||||||
onChange={(v: boolean) => settings.useQuickCss = v}
|
|
||||||
note="Loads styles from your QuickCss file">
|
|
||||||
Use QuickCss
|
|
||||||
</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>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface DonateCardProps {
|
|
||||||
image: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DonateCard({ image }: DonateCardProps) {
|
|
||||||
return (
|
|
||||||
<Card style={{
|
|
||||||
padding: "1em",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
marginBottom: "1em",
|
|
||||||
marginTop: "1em"
|
|
||||||
}}>
|
|
||||||
<div>
|
|
||||||
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle>
|
|
||||||
<Forms.FormText>
|
|
||||||
Please consider supporting the Development of Vencord by donating!
|
|
||||||
</Forms.FormText>
|
|
||||||
<DonateButton style={{ transform: "translateX(-1em)" }} />
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
role="presentation"
|
|
||||||
src={image}
|
|
||||||
alt=""
|
|
||||||
height={128}
|
|
||||||
style={{ marginLeft: "auto", transform: "rotate(10deg)" }}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ErrorBoundary.wrap(VencordSettings);
|
|
@ -1,92 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { findByCodeLazy } from "@webpack";
|
|
||||||
import { Forms, Router, Text } from "@webpack/common";
|
|
||||||
|
|
||||||
import cssText from "~fileContent/settingsStyles.css";
|
|
||||||
|
|
||||||
import BackupRestoreTab from "./BackupRestoreTab";
|
|
||||||
import PluginsTab from "./PluginsTab";
|
|
||||||
import ThemesTab from "./ThemesTab";
|
|
||||||
import Updater from "./Updater";
|
|
||||||
import VencordSettings from "./VencordTab";
|
|
||||||
|
|
||||||
const style = document.createElement("style");
|
|
||||||
style.textContent = cssText;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
|
|
||||||
const st = (style: string) => `vcSettings${style}`;
|
|
||||||
|
|
||||||
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');
|
|
||||||
|
|
||||||
interface SettingsProps {
|
|
||||||
tab: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SettingsTab {
|
|
||||||
name: string;
|
|
||||||
component?: React.ComponentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsTabs: Record<string, SettingsTab> = {
|
|
||||||
VencordSettings: { name: "Vencord", component: () => <VencordSettings /> },
|
|
||||||
VencordPlugins: { name: "Plugins", component: () => <PluginsTab /> },
|
|
||||||
VencordThemes: { name: "Themes", component: () => <ThemesTab /> },
|
|
||||||
VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false
|
|
||||||
VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater />;
|
|
||||||
|
|
||||||
function Settings(props: SettingsProps) {
|
|
||||||
const { tab = "VencordSettings" } = props;
|
|
||||||
|
|
||||||
const CurrentTab = SettingsTabs[tab]?.component;
|
|
||||||
|
|
||||||
return <Forms.FormSection>
|
|
||||||
<Text variant="heading-md/normal" tag="h2">Vencord Settings</Text>
|
|
||||||
|
|
||||||
<TabBar
|
|
||||||
type={TabBar.Types.TOP}
|
|
||||||
look={TabBar.Looks.BRAND}
|
|
||||||
className={st("TabBar")}
|
|
||||||
selectedItem={tab}
|
|
||||||
onItemSelect={Router.open}
|
|
||||||
>
|
|
||||||
{Object.entries(SettingsTabs).map(([key, { name, component }]) => {
|
|
||||||
if (!component) return null;
|
|
||||||
return <TabBar.Item
|
|
||||||
id={key}
|
|
||||||
className={st("TabBarItem")}
|
|
||||||
key={key}>
|
|
||||||
{name}
|
|
||||||
</TabBar.Item>;
|
|
||||||
})}
|
|
||||||
</TabBar>
|
|
||||||
<Forms.FormDivider />
|
|
||||||
{CurrentTab && <CurrentTab />}
|
|
||||||
</Forms.FormSection >;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function (props: SettingsProps) {
|
|
||||||
return <ErrorBoundary>
|
|
||||||
<Settings tab={props.tab} />
|
|
||||||
</ErrorBoundary>;
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
.vcSettingsTabBar {
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-bottom: -2px;
|
|
||||||
border-bottom: 2px solid var(--background-modifier-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vcSettingsTabBarItem {
|
|
||||||
margin-right: 32px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
margin-bottom: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vcSettingsQuickActionCard {
|
|
||||||
padding: 1em;
|
|
||||||
display: flex;
|
|
||||||
gap: 1em;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { isOutdated, rebuild, update } from "@utils/updater";
|
|
||||||
|
|
||||||
export async function handleComponentFailed() {
|
|
||||||
if (isOutdated) {
|
|
||||||
setImmediate(async () => {
|
|
||||||
const wantsUpdate = confirm(
|
|
||||||
"Uh Oh! Failed to render this Page." +
|
|
||||||
" However, there is an update available that might fix it." +
|
|
||||||
" Would you like to update and restart now?"
|
|
||||||
);
|
|
||||||
if (wantsUpdate) {
|
|
||||||
try {
|
|
||||||
await update();
|
|
||||||
await rebuild();
|
|
||||||
if (IS_WEB)
|
|
||||||
location.reload();
|
|
||||||
else
|
|
||||||
DiscordNative.app.relaunch();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
alert("That also failed :( Try updating or reinstalling with the installer!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -16,6 +16,6 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { default as PatchHelper } from "./PatchHelper";
|
|
||||||
export { default as PluginSettings } from "./PluginSettings";
|
export { default as PluginSettings } from "./PluginSettings";
|
||||||
export { default as VencordSettings } from "./VencordSettings";
|
export { default as Settings } from "./Settings";
|
||||||
|
export { default as Updater } from "./Updater";
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
|
|
||||||
if (IS_DEV) {
|
|
||||||
var traces = {} as Record<string, [number, any[]]>;
|
|
||||||
var logger = new Logger("Tracer", "#FFD166");
|
|
||||||
}
|
|
||||||
|
|
||||||
const noop = function () { };
|
|
||||||
|
|
||||||
export const beginTrace = !IS_DEV ? noop :
|
|
||||||
function beginTrace(name: string, ...args: any[]) {
|
|
||||||
if (name in traces)
|
|
||||||
throw new Error(`Trace ${name} already exists!`);
|
|
||||||
|
|
||||||
traces[name] = [performance.now(), args];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const finishTrace = !IS_DEV ? noop : function finishTrace(name: string) {
|
|
||||||
const end = performance.now();
|
|
||||||
|
|
||||||
const [start, args] = traces[name];
|
|
||||||
delete traces[name];
|
|
||||||
|
|
||||||
logger.debug(`${name} took ${end - start}ms`, args);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Func = (...args: any[]) => any;
|
|
||||||
type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;
|
|
||||||
|
|
||||||
const noopTracer =
|
|
||||||
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
|
|
||||||
|
|
||||||
export const traceFunction = !IS_DEV
|
|
||||||
? noopTracer
|
|
||||||
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
|
|
||||||
return function (this: any, ...args: Parameters<F>) {
|
|
||||||
const traceName = mapper?.(...args) ?? name;
|
|
||||||
|
|
||||||
beginTrace(traceName, ...arguments);
|
|
||||||
try {
|
|
||||||
return f.apply(this, args);
|
|
||||||
} finally {
|
|
||||||
finishTrace(traceName);
|
|
||||||
}
|
|
||||||
} as F;
|
|
||||||
};
|
|
4
src/globals.d.ts
vendored
4
src/globals.d.ts
vendored
@ -16,12 +16,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
/**
|
/**
|
||||||
* This exists only at build time, so references to it in patches should insert it
|
* This exists only at build time, so references to it in patches should insert it
|
||||||
* via String interpolation OR use different replacement code based on this
|
* via String interpolation OR use different replacement code based on this
|
||||||
* but NEVER reference it inside the patched code
|
* but NEVER refrence it inside the patched code
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // BAD
|
* // BAD
|
||||||
@ -32,7 +31,6 @@ declare global {
|
|||||||
* replace: `${IS_WEB}?foo:bar`
|
* replace: `${IS_WEB}?foo:bar`
|
||||||
*/
|
*/
|
||||||
export var IS_WEB: boolean;
|
export var IS_WEB: boolean;
|
||||||
export var IS_DEV: boolean;
|
|
||||||
export var IS_STANDALONE: boolean;
|
export var IS_STANDALONE: boolean;
|
||||||
|
|
||||||
export var VencordNative: typeof import("./VencordNative").default;
|
export var VencordNative: typeof import("./VencordNative").default;
|
||||||
|
@ -18,9 +18,6 @@
|
|||||||
|
|
||||||
import "./updater";
|
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, desktopCapturer, ipcMain, shell } from "electron";
|
||||||
import { mkdirSync, readFileSync, watch } from "fs";
|
import { mkdirSync, readFileSync, watch } from "fs";
|
||||||
import { open, readFile, writeFile } from "fs/promises";
|
import { open, readFile, writeFile } from "fs/promises";
|
||||||
@ -28,6 +25,9 @@ import { join } from "path";
|
|||||||
|
|
||||||
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
|
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
|
||||||
|
|
||||||
|
import { debounce } from "../utils/debounce";
|
||||||
|
import IpcEvents from "../utils/IpcEvents";
|
||||||
|
import { Queue } from "../utils/Queue";
|
||||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
|
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
|
||||||
|
|
||||||
mkdirSync(SETTINGS_DIR, { recursive: true });
|
mkdirSync(SETTINGS_DIR, { recursive: true });
|
||||||
@ -66,14 +66,14 @@ const settingsWriteQueue = new Queue();
|
|||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
||||||
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
|
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
|
||||||
cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css))
|
cssWriteQueue.add(() => writeFile(QUICKCSS_PATH, css))
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
||||||
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
|
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
|
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
|
||||||
settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s));
|
settingsWriteQueue.add(() => writeFile(SETTINGS_FILE, s));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -89,12 +89,8 @@ export function initIpc(mainWindow: BrowserWindow) {
|
|||||||
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
title: "QuickCss Editor",
|
title: "QuickCss Editor",
|
||||||
autoHideMenuBar: true,
|
|
||||||
darkTheme: true,
|
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, "preload.js"),
|
preload: join(__dirname, "preload.js"),
|
||||||
contextIsolation: true,
|
|
||||||
nodeIntegration: false
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
|
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
|
||||||
|
@ -16,25 +16,22 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
|
||||||
import { execFile as cpExecFile } from "child_process";
|
import { execFile as cpExecFile } from "child_process";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
import IpcEvents from "../../utils/IpcEvents";
|
||||||
import { calculateHashes, serializeErrors } from "./common";
|
import { calculateHashes, serializeErrors } from "./common";
|
||||||
|
|
||||||
const VENCORD_SRC_DIR = join(__dirname, "..");
|
const VENCORD_SRC_DIR = join(__dirname, "..");
|
||||||
|
|
||||||
const execFile = promisify(cpExecFile);
|
const execFile = promisify(cpExecFile);
|
||||||
|
|
||||||
const isFlatpak = Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
|
|
||||||
|
|
||||||
function git(...args: string[]) {
|
function git(...args: string[]) {
|
||||||
const opts = { cwd: VENCORD_SRC_DIR };
|
return execFile("git", args, {
|
||||||
|
cwd: VENCORD_SRC_DIR
|
||||||
if (isFlatpak) return execFile("flatpak-spawn", ["--host", "git", ...args], opts);
|
});
|
||||||
else return execFile("git", args, opts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRepo() {
|
async function getRepo() {
|
||||||
@ -64,13 +61,9 @@ async function pull() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function build() {
|
async function build() {
|
||||||
const opts = { cwd: VENCORD_SRC_DIR };
|
const res = await execFile("node", ["scripts/build/build.mjs"], {
|
||||||
|
cwd: VENCORD_SRC_DIR
|
||||||
let res;
|
});
|
||||||
|
|
||||||
if (isFlatpak) res = await execFile("flatpak-spawn", ["--host", "node", "scripts/build/build.mjs"], opts);
|
|
||||||
else res = await execFile("node", ["scripts/build/build.mjs"], opts);
|
|
||||||
|
|
||||||
return !res.stderr.includes("Build failed");
|
return !res.stderr.includes("Build failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,8 +16,6 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { VENCORD_USER_AGENT } from "@utils/constants";
|
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { writeFile } from "fs/promises";
|
import { writeFile } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
@ -25,11 +23,13 @@ import { join } from "path";
|
|||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
import gitRemote from "~git-remote";
|
import gitRemote from "~git-remote";
|
||||||
|
|
||||||
|
import { VENCORD_USER_AGENT } from "../../utils/constants";
|
||||||
|
import IpcEvents from "../../utils/IpcEvents";
|
||||||
import { get } from "../simpleGet";
|
import { get } from "../simpleGet";
|
||||||
import { calculateHashes, serializeErrors } from "./common";
|
import { calculateHashes, serializeErrors } from "./common";
|
||||||
|
|
||||||
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
|
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
|
||||||
let PendingUpdates = [] as [string, string][];
|
let PendingUpdates = [] as [string, Buffer][];
|
||||||
|
|
||||||
async function githubGet(endpoint: string) {
|
async function githubGet(endpoint: string) {
|
||||||
return get(API_BASE + endpoint, {
|
return get(API_BASE + endpoint, {
|
||||||
@ -46,9 +46,6 @@ async function githubGet(endpoint: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function calculateGitChanges() {
|
async function calculateGitChanges() {
|
||||||
const isOutdated = await fetchUpdates();
|
|
||||||
if (!isOutdated) return [];
|
|
||||||
|
|
||||||
const res = await githubGet(`/compare/${gitHash}...HEAD`);
|
const res = await githubGet(`/compare/${gitHash}...HEAD`);
|
||||||
|
|
||||||
const data = JSON.parse(res.toString("utf-8"));
|
const data = JSON.parse(res.toString("utf-8"));
|
||||||
@ -66,20 +63,18 @@ async function fetchUpdates() {
|
|||||||
const data = JSON.parse(release.toString());
|
const data = JSON.parse(release.toString());
|
||||||
const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
|
const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
|
||||||
if (hash === gitHash)
|
if (hash === gitHash)
|
||||||
return false;
|
return true;
|
||||||
|
|
||||||
data.assets.forEach(({ name, browser_download_url }) => {
|
await Promise.all(data.assets.map(async ({ name, browser_download_url }) => {
|
||||||
if (["patcher.js", "preload.js", "renderer.js"].some(s => name.startsWith(s))) {
|
if (["patcher.js", "preload.js", "renderer.js"].some(s => name.startsWith(s))) {
|
||||||
PendingUpdates.push([name, browser_download_url]);
|
PendingUpdates.push([name, await get(browser_download_url)]);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyUpdates() {
|
async function applyUpdates() {
|
||||||
await Promise.all(PendingUpdates.map(
|
await Promise.all(PendingUpdates.map(([name, data]) => writeFile(join(__dirname, name), data)));
|
||||||
async ([name, data]) => writeFile(join(__dirname, name), await get(data)))
|
|
||||||
);
|
|
||||||
PendingUpdates = [];
|
PendingUpdates = [];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
2
src/modules.d.ts
vendored
2
src/modules.d.ts
vendored
@ -20,7 +20,7 @@
|
|||||||
/// <reference types="standalone-electron-types"/>
|
/// <reference types="standalone-electron-types"/>
|
||||||
|
|
||||||
declare module "~plugins" {
|
declare module "~plugins" {
|
||||||
const plugins: Record<string, import("@utils/types").Plugin>;
|
const plugins: Record<string, import("./utils/types").Plugin>;
|
||||||
export default plugins;
|
export default plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
194
src/patcher.ts
194
src/patcher.ts
@ -16,7 +16,6 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { onceDefined } from "@utils/onceDefined";
|
|
||||||
import electron, { app, BrowserWindowConstructorOptions } from "electron";
|
import electron, { app, BrowserWindowConstructorOptions } from "electron";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { dirname, join } from "path";
|
import { dirname, join } from "path";
|
||||||
@ -31,7 +30,7 @@ console.log("[Vencord] Starting up...");
|
|||||||
const injectorPath = require.main!.filename;
|
const injectorPath = require.main!.filename;
|
||||||
|
|
||||||
// special discord_arch_electron injection method
|
// special discord_arch_electron injection method
|
||||||
const asarName = require.main!.path.endsWith("app.asar") ? "_app.asar" : "app.asar";
|
const asarName = injectorPath.endsWith("app.asar/index.js") ? "_app.asar" : "app.asar";
|
||||||
|
|
||||||
// The original app.asar
|
// The original app.asar
|
||||||
const asarPath = join(dirname(injectorPath), "..", asarName);
|
const asarPath = join(dirname(injectorPath), "..", asarName);
|
||||||
@ -42,122 +41,90 @@ require.main!.filename = join(asarPath, discordPkg.main);
|
|||||||
// @ts-ignore Untyped method? Dies from cringe
|
// @ts-ignore Untyped method? Dies from cringe
|
||||||
app.setAppPath(asarPath);
|
app.setAppPath(asarPath);
|
||||||
|
|
||||||
if (!process.argv.includes("--vanilla")) {
|
// Repatch after host updates on Windows
|
||||||
// Repatch after host updates on Windows
|
if (process.platform === "win32")
|
||||||
if (process.platform === "win32")
|
require("./patchWin32Updater");
|
||||||
require("./patchWin32Updater");
|
|
||||||
|
|
||||||
class BrowserWindow extends electron.BrowserWindow {
|
class BrowserWindow extends electron.BrowserWindow {
|
||||||
constructor(options: BrowserWindowConstructorOptions) {
|
constructor(options: BrowserWindowConstructorOptions) {
|
||||||
if (options?.webPreferences?.preload && options.title) {
|
if (options?.webPreferences?.preload && options.title) {
|
||||||
const original = options.webPreferences.preload;
|
const original = options.webPreferences.preload;
|
||||||
options.webPreferences.preload = join(__dirname, "preload.js");
|
options.webPreferences.preload = join(__dirname, "preload.js");
|
||||||
options.webPreferences.sandbox = false;
|
options.webPreferences.sandbox = false;
|
||||||
|
|
||||||
process.env.DISCORD_PRELOAD = original;
|
process.env.DISCORD_PRELOAD = original;
|
||||||
|
|
||||||
super(options);
|
super(options);
|
||||||
initIpc(this);
|
initIpc(this);
|
||||||
} else super(options);
|
} else super(options);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Object.assign(BrowserWindow, electron.BrowserWindow);
|
|
||||||
// esbuild may rename our BrowserWindow, which leads to it being excluded
|
|
||||||
// from getFocusedWindow(), so this is necessary
|
|
||||||
// https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62
|
|
||||||
Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true });
|
|
||||||
|
|
||||||
// Replace electrons exports with our custom BrowserWindow
|
|
||||||
const electronPath = require.resolve("electron");
|
|
||||||
delete require.cache[electronPath]!.exports;
|
|
||||||
require.cache[electronPath]!.exports = {
|
|
||||||
...electron,
|
|
||||||
BrowserWindow
|
|
||||||
};
|
|
||||||
|
|
||||||
// Patch appSettings to force enable devtools
|
|
||||||
onceDefined(global, "appSettings", s =>
|
|
||||||
s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true)
|
|
||||||
);
|
|
||||||
|
|
||||||
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
|
||||||
|
|
||||||
electron.app.whenReady().then(() => {
|
|
||||||
// Source Maps! Maybe there's a better way but since the renderer is executed
|
|
||||||
// from a string I don't think any other form of sourcemaps would work
|
|
||||||
electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
|
||||||
let url = unsafeUrl.slice("vencord://".length);
|
|
||||||
if (url.endsWith("/")) url = url.slice(0, -1);
|
|
||||||
switch (url) {
|
|
||||||
case "renderer.js.map":
|
|
||||||
case "preload.js.map":
|
|
||||||
case "patcher.js.map": // doubt
|
|
||||||
cb(join(__dirname, url));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
cb({ statusCode: 403 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const settings = JSON.parse(readSettings());
|
|
||||||
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));
|
|
||||||
} catch { }
|
|
||||||
|
|
||||||
|
|
||||||
// Remove CSP
|
|
||||||
type PolicyResult = Record<string, string[]>;
|
|
||||||
|
|
||||||
const parsePolicy = (policy: string): PolicyResult => {
|
|
||||||
const result: PolicyResult = {};
|
|
||||||
policy.split(";").forEach(directive => {
|
|
||||||
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
|
|
||||||
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
|
|
||||||
result[directiveKey] = directiveValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
const stringifyPolicy = (policy: PolicyResult): string =>
|
|
||||||
Object.entries(policy)
|
|
||||||
.filter(([, values]) => values?.length)
|
|
||||||
.map(directive => directive.flat().join(" "))
|
|
||||||
.join("; ");
|
|
||||||
|
|
||||||
function patchCsp(headers: Record<string, string[]>, header: string) {
|
|
||||||
if (header in headers) {
|
|
||||||
const csp = parsePolicy(headers[header][0]);
|
|
||||||
|
|
||||||
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
|
|
||||||
csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
|
|
||||||
}
|
|
||||||
// TODO: Restrict this to only imported packages with fixed version.
|
|
||||||
// Perhaps auto generate with esbuild
|
|
||||||
csp["script-src"] ??= [];
|
|
||||||
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
|
|
||||||
headers[header] = [stringifyPolicy(csp)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
|
|
||||||
if (responseHeaders) {
|
|
||||||
if (resourceType === "mainFrame")
|
|
||||||
patchCsp(responseHeaders, "content-security-policy");
|
|
||||||
|
|
||||||
// Fix hosts that don't properly set the css content type, such as
|
|
||||||
// raw.githubusercontent.com
|
|
||||||
if (resourceType === "stylesheet")
|
|
||||||
responseHeaders["content-type"] = ["text/css"];
|
|
||||||
}
|
|
||||||
cb({ cancel: false, responseHeaders });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
|
||||||
}
|
}
|
||||||
|
Object.assign(BrowserWindow, electron.BrowserWindow);
|
||||||
|
// esbuild may rename our BrowserWindow, which leads to it being excluded
|
||||||
|
// from getFocusedWindow(), so this is necessary
|
||||||
|
// https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62
|
||||||
|
Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true });
|
||||||
|
|
||||||
|
// Replace electrons exports with our custom BrowserWindow
|
||||||
|
const electronPath = require.resolve("electron");
|
||||||
|
delete require.cache[electronPath]!.exports;
|
||||||
|
require.cache[electronPath]!.exports = {
|
||||||
|
...electron,
|
||||||
|
BrowserWindow
|
||||||
|
};
|
||||||
|
|
||||||
|
// Patch appSettings to force enable devtools
|
||||||
|
Object.defineProperty(global, "appSettings", {
|
||||||
|
set: (v: typeof global.appSettings) => {
|
||||||
|
v.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true);
|
||||||
|
// @ts-ignore
|
||||||
|
delete global.appSettings;
|
||||||
|
global.appSettings = v;
|
||||||
|
},
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
||||||
|
|
||||||
|
electron.app.whenReady().then(() => {
|
||||||
|
// Source Maps! Maybe there's a better way but since the renderer is executed
|
||||||
|
// from a string I don't think any other form of sourcemaps would work
|
||||||
|
electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
||||||
|
let url = unsafeUrl.slice("vencord://".length);
|
||||||
|
if (url.endsWith("/")) url = url.slice(0, -1);
|
||||||
|
switch (url) {
|
||||||
|
case "renderer.js.map":
|
||||||
|
case "preload.js.map":
|
||||||
|
case "patcher.js.map": // doubt
|
||||||
|
cb(join(__dirname, url));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
cb({ statusCode: 403 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const settings = JSON.parse(readSettings());
|
||||||
|
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));
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
// Remove CSP
|
||||||
|
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, url }, cb) => {
|
||||||
|
if (responseHeaders) {
|
||||||
|
delete responseHeaders["content-security-policy-report-only"];
|
||||||
|
delete responseHeaders["content-security-policy"];
|
||||||
|
|
||||||
|
// Fix hosts that don't properly set the content type, such as
|
||||||
|
// raw.githubusercontent.com
|
||||||
|
if (url.endsWith(".css"))
|
||||||
|
responseHeaders["content-type"] = ["text/css"];
|
||||||
|
}
|
||||||
|
cb({ cancel: false, responseHeaders });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
console.log("[Vencord] Loading original Discord app.asar");
|
console.log("[Vencord] Loading original Discord app.asar");
|
||||||
// Legacy Vencord Injector requires "../app.asar". However, because we
|
// Legacy Vencord Injector requires "../app.asar". However, because we
|
||||||
@ -176,5 +143,6 @@ if (readFileSync(injectorPath, "utf-8").includes('require("../app.asar")')) {
|
|||||||
return loadModule.apply(this, arguments);
|
return loadModule.apply(this, arguments);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
console.log(require.main!.filename);
|
||||||
require(require.main!.filename);
|
require(require.main!.filename);
|
||||||
}
|
}
|
||||||
|
@ -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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import { makeLazy } from "@utils/misc";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "BetterNotesBox",
|
|
||||||
description: "Hide notes or disable spellcheck (Configure in settings!!)",
|
|
||||||
authors: [Devs.Ven],
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "hideNote:",
|
|
||||||
all: true,
|
|
||||||
predicate: makeLazy(() => Vencord.Settings.plugins.BetterNotesBox.hide),
|
|
||||||
replacement: {
|
|
||||||
match: /hideNote:.+?(?=[,}])/g,
|
|
||||||
replace: "hideNote:true",
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
find: "Messages.NOTE_PLACEHOLDER",
|
|
||||||
replacement: {
|
|
||||||
match: /\.NOTE_PLACEHOLDER,/,
|
|
||||||
replace: "$&spellCheck:!Vencord.Settings.plugins.BetterNotesBox.noSpellCheck,"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
options: {
|
|
||||||
hide: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Hide notes",
|
|
||||||
default: false,
|
|
||||||
restartNeeded: true
|
|
||||||
},
|
|
||||||
noSpellCheck: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Disable spellcheck in notes",
|
|
||||||
disabled: () => Settings.plugins.BetterNotesBox.hide,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -16,14 +16,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { migratePluginSettings } from "@api/settings";
|
import { Devs } from "../utils/constants";
|
||||||
import { Devs } from "@utils/constants";
|
import definePlugin from "../utils/types";
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
migratePluginSettings("NoDevtoolsWarning", "STFU");
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "NoDevtoolsWarning",
|
name: "STFU",
|
||||||
description: "Disables the 'HOLD UP' banner in the console",
|
description: "Disables the 'HOLD UP' banner in the console",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
patches: [{
|
patches: [{
|
@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "TimeBarAllActivities",
|
|
||||||
description: "Adds the Spotify time bar to all activities if they have start and end timestamps",
|
|
||||||
authors: [Devs.obscurity],
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "renderTimeBar=function",
|
|
||||||
replacement: {
|
|
||||||
match: /renderTimeBar=function\((.{1,3})\){.{0,50}?var/,
|
|
||||||
replace: "renderTimeBar=function($1){var"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
});
|
|
@ -16,9 +16,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
import { Devs } from "../utils/constants";
|
||||||
import { Devs } from "@utils/constants";
|
import definePlugin, { OptionType } from "../utils/types";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import { Settings } from "../Vencord";
|
||||||
|
|
||||||
enum Methods {
|
enum Methods {
|
||||||
Random,
|
Random,
|
||||||
@ -67,9 +67,7 @@ export default definePlugin({
|
|||||||
|
|
||||||
anonymise(file: string) {
|
anonymise(file: string) {
|
||||||
let name = "image";
|
let name = "image";
|
||||||
const extIdx = file.lastIndexOf(".");
|
const ext = file.match(/\..+$/g)?.[0] ?? "";
|
||||||
const ext = extIdx !== -1 ? file.slice(extIdx) : "";
|
|
||||||
|
|
||||||
switch (Settings.plugins.AnonymiseFileNames.method) {
|
switch (Settings.plugins.AnonymiseFileNames.method) {
|
||||||
case Methods.Random:
|
case Methods.Random:
|
||||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
@ -1,161 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BadgePosition, ProfileBadge } from "@api/Badges";
|
|
||||||
import DonateButton from "@components/DonateButton";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Flex } from "@components/Flex";
|
|
||||||
import { Heart } from "@components/Heart";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import IpcEvents from "@utils/IpcEvents";
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import { closeModal, Modals, openModal } from "@utils/modal";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
import { Forms, Margins } from "@webpack/common";
|
|
||||||
|
|
||||||
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
|
|
||||||
|
|
||||||
/** List of vencord contributor IDs */
|
|
||||||
const contributorIds: string[] = Object.values(Devs).map(d => d.id.toString());
|
|
||||||
|
|
||||||
const ContributorBadge: ProfileBadge = {
|
|
||||||
tooltip: "Vencord Contributor",
|
|
||||||
image: CONTRIBUTOR_BADGE,
|
|
||||||
position: BadgePosition.START,
|
|
||||||
props: {
|
|
||||||
style: {
|
|
||||||
borderRadius: "50%",
|
|
||||||
transform: "scale(0.9)" // The image is a bit too big compared to default badges
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shouldShow: ({ user }) => contributorIds.includes(user.id),
|
|
||||||
onClick: () => VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/Vendicated/Vencord")
|
|
||||||
};
|
|
||||||
|
|
||||||
const DonorBadges = {} as Record<string, Pick<ProfileBadge, "image" | "tooltip">>;
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "BadgeAPI",
|
|
||||||
description: "API to add badges to users.",
|
|
||||||
authors: [Devs.Megu],
|
|
||||||
required: true,
|
|
||||||
patches: [
|
|
||||||
/* Patch the badges array */
|
|
||||||
{
|
|
||||||
find: "PREMIUM_GUILD_SUBSCRIPTION_TOOLTIP.format({date:",
|
|
||||||
replacement: {
|
|
||||||
match: /&&((\w{1,3})\.push\({tooltip:\w{1,3}\.\w{1,3}\.Messages\.PREMIUM_GUILD_SUBSCRIPTION_TOOLTIP\.format.+?;)(?:return\s\w{1,3};?})/,
|
|
||||||
replace: (_, m, badgeArray) => `&&${m} return Vencord.Api.Badges.inject(${badgeArray}, arguments[0]);}`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/* Patch the badge list component on user profiles */
|
|
||||||
{
|
|
||||||
find: "Messages.PROFILE_USER_BADGES,role:",
|
|
||||||
replacement: {
|
|
||||||
match: /src:(\w{1,3})\[(\w{1,3})\.key\],/,
|
|
||||||
// <img src={badge.image ?? imageMap[badge.key]} {...badge.props} />
|
|
||||||
replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
async start() {
|
|
||||||
Vencord.Api.Badges.addBadge(ContributorBadge);
|
|
||||||
const badges = await fetch("https://gist.githubusercontent.com/Vendicated/51a3dd775f6920429ec6e9b735ca7f01/raw/badges.csv").then(r => r.text());
|
|
||||||
const lines = badges.trim().split("\n");
|
|
||||||
if (lines.shift() !== "id,tooltip,image") {
|
|
||||||
new Logger("BadgeAPI").error("Invalid badges.csv file!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const line of lines) {
|
|
||||||
const [id, tooltip, image] = line.split(",");
|
|
||||||
DonorBadges[id] = { image, tooltip };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addDonorBadge(badges: ProfileBadge[], userId: string) {
|
|
||||||
const badge = DonorBadges[userId];
|
|
||||||
if (badge) {
|
|
||||||
badges.unshift({
|
|
||||||
...badge,
|
|
||||||
position: BadgePosition.START,
|
|
||||||
props: {
|
|
||||||
style: {
|
|
||||||
borderRadius: "50%",
|
|
||||||
transform: "scale(0.9)" // The image is a bit too big compared to default badges
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick() {
|
|
||||||
const modalKey = openModal(props => (
|
|
||||||
<ErrorBoundary noop onError={() => {
|
|
||||||
closeModal(modalKey);
|
|
||||||
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/sponsors/Vendicated");
|
|
||||||
}}>
|
|
||||||
<Modals.ModalRoot {...props}>
|
|
||||||
<Modals.ModalHeader>
|
|
||||||
<Flex style={{ width: "100%", justifyContent: "center" }}>
|
|
||||||
<Forms.FormTitle
|
|
||||||
tag="h2"
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
textAlign: "center",
|
|
||||||
margin: 0
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Heart />
|
|
||||||
Vencord Donor
|
|
||||||
</Forms.FormTitle>
|
|
||||||
</Flex>
|
|
||||||
</Modals.ModalHeader>
|
|
||||||
<Modals.ModalContent>
|
|
||||||
<Flex>
|
|
||||||
<img
|
|
||||||
role="presentation"
|
|
||||||
src="https://cdn.discordapp.com/emojis/1026533070955872337.png"
|
|
||||||
alt=""
|
|
||||||
style={{ margin: "auto" }}
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
role="presentation"
|
|
||||||
src="https://cdn.discordapp.com/emojis/1026533090627174460.png"
|
|
||||||
alt=""
|
|
||||||
style={{ margin: "auto" }}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
<div style={{ padding: "1em" }}>
|
|
||||||
<Forms.FormText>
|
|
||||||
This Badge is a special perk for Vencord Donors
|
|
||||||
</Forms.FormText>
|
|
||||||
<Forms.FormText className={Margins.marginTop20}>
|
|
||||||
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
|
||||||
</Forms.FormText>
|
|
||||||
</div>
|
|
||||||
</Modals.ModalContent>
|
|
||||||
<Modals.ModalFooter>
|
|
||||||
<Flex style={{ width: "100%", justifyContent: "center" }}>
|
|
||||||
<DonateButton />
|
|
||||||
</Flex>
|
|
||||||
</Modals.ModalFooter>
|
|
||||||
</Modals.ModalRoot>
|
|
||||||
</ErrorBoundary>
|
|
||||||
));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -16,8 +16,8 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "../utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "../utils/types";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "CommandsAPI",
|
name: "CommandsAPI",
|
||||||
@ -47,15 +47,6 @@ export default definePlugin({
|
|||||||
match: /,(.{1,2})\.execute\((.{1,2}),(.{1,2})\)]/,
|
match: /,(.{1,2})\.execute\((.{1,2}),(.{1,2})\)]/,
|
||||||
replace: (_, cmd, args, ctx) => `,Vencord.Api.Commands._handleCommand(${cmd}, ${args}, ${ctx})]`
|
replace: (_, cmd, args, ctx) => `,Vencord.Api.Commands._handleCommand(${cmd}, ${args}, ${ctx})]`
|
||||||
}
|
}
|
||||||
},
|
|
||||||
// Show plugin name instead of "Built-In"
|
|
||||||
{
|
|
||||||
find: "().source,children",
|
|
||||||
replacement: {
|
|
||||||
// ...children: p?.name
|
|
||||||
match: /(?<=:(.{1,3})\.displayDescription\}.{0,200}\(\)\.source,children:)[^}]+/,
|
|
||||||
replace: "$1.plugin||($&)"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { migratePluginSettings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
// duplicate values have multiple branches with different types. Just include all to be safe
|
|
||||||
const nameMap = {
|
|
||||||
radio: "MenuRadioItem",
|
|
||||||
separator: "MenuSeparator",
|
|
||||||
checkbox: "MenuCheckboxItem",
|
|
||||||
groupstart: "MenuGroup",
|
|
||||||
|
|
||||||
control: "MenuControlItem",
|
|
||||||
compositecontrol: "MenuControlItem",
|
|
||||||
|
|
||||||
item: "MenuItem",
|
|
||||||
customitem: "MenuItem",
|
|
||||||
};
|
|
||||||
|
|
||||||
migratePluginSettings("MenuItemDeobfuscatorAPI", "MenuItemDeobfuscatorApi");
|
|
||||||
export default definePlugin({
|
|
||||||
name: "MenuItemDeobfuscatorAPI",
|
|
||||||
description: "Deobfuscates Discord's Menu Item module",
|
|
||||||
authors: [Devs.Ven],
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: '"Menu API',
|
|
||||||
replacement: {
|
|
||||||
match: /function.{0,80}type===(.{1,3})\..{1,3}\).{0,50}navigable:.+?Menu API/s,
|
|
||||||
replace: (m, mod) => {
|
|
||||||
let nicenNames = "";
|
|
||||||
const redefines = [] as string[];
|
|
||||||
// if (t.type === m.MenuItem)
|
|
||||||
const typeCheckRe = /\(.{1,3}\.type===(.{1,5})\)/g;
|
|
||||||
// push({type:"item"})
|
|
||||||
const pushTypeRe = /type:"(\w+)"/g;
|
|
||||||
|
|
||||||
let typeMatch: RegExpExecArray | null;
|
|
||||||
// for each if (t.type === ...)
|
|
||||||
while ((typeMatch = typeCheckRe.exec(m)) !== null) {
|
|
||||||
// extract the current menu item
|
|
||||||
const item = typeMatch[1];
|
|
||||||
// Set the starting index of the second regex to that of the first to start
|
|
||||||
// matching from after the if
|
|
||||||
pushTypeRe.lastIndex = typeCheckRe.lastIndex;
|
|
||||||
// extract the first type: "..."
|
|
||||||
const type = pushTypeRe.exec(m)?.[1];
|
|
||||||
if (type && type in nameMap) {
|
|
||||||
const name = nameMap[type];
|
|
||||||
nicenNames += `Object.defineProperty(${item},"name",{value:"${name}"});`;
|
|
||||||
redefines.push(`${name}:${item}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (redefines.length < 6) {
|
|
||||||
console.warn("[ApiMenuItemDeobfuscator] Expected to at least remap 6 items, only remapped", redefines.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge all our redefines with the actual module
|
|
||||||
return `${nicenNames}Object.assign(${mod},{${redefines.join(",")}});${m}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
@ -16,8 +16,8 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "../utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "../utils/types";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "MessageAccessoriesAPI",
|
name: "MessageAccessoriesAPI",
|
||||||
@ -27,9 +27,9 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: "_messageAttachmentToEmbedMedia",
|
find: "_messageAttachmentToEmbedMedia",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(\(\)\.container\)?,children:)(\[[^\]]+\])(}\)\};return)/,
|
match: /\(\)\.container\)},(.+?)\)};return/,
|
||||||
replace: (_, pre, accessories, post) =>
|
replace: (_, accessories) =>
|
||||||
`${pre}Vencord.Api.MessageAccessories._modifyAccessories(${accessories},this.props)${post}`,
|
`().container)},Vencord.Api.MessageAccessories._modifyAccessories([${accessories}],this.props))};return`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "../utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "../utils/types";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "MessageEventsAPI",
|
name: "MessageEventsAPI",
|
||||||
@ -28,14 +28,14 @@ export default definePlugin({
|
|||||||
find: "sendMessage:function",
|
find: "sendMessage:function",
|
||||||
replacement: [{
|
replacement: [{
|
||||||
match: /(?<=_sendMessage:function\([^)]+\)){/,
|
match: /(?<=_sendMessage:function\([^)]+\)){/,
|
||||||
replace: "{if(Vencord.Api.MessageEvents._handlePreSend(...arguments)){return;};"
|
replace: "{Vencord.Api.MessageEvents._handlePreSend(...arguments);"
|
||||||
}, {
|
}, {
|
||||||
match: /(?<=\beditMessage:function\([^)]+\)){/,
|
match: /(?<=\beditMessage:function\([^)]+\)){/,
|
||||||
replace: "{Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
|
replace: "{Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: '("interactionUsernameProfile',
|
find: "if(e.altKey){",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /var \w=(\w)\.id,\w=(\w)\.id;return .{1,2}\.useCallback\(\(?function\((.{1,2})\){/,
|
match: /var \w=(\w)\.id,\w=(\w)\.id;return .{1,2}\.useCallback\(\(?function\((.{1,2})\){/,
|
||||||
replace: (m, message, channel, event) =>
|
replace: (m, message, channel, event) =>
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "MessagePopoverAPI",
|
|
||||||
description: "API to add buttons to message popovers.",
|
|
||||||
authors: [Devs.KingFish],
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
});
|
|
@ -16,14 +16,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { migratePluginSettings } from "@api/settings";
|
import { Devs } from "../utils/constants";
|
||||||
import { Devs } from "@utils/constants";
|
import definePlugin from "../utils/types";
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
migratePluginSettings("NoticesAPI", "NoticesApi");
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "NoticesAPI",
|
name: "ApiNotices",
|
||||||
description: "Fixes notices being automatically dismissed",
|
description: "Fixes notices being automatically dismissed",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "ServerListAPI",
|
|
||||||
authors: [Devs.kemo],
|
|
||||||
description: "Api required for plugins that modify the server list",
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "Messages.DISCODO_DISABLED",
|
|
||||||
replacement: {
|
|
||||||
match: /(Messages\.DISCODO_DISABLED\);return)(.*?homeIcon.*?)(\}function)/,
|
|
||||||
replace: "$1[$2].concat(Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.Above))$3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "Messages.SERVERS",
|
|
||||||
replacement: {
|
|
||||||
match: /(Messages\.SERVERS,children:)(.+?default:return null\}\}\)\))/,
|
|
||||||
replace: "$1Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.In).concat($2)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
@ -1,107 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 OpenAsar
|
|
||||||
*
|
|
||||||
* 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 { popNotice, showNotice } from "@api/Notices";
|
|
||||||
import { Link } from "@components/Link";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
import { filters, findByCodeLazy, mapMangledModuleLazy } from "@webpack";
|
|
||||||
import { FluxDispatcher, Forms, Toasts } from "@webpack/common";
|
|
||||||
|
|
||||||
const assetManager = mapMangledModuleLazy(
|
|
||||||
"getAssetImage: size must === [number, number] for Twitch",
|
|
||||||
{
|
|
||||||
getAsset: filters.byCode("apply("),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const rpcManager = findByCodeLazy(".APPLICATION_RPC(");
|
|
||||||
|
|
||||||
async function lookupAsset(applicationId: string, key: string): Promise<string> {
|
|
||||||
return (await assetManager.getAsset(applicationId, [key, undefined]))[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const apps: any = {};
|
|
||||||
async function lookupApp(applicationId: string): Promise<string> {
|
|
||||||
const socket: any = {};
|
|
||||||
await rpcManager.lookupApp(socket, applicationId);
|
|
||||||
return socket.application;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ws: WebSocket;
|
|
||||||
export default definePlugin({
|
|
||||||
name: "WebRichPresence (arRPC)",
|
|
||||||
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
|
|
||||||
authors: [Devs.Ducko],
|
|
||||||
target: "WEB",
|
|
||||||
|
|
||||||
settingsAboutComponent: () => (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle tag="h3">How to use arRPC</Forms.FormTitle>
|
|
||||||
<Forms.FormText>
|
|
||||||
<Link href="https://github.com/OpenAsar/arrpc/tree/main#server">Follow the instructions in the GitHub repo</Link> to get the server running, and then enable the plugin.
|
|
||||||
</Forms.FormText>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
|
|
||||||
async start() {
|
|
||||||
if (ws) ws.close();
|
|
||||||
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket
|
|
||||||
|
|
||||||
ws.onmessage = async e => { // on message, set status to data
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
|
|
||||||
if (data.activity?.assets?.large_image) data.activity.assets.large_image = await lookupAsset(data.activity.application_id, data.activity.assets.large_image);
|
|
||||||
if (data.activity?.assets?.small_image) data.activity.assets.small_image = await lookupAsset(data.activity.application_id, data.activity.assets.small_image);
|
|
||||||
|
|
||||||
if (data.activity) {
|
|
||||||
const appId = data.activity.application_id;
|
|
||||||
apps[appId] ||= await lookupApp(appId);
|
|
||||||
|
|
||||||
const app = apps[appId];
|
|
||||||
data.activity.name ||= app.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", ...data });
|
|
||||||
};
|
|
||||||
|
|
||||||
const connectionSuccessful = await new Promise(res => setTimeout(() => res(ws.readyState === WebSocket.OPEN), 1000)); // check if open after 1s
|
|
||||||
if (!connectionSuccessful) {
|
|
||||||
showNotice("Failed to connect to arRPC, is it running?", "Retry", () => { // show notice about failure to connect, with retry/ignore
|
|
||||||
popNotice();
|
|
||||||
this.start();
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Toasts.show({ // show toast on success
|
|
||||||
message: "Connected to arRPC",
|
|
||||||
type: Toasts.Type.SUCCESS,
|
|
||||||
id: Toasts.genId(),
|
|
||||||
options: {
|
|
||||||
duration: 1000,
|
|
||||||
position: Toasts.Position.BOTTOM
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null }); // clear status
|
|
||||||
ws.close(); // close WebSocket
|
|
||||||
}
|
|
||||||
});
|
|
@ -16,13 +16,19 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "../utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "../utils/types";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BANger",
|
name: "BANger",
|
||||||
description: "Replaces the GIF in the ban dialogue with a custom one.",
|
description: "Replaces the GIF in the ban dialogue with a custom one.",
|
||||||
authors: [Devs.Xinto, Devs.Glitch],
|
authors: [
|
||||||
|
{
|
||||||
|
name: "Xinto",
|
||||||
|
id: 423915768191647755n
|
||||||
|
},
|
||||||
|
Devs.Glitch
|
||||||
|
],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "BAN_CONFIRM_TITLE.",
|
find: "BAN_CONFIRM_TITLE.",
|
||||||
|
@ -16,9 +16,8 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Devs } from "../utils/constants";
|
||||||
import { Devs } from "@utils/constants";
|
import definePlugin from "../utils/types";
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BetterGifAltText",
|
name: "BetterGifAltText",
|
||||||
@ -29,7 +28,7 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: "onCloseImage=",
|
find: "onCloseImage=",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
|
match: /(return .{1,2}\.createElement.{0,50}isWindowFocused)/,
|
||||||
replace:
|
replace:
|
||||||
"Vencord.Plugins.plugins.BetterGifAltText.altify(e);$1",
|
"Vencord.Plugins.plugins.BetterGifAltText.altify(e);$1",
|
||||||
},
|
},
|
||||||
@ -37,9 +36,9 @@ export default definePlugin({
|
|||||||
{
|
{
|
||||||
find: 'preload:"none","aria',
|
find: 'preload:"none","aria',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/,
|
match: /\?.{0,5}\.Messages\.GIF/,
|
||||||
replace:
|
replace:
|
||||||
"?($1.alt='GIF',Vencord.Plugins.plugins.BetterGifAltText.altify($1))",
|
"?(e.alt='GIF',Vencord.Plugins.plugins.BetterGifAltText.altify(e))",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -16,22 +16,20 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "../utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "../utils/types";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BetterUploadButton",
|
name: "BetterUploadButton",
|
||||||
authors: [Devs.obscurity, Devs.Ven],
|
authors: [Devs.obscurity],
|
||||||
description: "Upload with a single click, open menu with right click",
|
description: "Upload with a single click, open menu with right click",
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "Messages.CHAT_ATTACH_UPLOAD_OR_INVITE",
|
find: "Messages.CHAT_ATTACH_UPLOAD_OR_INVITE",
|
||||||
replacement: {
|
replacement: {
|
||||||
// Discord merges multiple props here with Object.assign()
|
match: /CHAT_ATTACH_UPLOAD_OR_INVITE,onDoubleClick:([^,]+),onClick:([^,]+)}}/,
|
||||||
// This patch passes a third object to it with which we override onClick and onContextMenu
|
replace:
|
||||||
match: /CHAT_ATTACH_UPLOAD_OR_INVITE,onDoubleClick:(.+?:void 0)\},(.{1,3})\)/,
|
"CHAT_ATTACH_UPLOAD_OR_INVITE,onClick:$1,onContextMenu:$2}}",
|
||||||
replace: (m, onDblClick, otherProps) =>
|
|
||||||
`${m.slice(0, -1)},{onClick:${onDblClick},onContextMenu:${otherProps}.onClick})`,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
|
|
||||||
let style: HTMLStyleElement;
|
|
||||||
|
|
||||||
function setCss() {
|
|
||||||
style.textContent = `
|
|
||||||
.vc-nsfw-img [class^=imageWrapper] img,
|
|
||||||
.vc-nsfw-img [class^=wrapperPaused] video {
|
|
||||||
filter: blur(${Settings.plugins.BlurNSFW.blurAmount}px);
|
|
||||||
transition: filter 0.2s;
|
|
||||||
}
|
|
||||||
.vc-nsfw-img [class^=imageWrapper]:hover img,
|
|
||||||
.vc-nsfw-img [class^=wrapperPaused]:hover video {
|
|
||||||
filter: unset;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "BlurNSFW",
|
|
||||||
description: "Blur attachments in NSFW channels until hovered",
|
|
||||||
authors: [Devs.Ven],
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "().embedWrapper,embed",
|
|
||||||
replacement: [{
|
|
||||||
match: /(\.renderEmbed=.+?(.)=.\.props)(.+?\(\)\.embedWrapper)/g,
|
|
||||||
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
|
||||||
}, {
|
|
||||||
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\(\)\.embedWrapper)/g,
|
|
||||||
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
options: {
|
|
||||||
blurAmount: {
|
|
||||||
type: OptionType.NUMBER,
|
|
||||||
description: "Blur Amount",
|
|
||||||
default: 10,
|
|
||||||
onChange: setCss
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
start() {
|
|
||||||
style = document.createElement("style");
|
|
||||||
style.id = "VcBlurNsfw";
|
|
||||||
document.head.appendChild(style);
|
|
||||||
|
|
||||||
setCss();
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
style?.remove();
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { React } from "@webpack/common";
|
|
||||||
|
|
||||||
function formatDuration(ms: number) {
|
|
||||||
// here be dragons (moment fucking sucks)
|
|
||||||
const human = Settings.plugins.CallTimer.format === "human";
|
|
||||||
|
|
||||||
const format = (n: number) => human ? n : n.toString().padStart(2, "0");
|
|
||||||
const unit = (s: string) => human ? s : "";
|
|
||||||
const delim = human ? " " : ":";
|
|
||||||
|
|
||||||
// thx copilot
|
|
||||||
const d = Math.floor(ms / 86400000);
|
|
||||||
const h = Math.floor((ms % 86400000) / 3600000);
|
|
||||||
const m = Math.floor(((ms % 86400000) % 3600000) / 60000);
|
|
||||||
const s = Math.floor((((ms % 86400000) % 3600000) % 60000) / 1000);
|
|
||||||
|
|
||||||
let res = "";
|
|
||||||
if (d) res += `${d}d `;
|
|
||||||
if (h || res) res += `${format(h)}${unit("h")}${delim}`;
|
|
||||||
if (m || res || !human) res += `${format(m)}${unit("m")}${delim}`;
|
|
||||||
res += `${format(s)}${unit("s")}`;
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "CallTimer",
|
|
||||||
description: "Adds a timer to vcs",
|
|
||||||
authors: [Devs.Ven],
|
|
||||||
|
|
||||||
startTime: 0,
|
|
||||||
interval: void 0 as NodeJS.Timeout | undefined,
|
|
||||||
|
|
||||||
options: {
|
|
||||||
format: {
|
|
||||||
type: OptionType.SELECT,
|
|
||||||
description: "The timer format. This can be any valid moment.js format",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: "30d 23:00:42",
|
|
||||||
value: "stopwatch",
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "30d 23h 00m 42s",
|
|
||||||
value: "human"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
patches: [{
|
|
||||||
find: ".renderConnectionStatus=",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=renderConnectionStatus=.+\(\)\.channel,children:)\w/,
|
|
||||||
replace: "[$&, Vencord.Plugins.plugins.CallTimer.renderTimer(this.props.channel.id)]"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
renderTimer(channelId: string) {
|
|
||||||
return <ErrorBoundary noop>
|
|
||||||
<this.Timer channelId={channelId} />
|
|
||||||
</ErrorBoundary>;
|
|
||||||
},
|
|
||||||
|
|
||||||
Timer({ channelId }: { channelId: string; }) {
|
|
||||||
const [time, setTime] = React.useState(0);
|
|
||||||
const startTime = React.useMemo(() => Date.now(), [channelId]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const interval = setInterval(() => setTime(Date.now() - startTime), 1000);
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
setTime(0);
|
|
||||||
};
|
|
||||||
}, [channelId]);
|
|
||||||
|
|
||||||
return <p style={{ margin: 0 }}>Connected for {formatDuration(time)}</p>;
|
|
||||||
}
|
|
||||||
});
|
|
@ -21,23 +21,24 @@ import {
|
|||||||
addPreSendListener,
|
addPreSendListener,
|
||||||
MessageObject,
|
MessageObject,
|
||||||
removePreEditListener,
|
removePreEditListener,
|
||||||
removePreSendListener
|
removePreSendListener,
|
||||||
} from "@api/MessageEvents";
|
} from "../../api/MessageEvents";
|
||||||
import { migratePluginSettings } from "@api/settings";
|
import definePlugin from "../../utils/types";
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
import { defaultRules } from "./defaultRules";
|
import { defaultRules } from "./defaultRules";
|
||||||
|
|
||||||
// From lodash
|
// From lodash
|
||||||
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
|
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
|
||||||
const reHasRegExpChar = RegExp(reRegExpChar.source);
|
const reHasRegExpChar = RegExp(reRegExpChar.source);
|
||||||
|
|
||||||
migratePluginSettings("ClearURLs", "clearURLs");
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ClearURLs",
|
name: "clearURLs",
|
||||||
description: "Removes tracking garbage from URLs",
|
description: "Removes tracking garbage from URLs",
|
||||||
authors: [Devs.adryd],
|
authors: [
|
||||||
|
{
|
||||||
|
name: "adryd",
|
||||||
|
id: 0n,
|
||||||
|
},
|
||||||
|
],
|
||||||
dependencies: ["MessageEventsAPI"],
|
dependencies: ["MessageEventsAPI"],
|
||||||
|
|
||||||
escapeRegExp(str: string) {
|
escapeRegExp(str: string) {
|
||||||
|
@ -16,47 +16,36 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { migratePluginSettings, Settings } from "@api/settings";
|
import { Devs } from "../utils/constants";
|
||||||
import { Devs } from "@utils/constants";
|
import definePlugin from "../utils/types";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import { Toasts } from "../webpack/common";
|
||||||
import { Clipboard, Toasts } from "@webpack/common";
|
|
||||||
|
|
||||||
migratePluginSettings("BetterRoleDot", "ClickableRoleDot");
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BetterRoleDot",
|
name: "ClickableRoleDot",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
description:
|
description:
|
||||||
"Copy role colour on RoleDot (accessibility setting) click. Also allows using both RoleDot and coloured names simultaneously",
|
"Makes RoleDots (Accessibility Feature) copy colour to clipboard on click",
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
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",
|
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: {
|
replacement: {
|
||||||
match: /viewBox:"0 0 20 20"/,
|
match: /(viewBox:"0 0 20 20")/,
|
||||||
replace: "$&,onClick:()=>Vencord.Plugins.plugins.BetterRoleDot.copyToClipBoard(e.color),style:{cursor:'pointer'}",
|
replace: "$1,onClick:()=>Vencord.Plugins.plugins.ClickableRoleDot.copyToClipBoard(e.color)",
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: '"username"===',
|
|
||||||
all: true,
|
|
||||||
predicate: () => Settings.plugins.BetterRoleDot.bothStyles,
|
|
||||||
replacement: {
|
|
||||||
match: /"(?:username|dot)"===\w\b/g,
|
|
||||||
replace: "true",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
options: {
|
copyToClipBoard(color: string) {
|
||||||
bothStyles: {
|
if (IS_WEB) {
|
||||||
type: OptionType.BOOLEAN,
|
navigator.clipboard.writeText(color)
|
||||||
description: "Show both role dot and coloured names",
|
.then(() => this.notifySuccess);
|
||||||
default: false,
|
} else {
|
||||||
|
DiscordNative.clipboard.copy(color);
|
||||||
|
this.notifySuccess();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
copyToClipBoard(color: string) {
|
notifySuccess() {
|
||||||
Clipboard.copy(color);
|
|
||||||
Toasts.show({
|
Toasts.show({
|
||||||
message: "Copied to Clipboard!",
|
message: "Copied to Clipboard!",
|
||||||
type: Toasts.Type.SUCCESS,
|
type: Toasts.Type.SUCCESS,
|
||||||
@ -66,5 +55,5 @@ export default definePlugin({
|
|||||||
position: Toasts.Position.BOTTOM
|
position: Toasts.Position.BOTTOM
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
});
|
});
|
@ -16,8 +16,8 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "../utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "../utils/types";
|
||||||
|
|
||||||
const WEB_ONLY = (f: string) => () => {
|
const WEB_ONLY = (f: string) => () => {
|
||||||
throw new Error(`'${f}' is Discord Desktop only.`);
|
throw new Error(`'${f}' is Discord Desktop only.`);
|
||||||
@ -37,7 +37,6 @@ export default definePlugin({
|
|||||||
wreq: Vencord.Webpack.wreq,
|
wreq: Vencord.Webpack.wreq,
|
||||||
wpsearch: Vencord.Webpack.search,
|
wpsearch: Vencord.Webpack.search,
|
||||||
wpex: Vencord.Webpack.extract,
|
wpex: Vencord.Webpack.extract,
|
||||||
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!),
|
|
||||||
findByProps: Vencord.Webpack.findByProps,
|
findByProps: Vencord.Webpack.findByProps,
|
||||||
find: Vencord.Webpack.find,
|
find: Vencord.Webpack.find,
|
||||||
Plugins: Vencord.Plugins,
|
Plugins: Vencord.Plugins,
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ApplicationCommandOptionType, sendBotMessage } from "@api/Commands";
|
|
||||||
import { findOption } from "@api/Commands/commandHelpers";
|
|
||||||
import { ApplicationCommandInputType } from "@api/Commands/types";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
import { findByCode, findByProps } from "@webpack";
|
|
||||||
|
|
||||||
const DRAFT_TYPE = 0;
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "CorruptMp4s",
|
|
||||||
description: "Create corrupt mp4s with extremely high or negative duration",
|
|
||||||
authors: [Devs.Ven],
|
|
||||||
dependencies: ["CommandsAPI"],
|
|
||||||
commands: [{
|
|
||||||
name: "corrupt",
|
|
||||||
description: "Create a corrupt mp4 with extremely high or negative duration",
|
|
||||||
inputType: ApplicationCommandInputType.BUILT_IN,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: "mp4",
|
|
||||||
description: "the video to corrupt",
|
|
||||||
type: ApplicationCommandOptionType.ATTACHMENT,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "kind",
|
|
||||||
description: "the kind of corruption",
|
|
||||||
type: ApplicationCommandOptionType.STRING,
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
name: "infinite",
|
|
||||||
value: "infinite",
|
|
||||||
label: "Very high duration"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "negative",
|
|
||||||
value: "negative",
|
|
||||||
label: "Negative duration"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
execute: async (args, ctx) => {
|
|
||||||
const UploadStore = findByProps("getUploads");
|
|
||||||
const upload = UploadStore.getUploads(ctx.channel.id, DRAFT_TYPE)[0];
|
|
||||||
|
|
||||||
const video = upload?.item?.file as File | undefined;
|
|
||||||
|
|
||||||
if (video?.type !== "video/mp4")
|
|
||||||
return void sendBotMessage(ctx.channel.id, {
|
|
||||||
content: "Please upload a mp4 file"
|
|
||||||
});
|
|
||||||
|
|
||||||
const corruption = findOption<string>(args, "kind", "infinite");
|
|
||||||
|
|
||||||
const buf = new Uint8Array(await video.arrayBuffer());
|
|
||||||
let found = false;
|
|
||||||
|
|
||||||
// adapted from https://github.com/GeopJr/exorcism/blob/c9a12d77ccbcb49c987b385eafae250906efc297/src/App.svelte#L41-L48
|
|
||||||
for (let i = 0; i < buf.length; i++) {
|
|
||||||
if (buf[i] === 0x6d && buf[i + 1] === 0x76 && buf[i + 2] === 0x68 && buf[i + 3] === 0x64) {
|
|
||||||
let start = i + 18;
|
|
||||||
buf[start++] = 0x00;
|
|
||||||
buf[start++] = 0x01;
|
|
||||||
buf[start++] = corruption === "negative" ? 0xff : 0x7f;
|
|
||||||
buf[start++] = 0xff;
|
|
||||||
buf[start++] = 0xff;
|
|
||||||
buf[start++] = corruption === "negative" ? 0xf0 : 0xff;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
return void sendBotMessage(ctx.channel.id, {
|
|
||||||
content: "Could not find signature. Is this even a mp4?"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ApplicationCommandOptionType, sendBotMessage } from "@api/Commands";
|
|
||||||
import { ApplicationCommandInputType } from "@api/Commands/types";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "UrbanDictionary",
|
|
||||||
description: "Searches for a word on Urban Dictionary",
|
|
||||||
authors: [Devs.jewdev],
|
|
||||||
dependencies: ["CommandsAPI"],
|
|
||||||
commands: [
|
|
||||||
{
|
|
||||||
name: "urban",
|
|
||||||
description: "Returns the definition of a word from Urban Dictionary",
|
|
||||||
inputType: ApplicationCommandInputType.BUILT_IN,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
type: ApplicationCommandOptionType.STRING,
|
|
||||||
name: "word",
|
|
||||||
description: "The word to search for on Urban Dictionary",
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
execute: async (args, ctx) => {
|
|
||||||
try {
|
|
||||||
const { list: [definition] } = await (await fetch(`https://api.urbandictionary.com/v0/define?term=${args[0].value}`)).json();
|
|
||||||
|
|
||||||
if (!definition)
|
|
||||||
return void sendBotMessage(ctx.channel.id, { content: "No results found." });
|
|
||||||
|
|
||||||
const linkify = text => text.replace(/\[(.+?)\]/g, (_, word) => `[${word}](https://www.urbandictionary.com/define.php?term=${encodeURIComponent(word)})`);
|
|
||||||
|
|
||||||
return void sendBotMessage(ctx.channel.id, {
|
|
||||||
embeds: [
|
|
||||||
{
|
|
||||||
type: "rich",
|
|
||||||
author: {
|
|
||||||
name: `Definition of ${definition.word}`,
|
|
||||||
url: definition.permalink
|
|
||||||
},
|
|
||||||
description: linkify(definition.definition),
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: "Example",
|
|
||||||
value: linkify(definition.example)
|
|
||||||
}
|
|
||||||
],
|
|
||||||
color: 0xFF9900,
|
|
||||||
footer: { text: `👍 ${definition.thumbs_up.toString()} | 👎 ${definition.thumbs_down.toString()} | Uploaded by ${definition.author}`, icon_url: "https://www.urbandictionary.com/favicon.ico" },
|
|
||||||
timestamp: new Date(definition.written_on).toISOString()
|
|
||||||
}
|
|
||||||
] as any
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return void sendBotMessage(ctx.channel.id, {
|
|
||||||
content: `Something went wrong: \`${error}\``
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
@ -1,246 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { migratePluginSettings, Settings } from "@api/settings";
|
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import Logger from "@utils/Logger";
|
|
||||||
import { makeLazy } from "@utils/misc";
|
|
||||||
import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
|
||||||
import { Forms, GuildStore, Margins, Menu, PermissionStore, React, Toasts, Tooltip, UserStore } from "@webpack/common";
|
|
||||||
|
|
||||||
const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n;
|
|
||||||
|
|
||||||
const GuildEmojiStore = findByPropsLazy("getGuilds", "getGuildEmoji");
|
|
||||||
const uploadEmoji = findByCodeLazy('"EMOJI_UPLOAD_START"', "GUILD_EMOJIS(");
|
|
||||||
|
|
||||||
function getGuildCandidates(isAnimated: boolean) {
|
|
||||||
const meId = UserStore.getCurrentUser().id;
|
|
||||||
|
|
||||||
return Object.values(GuildStore.getGuilds()).filter(g => {
|
|
||||||
const canCreate = g.ownerId === meId ||
|
|
||||||
BigInt(PermissionStore.getGuildPermissions({ id: g.id }) & MANAGE_EMOJIS_AND_STICKERS) === MANAGE_EMOJIS_AND_STICKERS;
|
|
||||||
if (!canCreate) return false;
|
|
||||||
|
|
||||||
const emojiSlots = g.getMaxEmojiSlots();
|
|
||||||
const { emojis } = GuildEmojiStore.getGuilds()[g.id];
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
for (const emoji of emojis)
|
|
||||||
if (emoji.animated === isAnimated) count++;
|
|
||||||
return count < emojiSlots;
|
|
||||||
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doClone(guildId: string, id: string, name: string, isAnimated: boolean) {
|
|
||||||
const data = await fetch(`https://cdn.discordapp.com/emojis/${id}.${isAnimated ? "gif" : "png"}`)
|
|
||||||
.then(r => r.blob());
|
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
reader.onload = () => {
|
|
||||||
uploadEmoji({
|
|
||||||
guildId,
|
|
||||||
name,
|
|
||||||
image: reader.result
|
|
||||||
}).then(() => {
|
|
||||||
Toasts.show({
|
|
||||||
message: `Successfully cloned ${name}!`,
|
|
||||||
type: Toasts.Type.SUCCESS,
|
|
||||||
id: Toasts.genId()
|
|
||||||
});
|
|
||||||
}).catch((e: any) => {
|
|
||||||
new Logger("EmoteCloner").error("Failed to upload emoji", e);
|
|
||||||
Toasts.show({
|
|
||||||
message: "Oopsie something went wrong :( Check console!!!",
|
|
||||||
type: Toasts.Type.FAILURE,
|
|
||||||
id: Toasts.genId()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsDataURL(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFontSize = (s: string) => {
|
|
||||||
// [18, 18, 16, 16, 14, 12, 10]
|
|
||||||
const sizes = [20, 20, 18, 18, 16, 14, 12];
|
|
||||||
return sizes[s.length] ?? 4;
|
|
||||||
};
|
|
||||||
|
|
||||||
const nameValidator = /^\w+$/i;
|
|
||||||
|
|
||||||
function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: string; isAnimated: boolean; }) {
|
|
||||||
const [isCloning, setIsCloning] = React.useState(false);
|
|
||||||
const [name, setName] = React.useState(emojiName);
|
|
||||||
|
|
||||||
const [x, invalidateMemo] = React.useReducer(x => x + 1, 0);
|
|
||||||
|
|
||||||
const guilds = React.useMemo(() => getGuildCandidates(isAnimated), [isAnimated, x]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle className={Margins.marginTop20}>Custom Name</Forms.FormTitle>
|
|
||||||
<CheckedTextInput
|
|
||||||
value={name}
|
|
||||||
onChange={setName}
|
|
||||||
validate={v =>
|
|
||||||
(v.length > 1 && v.length < 32 && nameValidator.test(v))
|
|
||||||
|| "Name must be between 2 and 32 characters and only contain alphanumeric characters"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div style={{
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: "1em",
|
|
||||||
padding: "1em 0.5em",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center"
|
|
||||||
}}>
|
|
||||||
{guilds.map(g => (
|
|
||||||
<Tooltip text={g.name}>
|
|
||||||
{({ onMouseLeave, onMouseEnter }) => (
|
|
||||||
<div
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
role="button"
|
|
||||||
aria-label={"Clone to " + g.name}
|
|
||||||
aria-disabled={isCloning}
|
|
||||||
style={{
|
|
||||||
borderRadius: "50%",
|
|
||||||
backgroundColor: "var(--background-secondary)",
|
|
||||||
display: "inline-flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "4em",
|
|
||||||
height: "4em",
|
|
||||||
cursor: isCloning ? "not-allowed" : "pointer",
|
|
||||||
filter: isCloning ? "brightness(50%)" : "none"
|
|
||||||
}}
|
|
||||||
onClick={isCloning ? void 0 : async () => {
|
|
||||||
setIsCloning(true);
|
|
||||||
doClone(g.id, id, name, isAnimated).finally(() => {
|
|
||||||
invalidateMemo();
|
|
||||||
setIsCloning(false);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{g.icon ? (
|
|
||||||
<img
|
|
||||||
aria-hidden
|
|
||||||
style={{
|
|
||||||
borderRadius: "50%",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
src={g.getIconURL(512, true)}
|
|
||||||
alt={g.name}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Forms.FormText
|
|
||||||
style={{
|
|
||||||
fontSize: getFontSize(g.acronym),
|
|
||||||
width: "100%",
|
|
||||||
overflow: "hidden",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
textAlign: "center",
|
|
||||||
cursor: isCloning ? "not-allowed" : "pointer",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{g.acronym}
|
|
||||||
</Forms.FormText>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
migratePluginSettings("EmoteCloner", "EmoteYoink");
|
|
||||||
export default definePlugin({
|
|
||||||
name: "EmoteCloner",
|
|
||||||
description: "Adds a Clone context menu item to emotes to clone them your own server",
|
|
||||||
authors: [Devs.Ven],
|
|
||||||
dependencies: ["MenuItemDeobfuscatorAPI"],
|
|
||||||
|
|
||||||
patches: [{
|
|
||||||
// Literally copy pasted from ReverseImageSearch lol
|
|
||||||
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])"
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
// Also copy pasted from Reverse Image Search
|
|
||||||
{
|
|
||||||
// pass the target to the open link menu so we can grab its data
|
|
||||||
find: "REMOVE_ALL_REACTIONS_CONFIRM_BODY,",
|
|
||||||
predicate: makeLazy(() => !Settings.plugins.ReverseImageSearch.enabled),
|
|
||||||
noWarn: true,
|
|
||||||
replacement: {
|
|
||||||
match: /(?<props>.).onHeightUpdate.{0,200}(.)=(.)=.\.url;.+?\(null!=\3\?\3:\2[^)]+/,
|
|
||||||
replace: "$&,$<props>.target"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
|
|
||||||
makeMenu(htmlElement: HTMLImageElement) {
|
|
||||||
if (htmlElement?.dataset.type !== "emoji")
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const { id } = htmlElement.dataset;
|
|
||||||
const name = htmlElement.alt.match(/:(.*)(?:~\d+)?:/)?.[1];
|
|
||||||
|
|
||||||
if (!name || !id)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const isAnimated = new URL(htmlElement.src).pathname.endsWith(".gif");
|
|
||||||
|
|
||||||
return <Menu.MenuItem
|
|
||||||
id="emote-cloner"
|
|
||||||
key="emote-cloner"
|
|
||||||
label="Clone"
|
|
||||||
action={() =>
|
|
||||||
openModal(modalProps => (
|
|
||||||
<ModalRoot {...modalProps}>
|
|
||||||
<ModalHeader>
|
|
||||||
<img
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden
|
|
||||||
src={`https://cdn.discordapp.com/emojis/${id}.${isAnimated ? "gif" : "png"}`}
|
|
||||||
alt=""
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
style={{ marginRight: "0.5em" }}
|
|
||||||
/>
|
|
||||||
<Forms.FormText>Clone {name}</Forms.FormText>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalContent>
|
|
||||||
<CloneModal id={id} name={name} isAnimated={isAnimated} />
|
|
||||||
</ModalContent>
|
|
||||||
</ModalRoot>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
</Menu.MenuItem>;
|
|
||||||
},
|
|
||||||
});
|
|
@ -16,21 +16,24 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Settings } from "@api/settings";
|
import { Forms } from "@components";
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { findByPropsLazy } from "@webpack";
|
|
||||||
import { Forms, React } from "@webpack/common";
|
|
||||||
|
|
||||||
const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
|
import { lazyWebpack } from "../utils";
|
||||||
|
import { Devs } from "../utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "../utils/types";
|
||||||
|
import { Settings } from "../Vencord";
|
||||||
|
import { filters } from "../webpack";
|
||||||
|
import { React } from "../webpack/common";
|
||||||
|
|
||||||
|
const KbdStyles = lazyWebpack(filters.byProps(["key", "removeBuildOverride"]));
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Experiments",
|
name: "Experiments",
|
||||||
authors: [
|
authors: [
|
||||||
Devs.Megu,
|
Devs.Megu,
|
||||||
Devs.Ven,
|
Devs.Ven,
|
||||||
Devs.Nickyux,
|
{ name: "Nickyux", id: 427146305651998721n },
|
||||||
Devs.BanTheNons
|
{ name: "BanTheNons", id: 460478012794863637n },
|
||||||
],
|
],
|
||||||
description: "Enable Access to Experiments in Discord!",
|
description: "Enable Access to Experiments in Discord!",
|
||||||
patches: [{
|
patches: [{
|
||||||
@ -75,7 +78,7 @@ export default definePlugin({
|
|||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Forms.FormTitle tag="h3">More Information</Forms.FormTitle>
|
<Forms.FormTitle tag="h3">More Information</Forms.FormTitle>
|
||||||
<Forms.FormText variant="text-md/normal">
|
<Forms.FormText>
|
||||||
You can enable client DevTools{" "}
|
You can enable client DevTools{" "}
|
||||||
<kbd className={KbdStyles.key}>{modKey}</kbd> +{" "}
|
<kbd className={KbdStyles.key}>{modKey}</kbd> +{" "}
|
||||||
<kbd className={KbdStyles.key}>{altKey}</kbd> +{" "}
|
<kbd className={KbdStyles.key}>{altKey}</kbd> +{" "}
|
||||||
|
@ -1,309 +0,0 @@
|
|||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 Vendicated and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
|
|
||||||
import { migratePluginSettings, Settings } from "@api/settings";
|
|
||||||
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";
|
|
||||||
|
|
||||||
const DRAFT_TYPE = 0;
|
|
||||||
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
|
|
||||||
|
|
||||||
interface BaseSticker {
|
|
||||||
available: boolean;
|
|
||||||
description: string;
|
|
||||||
format_type: number;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
tags: string;
|
|
||||||
type: number;
|
|
||||||
}
|
|
||||||
interface GuildSticker extends BaseSticker {
|
|
||||||
guild_id: string;
|
|
||||||
}
|
|
||||||
interface DiscordSticker extends BaseSticker {
|
|
||||||
pack_id: string;
|
|
||||||
}
|
|
||||||
type Sticker = GuildSticker | DiscordSticker;
|
|
||||||
|
|
||||||
interface StickerPack {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
sku_id: string;
|
|
||||||
description: string;
|
|
||||||
cover_sticker_id: string;
|
|
||||||
banner_asset_id: string;
|
|
||||||
stickers: Sticker[];
|
|
||||||
}
|
|
||||||
|
|
||||||
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.",
|
|
||||||
dependencies: ["MessageEventsAPI"],
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "canUseAnimatedEmojis:function",
|
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
|
||||||
replacement: [
|
|
||||||
"canUseAnimatedEmojis",
|
|
||||||
"canUseEmojisEverywhere"
|
|
||||||
].map(func => {
|
|
||||||
return {
|
|
||||||
match: new RegExp(`${func}:function\\(.+?}`),
|
|
||||||
replace: `${func}:function(e){return true;}`
|
|
||||||
};
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "canUseAnimatedEmojis:function",
|
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
|
|
||||||
replacement: {
|
|
||||||
match: /canUseStickersEverywhere:function\(.+?}/,
|
|
||||||
replace: "canUseStickersEverywhere:function(e){return true;}"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "\"SENDABLE\"",
|
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
|
|
||||||
replacement: {
|
|
||||||
match: /(\w+)\.available\?/,
|
|
||||||
replace: "true?"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "canUseAnimatedEmojis:function",
|
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
|
|
||||||
replacement: [
|
|
||||||
"canUseHighVideoUploadQuality",
|
|
||||||
"canStreamHighQuality",
|
|
||||||
"canStreamMidQuality"
|
|
||||||
].map(func => {
|
|
||||||
return {
|
|
||||||
match: new RegExp(`${func}:function\\(.+?}`),
|
|
||||||
replace: `${func}:function(e){return true;}`
|
|
||||||
};
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "STREAM_FPS_OPTION.format",
|
|
||||||
predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
|
|
||||||
replacement: {
|
|
||||||
match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g,
|
|
||||||
replace: ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
options: {
|
|
||||||
enableEmojiBypass: {
|
|
||||||
description: "Allow sending fake emojis",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true,
|
|
||||||
},
|
|
||||||
emojiSize: {
|
|
||||||
description: "Size of the emojis when sending",
|
|
||||||
type: OptionType.SLIDER,
|
|
||||||
default: 48,
|
|
||||||
markers: [32, 48, 64, 128, 160, 256, 512],
|
|
||||||
},
|
|
||||||
enableStickerBypass: {
|
|
||||||
description: "Allow sending fake stickers",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true,
|
|
||||||
},
|
|
||||||
stickerSize: {
|
|
||||||
description: "Size of the stickers when sending",
|
|
||||||
type: OptionType.SLIDER,
|
|
||||||
default: 160,
|
|
||||||
markers: [32, 64, 128, 160, 256, 512],
|
|
||||||
},
|
|
||||||
enableStreamQualityBypass: {
|
|
||||||
description: "Allow streaming in nitro quality",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
get guildId() {
|
|
||||||
return window.location.href.split("channels/")[1].split("/")[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
get canUseEmotes() {
|
|
||||||
return (UserStore.getCurrentUser().premiumType ?? 0) > 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
get canUseStickers() {
|
|
||||||
return (UserStore.getCurrentUser().premiumType ?? 0) > 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
getStickerLink(stickerId: string) {
|
|
||||||
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.FakeNitro.stickerSize}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) {
|
|
||||||
const [{ parseURL }, {
|
|
||||||
GIFEncoder,
|
|
||||||
quantize,
|
|
||||||
applyPalette
|
|
||||||
}] = await Promise.all([importApngJs(), getGifEncoder()]);
|
|
||||||
|
|
||||||
const { frames, width, height } = await parseURL(stickerLink);
|
|
||||||
|
|
||||||
const gif = new GIFEncoder();
|
|
||||||
const resolution = Settings.plugins.FakeNitro.stickerSize;
|
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = resolution;
|
|
||||||
canvas.height = resolution;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d", {
|
|
||||||
willReadFrequently: true
|
|
||||||
})!;
|
|
||||||
|
|
||||||
const scale = resolution / Math.max(width, height);
|
|
||||||
ctx.scale(scale, scale);
|
|
||||||
|
|
||||||
let lastImg: HTMLImageElement | null = null;
|
|
||||||
for (const { left, top, width, height, disposeOp, img, delay } of frames) {
|
|
||||||
ctx.drawImage(img, left, top, width, height);
|
|
||||||
|
|
||||||
const { data } = ctx.getImageData(0, 0, resolution, resolution);
|
|
||||||
|
|
||||||
const palette = quantize(data, 256);
|
|
||||||
const index = applyPalette(data, palette);
|
|
||||||
|
|
||||||
gif.writeFrame(index, resolution, resolution, {
|
|
||||||
transparent: true,
|
|
||||||
palette,
|
|
||||||
delay,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (disposeOp === ApngDisposeOp.BACKGROUND) {
|
|
||||||
ctx.clearRect(left, top, width, height);
|
|
||||||
} else if (disposeOp === ApngDisposeOp.PREVIOUS && lastImg) {
|
|
||||||
ctx.drawImage(lastImg, left, top, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastImg = img;
|
|
||||||
}
|
|
||||||
|
|
||||||
gif.finish();
|
|
||||||
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
|
|
||||||
promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
|
|
||||||
},
|
|
||||||
|
|
||||||
start() {
|
|
||||||
const settings = Settings.plugins.FakeNitro;
|
|
||||||
if (!settings.enableEmojiBypass && !settings.enableStickerBypass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EmojiStore = findByPropsLazy("getCustomEmojiById");
|
|
||||||
const StickerStore = findByPropsLazy("getAllGuildStickers") as {
|
|
||||||
getPremiumPacks(): StickerPack[];
|
|
||||||
getAllGuildStickers(): Map<string, Sticker[]>;
|
|
||||||
getStickerById(id: string): Sticker | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getWordBoundary(origStr: string, offset: number) {
|
|
||||||
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.preSend = addPreSendListener((channelId, messageObj, extra) => {
|
|
||||||
const { guildId } = this;
|
|
||||||
|
|
||||||
stickerBypass: {
|
|
||||||
if (!settings.enableStickerBypass)
|
|
||||||
break stickerBypass;
|
|
||||||
|
|
||||||
const sticker = StickerStore.getStickerById(extra?.stickerIds?.[0]!);
|
|
||||||
if (!sticker)
|
|
||||||
break stickerBypass;
|
|
||||||
|
|
||||||
if (sticker.available !== false && (this.canUseStickers || (sticker as GuildSticker)?.guild_id === guildId))
|
|
||||||
break stickerBypass;
|
|
||||||
|
|
||||||
let link = this.getStickerLink(sticker.id);
|
|
||||||
if (sticker.format_type === 2) {
|
|
||||||
this.sendAnimatedSticker(this.getStickerLink(sticker.id), sticker.id, channelId);
|
|
||||||
return { cancel: true };
|
|
||||||
} else {
|
|
||||||
if ("pack_id" in sticker) {
|
|
||||||
const packId = sticker.pack_id === "847199849233514549"
|
|
||||||
// Discord moved these stickers into a different pack at some point, but
|
|
||||||
// Distok still uses the old id
|
|
||||||
? "749043879713701898"
|
|
||||||
: sticker.pack_id;
|
|
||||||
|
|
||||||
link = `https://distok.top/stickers/${packId}/${sticker.id}.gif`;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete extra.stickerIds;
|
|
||||||
messageObj.content += " " + link;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.canUseEmotes && settings.enableEmojiBypass) {
|
|
||||||
for (const emoji of messageObj.validNonShortcutEmojis) {
|
|
||||||
if (!emoji.require_colons) continue;
|
|
||||||
if (emoji.guildId === guildId && !emoji.animated) continue;
|
|
||||||
|
|
||||||
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
|
|
||||||
const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`);
|
|
||||||
messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {
|
|
||||||
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { cancel: false };
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!this.canUseEmotes && settings.enableEmojiBypass) {
|
|
||||||
this.preEdit = addPreEditListener((_, __, messageObj) => {
|
|
||||||
const { guildId } = this;
|
|
||||||
|
|
||||||
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
|
|
||||||
const emoji = EmojiStore.getCustomEmojiById(emojiId);
|
|
||||||
if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
|
|
||||||
if (!emoji.require_colons) continue;
|
|
||||||
|
|
||||||
const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`);
|
|
||||||
messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => {
|
|
||||||
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
removePreSendListener(this.preSend);
|
|
||||||
removePreEditListener(this.preEdit);
|
|
||||||
}
|
|
||||||
});
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user