Compare commits

..

2 Commits

Author SHA1 Message Date
Rie Takahashi
c3da99eeee linting and stuff 2022-10-24 18:43:06 +01:00
Rie Takahashi
0e7bd87cee wip: components library 2022-10-24 18:04:25 +01:00
215 changed files with 1703 additions and 12482 deletions

View File

@ -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
View File

@ -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']

View File

@ -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/*

View File

@ -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 }}

View File

@ -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
View File

@ -18,6 +18,3 @@ lerna-debug.log*
*.tsbuildinfo *.tsbuildinfo
src/userplugins src/userplugins
ExtensionCache/
settings/

1
.npmrc
View File

@ -1 +0,0 @@
strict-peer-dependencies=false

View File

@ -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
View File

@ -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"]
}
]
}

View File

@ -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 [![Chrome](https://img.shields.io/badge/chrome-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip), [![Firefox](https://img.shields.io/badge/firefox-ext-brightgreen)](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!

View File

@ -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;

View File

@ -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) {
if (details.type === "stylesheet") {
details.responseHeaders = details.responseHeaders.filter(it => it.name.toLowerCase() !== 'content-type');
details.responseHeaders.push({ name: "Content-Type", value: "text/css" });
}
return { responseHeaders: details.responseHeaders };
} }
var cspHeaders = [ browser.webRequest.onHeadersReceived.addListener(({ responseHeaders, url }) => {
"content-security-policy", const cspIdx = responseHeaders.findIndex(h => h.name === "content-security-policy");
"content-security-policy-report-only", if (cspIdx !== -1)
]; 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"
});
}
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"]
);

View File

@ -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"
]
} }

View File

@ -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"
}
]
}
}

View File

@ -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/*"
}
}
]

View File

@ -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!

View File

@ -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",

View File

@ -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"
}
} }

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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 => {

View File

@ -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 => [
f,
await readFile(join("browser", f)) await readFile(join("browser", f))
]))), ]))),
}; }, {}, (err, data) => {
if (shouldZip) {
zip(entries, {}, (err, data) => {
if (err) { if (err) {
console.error(err); console.error(err);
process.exitCode = 1; process.exitCode = 1;
} else { } else {
writeFileSync("dist/" + target, data); writeFileSync("dist/extension.zip", data);
console.info("Extension written to dist/" + target); console.info("Extension written to dist/extension.zip");
} }
}); });
} else {
if (existsSync(target))
rmSync(target, { recursive: true });
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);

View File

@ -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"
}; };

View File

@ -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);

View File

@ -1,7 +0,0 @@
// Work around https://github.com/evanw/esbuild/issues/2460
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react"
}
}

View File

@ -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);
})();

View File

@ -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`,
]; ];

View File

@ -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) {

View File

@ -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();

View File

@ -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.`);
} }

View File

@ -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;
}

View File

@ -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);

View File

@ -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);
} }

View File

@ -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 {

View File

@ -16,10 +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 Logger from "@utils/Logger";
import { MessageStore } from "@webpack/common";
import type { Channel,Message } from "discord-types/general"; 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");
export interface Emoji { export interface Emoji {
@ -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);
}
} }
} }

View File

@ -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;
}

View File

@ -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);

View File

@ -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;
};

View File

@ -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 };

View File

@ -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;
}
}
}

View File

@ -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}
/>
</>
);
}

View File

@ -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>
);
}

View File

@ -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,10 +32,15 @@ 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>
<Component {...props as any/* I hate react typings ??? */} />
</ErrorBoundary>
);
}
state = { state = {
error: NO_ERROR as any, error: NO_ERROR as any,
stack: "", stack: "",
@ -73,8 +71,6 @@ const ErrorBoundary = LazyComponent(() => {
render() { render() {
if (this.state.error === NO_ERROR) return this.props.children; if (this.state.error === NO_ERROR) return this.props.children;
if (this.props.noop) return null;
if (this.props.fallback) if (this.props.fallback)
return <this.props.fallback return <this.props.fallback
children={this.props.children} children={this.props.children}
@ -100,16 +96,4 @@ const ErrorBoundary = LazyComponent(() => {
</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;

View File

@ -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;

View File

@ -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}

View File

@ -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>
);
}

View File

@ -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>
); );

View File

@ -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);
} }

View File

@ -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;

View File

@ -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,14 +196,13 @@ 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}
> >
Cancel Exit Without Saving
</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 }) => (
@ -226,13 +214,11 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
disabled={!canSubmit()} disabled={!canSubmit()}
> >
Save & Close Save & Exit
</Button> </Button>
)} )}
</Tooltip> </Tooltip>
</Flex> </Flex>
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>}
</Flex>
</ModalFooter> </ModalFooter>
</ModalRoot> </ModalRoot>
); );

View File

@ -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>) {

View File

@ -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 });
}

View File

@ -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);

View File

@ -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>
); );
} }

View File

@ -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) {

View File

@ -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);

View File

@ -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";

View File

@ -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
View 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>
);
});

View File

@ -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,
});

View File

@ -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>&mdash; Custom QuickCSS</li>
<li>&mdash; 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);

View File

@ -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} />
</>
);
});

View File

@ -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);

View File

@ -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>;
}

View File

@ -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;
}

View File

@ -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!");
}
}
});
}
}

View File

@ -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";

View File

@ -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
View File

@ -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;

View File

@ -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}`);

View File

@ -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");
} }

View File

@ -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
View File

@ -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;
} }

View File

@ -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,7 +41,6 @@ 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");
@ -76,9 +74,15 @@ if (!process.argv.includes("--vanilla")) {
}; };
// Patch appSettings to force enable devtools // Patch appSettings to force enable devtools
onceDefined(global, "appSettings", s => Object.defineProperty(global, "appSettings", {
s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true) 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"); process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
@ -107,57 +111,20 @@ if (!process.argv.includes("--vanilla")) {
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err)); .catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
} catch { } } catch { }
// Remove CSP // Remove CSP
type PolicyResult = Record<string, string[]>; electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, url }, cb) => {
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 (responseHeaders) {
if (resourceType === "mainFrame") delete responseHeaders["content-security-policy-report-only"];
patchCsp(responseHeaders, "content-security-policy"); delete responseHeaders["content-security-policy"];
// Fix hosts that don't properly set the css content type, such as // Fix hosts that don't properly set the content type, such as
// raw.githubusercontent.com // raw.githubusercontent.com
if (resourceType === "stylesheet") if (url.endsWith(".css"))
responseHeaders["content-type"] = ["text/css"]; responseHeaders["content-type"] = ["text/css"];
} }
cb({ cancel: false, responseHeaders }); cb({ cancel: false, responseHeaders });
}); });
}); });
} else {
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
}
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);
} }

View File

@ -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
}
}
});

View File

@ -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: [{

View File

@ -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"
}
}
],
});

View File

@ -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";

View File

@ -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>
));
},
});
}
}
});

View File

@ -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||($&)"
}
} }
], ],
}); });

View File

@ -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}`;
},
},
},
],
});

View File

@ -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`,
}, },
}, },
], ],

View File

@ -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) =>

View File

@ -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"
}
}],
});

View File

@ -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,

View File

@ -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)"
}
}
]
});

View File

@ -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
}
});

View File

@ -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.",

View File

@ -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))",
}, },
}, },
], ],

View File

@ -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})`,
}, },
}, },
], ],

View File

@ -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();
}
});

View File

@ -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>;
}
});

View File

@ -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) {

View File

@ -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
} }
}); });
}, }
}); });

View File

@ -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,

View File

@ -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));
}
}]
});

View File

@ -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}\``
});
}
}
}
]
});

View File

@ -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>;
},
});

View File

@ -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> +{" "}

View File

@ -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