Compare commits

..

4 Commits

Author SHA1 Message Date
Rie Takahashi
0e06b8d34c grammar lol 2023-02-22 03:45:56 +00:00
Rie Takahashi
b972aa1663 fix some labels in settings 2023-02-22 03:44:47 +00:00
Rie Takahashi
3bf81ee0fa make each notification type toggleable 2023-02-22 03:42:19 +00:00
Rie Takahashi
486230a335 feat(plugins): add relationship notifier plugin 2023-02-22 03:13:39 +00:00
276 changed files with 4768 additions and 13802 deletions

View File

@ -62,7 +62,7 @@
"indent": ["error", 4, { "SwitchCase": 1 }],
"arrow-parens": ["error", "as-needed"],
"eol-last": ["error", "always"],
"@typescript-eslint/func-call-spacing": ["error", "never"],
"func-call-spacing": ["error", "never"],
"no-multi-spaces": "error",
"no-trailing-spaces": "error",
"no-whitespace-before-property": "error",

View File

@ -37,12 +37,9 @@ jobs:
- name: Build
run: pnpm build --standalone
- name: Generate plugin list
run: pnpm generatePluginJson dist/plugins.json
- name: Clean up obsolete files
run: |
rm -rf dist/*-unpacked Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
rm -rf dist/extension* Vencord.user.css
- name: Get some values needed for the release
id: release_values

View File

@ -35,15 +35,15 @@ jobs:
- name: Publish extension
run: |
cd dist/extension-unpacked
# Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later
EXIT_CODE=0
# Chrome
cd dist/chromium-unpacked
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$?
# Firefox
cd ../firefox-unpacked
npm i -g web-ext@7.4.0 web-ext-submit@7.4.0
web-ext-submit || EXIT_CODE=$?
@ -58,3 +58,4 @@ jobs:
# Firefox
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}

View File

@ -36,7 +36,7 @@ jobs:
export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=$(which chromium-browser)
esbuild scripts/generateReport.ts > dist/report.mjs
esbuild test/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
@ -50,7 +50,7 @@ jobs:
export CHROMIUM_BIN=$(which chromium-browser)
export USE_CANARY=true
esbuild scripts/generateReport.ts > dist/report.mjs
esbuild test/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}

View File

@ -15,7 +15,7 @@ jobs:
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 18
uses: actions/setup-node@v3
uses: actions/setup-node@v2
with:
node-version: 18
cache: "pnpm"
@ -26,8 +26,5 @@ jobs:
- name: Lint & Test if desktop version compiles
run: pnpm test
- name: Test if web version compiles
run: pnpm buildWeb
- name: Test if plugin structure is valid
run: pnpm generatePluginJson
- name: Lint & Test if web version compiles
run: pnpm testWeb

View File

@ -1,20 +0,0 @@
# Code of Conduct
Our community is welcoming to everyone, regardless of their characteristics.
As such, we expect you to treat everyone with respect and contribute to an open and welcoming community.
DO
- have empathy and be nice to others
- be respectful of differing opinions, even if you disagree
- give and accept constructive criticism
DON'T
- use offensive or derogatory language
- troll or spam
- personally attack or harass others
Repetitive violations of these guidelines might get your access to the repository restricted.
If you feel like a user is violating these guidelines or feel treated unfairly, please refrain from publicly challenging them and instead contact a Moderator on our Discord server or send an email to vendicated+conduct@riseup.net!

View File

@ -2,60 +2,40 @@
The cutest Discord client mod
![](https://user-images.githubusercontent.com/45497981/235015332-0453d3eb-1da6-4601-963e-ef5e454123a1.png)
*A screenshot of Vencord featuring the [ClearVision-v6 theme](https://github.com/ClearVision/ClearVision-v6) (Vencord does not come with it pre-installed, it is only an example)*
## Features
- Super easy to install (Download Installer, open, click install button, done)
- 100+ plugins built in: [See a list](https://vencord.dev/plugins)
- Some highlights: SpotifyControls, MessageLogger, Experiments, GameActivityToggle, Translate, NoTrack, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
- Fairly lightweight despite the many inbuilt plugins
- Super easy to install (one click installer)
- 90+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
- Privacy friendly, blocks Discord analytics & crash reporting out of the box and has no telemetry
- Works in all Electron versions (Confirmed working on versions 13-23)
- Maintained very actively, broken plugins are usually fixed within 12 hours
- Settings sync: Keep your plugins and their settings synchronised between devices / apps (optional)
## Installing / Uninstalling
Click the below button to install Vencord to the Discord Desktop app
[![Download and run the Installer](https://img.shields.io/github/v/release/Vencord/Installer?label=Download%20Vencord%20Installer&style=for-the-badge)](https://github.com/Vencord/Installer#vencord-installer)
[![Download and run the Installer ](https://img.shields.io/github/v/release/Vencord/Installer?label=Download%20Vencord%20Installer&style=for-the-badge)](https://github.com/Vencord/Installer#usage)
## Installing on Browser
[![Get it on the Firefox Webstore](https://blog.mozilla.org/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [![Get it on the Chrome Webstore](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that the CSS Editor, Themes loaded from remote sources and co. will not work in the UserScript. Use the extension if you need any of those
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that QuickCSS and plugins making use of external resources will not work with the UserScript.
## Installing our Desktop App
## Building from Source
As an alternative to the Discord Desktop app, Vencord also has its own standalone Desktop app that is snappier and lighter than Discord's official Desktop app. It is currently in beta and we have yet to implement some features like screensharing, but you can try the beta nonetheless
See the docs folder
[![Download Vencord Desktop](https://img.shields.io/github/v/release/Vencord/Desktop?label=Download%20Vencord%20Desktop&style=for-the-badge)](https://github.com/Vencord/Desktop#vencord-desktop)
## Contributing
## Join our Support/Community Server
See [CONTRIBUTING.md](CONTRIBUTING.md) and [Megu's Plugin Guide!](docs/2_PLUGINS.md)
[![Vencord Discord Server](https://invidget.switchblade.xyz/D9uwnFnqmd?theme=dark)](https://discord.gg/D9uwnFnqmd)
[contribute]: CONTRIBUTING.md
## Disclaimer
[contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute]
Discord is trademark of Discord Inc. and solely mentioned for the sake of descriptivity.
Mention of it does not imply any affiliation with or endorsement by Discord Inc.
## Join
<details>
<summary>Using Vencord violates Discord's terms of service</summary>
[join]: https://discord.gg/D9uwnFnqmd
Client modifications are against Discords Terms of Service.
However, Discord is pretty indifferent about them and there are no known cases of users getting banned for using client mods! So you should generally be fine as long as you dont use any plugins that implement abusive behaviour. But no worries, all inbuilt plugins are safe to use!
Regardless, if your account is very important to you and it getting disabled would be a disaster for you, you should probably not use any client mods (not exclusive to Vencord), just to be safe
Additionally, make sure not to post screenshots with Vencord in a server where you might get banned for it
</details>
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]

View File

@ -59,10 +59,8 @@ async function checkCors(url, method) {
const origin = headers["access-control-allow-origin"];
if (origin !== "*" && origin !== window.location.origin) return false;
const methods = headers["access-control-allow-methods"]?.toLowerCase()
.split(",")
.map(s => s.trim());
if (methods && !methods.includes(method.toLowerCase())) return false;
const methods = headers["access-control-allow-methods"]?.split(/,\s/g);
if (methods && !methods.includes(method)) return false;
return true;
}
@ -94,7 +92,6 @@ function GM_fetch(url, opt) {
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
resp.text = () => blobTo("text", blob);
resp.json = async () => JSON.parse(await blobTo("text", blob));
resp.headers = new Headers(parseHeaders(resp.responseHeaders));
resolve(resp);
};
options.ontimeout = () => reject("fetch timeout");

View File

@ -16,70 +16,51 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/// <reference path="../src/modules.d.ts" />
/// <reference path="../src/globals.d.ts" />
import monacoHtml from "~fileContent/../src/components/monacoWin.html";
import * as DataStore from "../src/api/DataStore";
import { debounce } from "../src/utils";
import { getTheme, Theme } from "../src/utils/discord";
import IpcEvents from "../src/utils/IpcEvents";
// Discord deletes this so need to store in variable
const { localStorage } = window;
// listeners for ipc.on
const cssListeners = new Set<(css: string) => void>();
const NOOP = () => { };
const NOOP_ASYNC = async () => { };
const listeners = {} as Record<string, Set<Function>>;
const setCssDebounced = debounce((css: string) => VencordNative.quickCss.set(css));
const handlers = {
[IpcEvents.GET_REPO]: () => "https://github.com/Vendicated/Vencord", // shrug
[IpcEvents.GET_SETTINGS_DIR]: () => "LocalStorage",
[IpcEvents.GET_QUICK_CSS]: () => DataStore.get("VencordQuickCss").then(s => s ?? ""),
[IpcEvents.SET_QUICK_CSS]: (css: string) => {
DataStore.set("VencordQuickCss", css);
listeners[IpcEvents.QUICK_CSS_UPDATE]?.forEach(l => l(null, css));
},
[IpcEvents.GET_SETTINGS]: () => localStorage.getItem("VencordSettings") || "{}",
[IpcEvents.SET_SETTINGS]: (s: string) => localStorage.setItem("VencordSettings", s),
[IpcEvents.GET_UPDATES]: () => ({ ok: true, value: [] }),
[IpcEvents.OPEN_EXTERNAL]: (url: string) => open(url, "_blank"),
};
function onEvent(event: string, ...args: any[]) {
const handler = handlers[event];
if (!handler) throw new Error(`Event ${event} not implemented.`);
return handler(...args);
}
// probably should make this less cursed at some point
window.VencordNative = {
native: {
getVersions: () => ({}),
openExternal: async (url) => void open(url, "_blank")
ipc: {
send: (event: string, ...args: any[]) => void onEvent(event, ...args),
sendSync: onEvent,
on(event: string, listener: () => {}) {
(listeners[event] ??= new Set()).add(listener);
},
updater: {
getRepo: async () => ({ ok: true, value: "https://github.com/Vendicated/Vencord" }),
getUpdates: async () => ({ ok: true, value: [] }),
update: async () => ({ ok: true, value: false }),
rebuild: async () => ({ ok: true, value: true }),
off(event: string, listener: () => {}) {
return listeners[event]?.delete(listener);
},
quickCss: {
get: () => DataStore.get("VencordQuickCss").then(s => s ?? ""),
set: async (css: string) => {
await DataStore.set("VencordQuickCss", css);
cssListeners.forEach(l => l(css));
invoke: (event: string, ...args: any[]) => Promise.resolve(onEvent(event, ...args))
},
addChangeListener(cb) {
cssListeners.add(cb);
},
openFile: NOOP_ASYNC,
async openEditor() {
const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
const win = open("about:blank", "VencordQuickCss", features);
if (!win) {
alert("Failed to open QuickCSS popup. Make sure to allow popups!");
return;
}
win.setCss = setCssDebounced;
win.getCurrentCss = () => VencordNative.quickCss.get();
win.getTheme = () =>
getTheme() === Theme.Light
? "vs-light"
: "vs-dark";
win.document.write(monacoHtml);
},
},
settings: {
get: () => localStorage.getItem("VencordSettings") || "{}",
set: async (s: string) => localStorage.setItem("VencordSettings", s),
getSettingsDir: async () => "LocalStorage"
}
};

View File

@ -1,32 +0,0 @@
/**
* @template T
* @param {T[]} arr
* @param {(v: T) => boolean} predicate
*/
function removeFirst(arr, predicate) {
const idx = arr.findIndex(predicate);
if (idx !== -1) arr.splice(idx, 1);
}
chrome.webRequest.onHeadersReceived.addListener(
({ responseHeaders, type, url }) => {
if (!responseHeaders) return;
if (type === "main_frame") {
// In main frame requests, the CSP needs to be removed to enable fetching of custom css
// as desired by the user
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-security-policy");
} else if (type === "stylesheet" && url.startsWith("https://raw.githubusercontent.com/")) {
// Most users will load css from GitHub, but GitHub doesn't set the correct content type,
// so we fix it here
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-type");
responseHeaders.push({
name: "Content-Type",
value: "text/css"
});
}
return { responseHeaders };
},
{ urls: ["https://raw.githubusercontent.com/*", "*://*.discord.com/*"], types: ["main_frame", "stylesheet"] },
["blocking", "responseHeaders"]
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -21,8 +21,7 @@
{
"run_at": "document_start",
"matches": ["*://*.discord.com/*"],
"js": ["content.js"],
"all_frames": true
"js": ["content.js"]
}
],

View File

@ -1,41 +0,0 @@
{
"manifest_version": 2,
"minimum_chrome_version": "91",
"name": "Vencord Web",
"description": "The cutest Discord mod now in your browser",
"author": "Vendicated",
"homepage_url": "https://github.com/Vendicated/Vencord",
"icons": {
"128": "icon.png"
},
"permissions": [
"webRequest",
"webRequestBlocking",
"*://*.discord.com/*",
"https://raw.githubusercontent.com/*"
],
"content_scripts": [
{
"run_at": "document_start",
"matches": ["*://*.discord.com/*"],
"js": ["content.js"],
"all_frames": true
}
],
"background": {
"scripts": ["background.js"]
},
"web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"],
"browser_specific_settings": {
"gecko": {
"id": "vencord-firefox@vendicated.dev",
"strict_min_version": "91.0"
}
}
}

View File

@ -15,7 +15,7 @@
]
},
"condition": {
"resourceTypes": ["main_frame", "sub_frame"]
"resourceTypes": ["main_frame"]
}
},
{

View File

@ -13,6 +13,12 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
- [Installing Vencord](#installing-vencord)
- [Updating Vencord](#updating-vencord)
- [Uninstalling Vencord](#uninstalling-vencord)
- [Manually Installing Vencord](#manually-installing-vencord)
- [On Windows](#on-windows)
- [On Linux](#on-linux)
- [On MacOS](#on-macos)
- [Manual Patching](#manual-patching)
- [Manually Uninstalling Vencord](#manually-uninstalling-vencord)
## Dependencies
@ -21,16 +27,16 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
## Installing Vencord
> :exclamation: If this doesn't work, see [Manually Installing Vencord](#manually-installing-vencord)
Install `pnpm`:
> :exclamation: This next command may need to be run as admin/root depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
> :exclamation: this may need to be run as admin depending on your system, and you may need to close and reopen your terminal.
```shell
npm i -g pnpm
```
> :exclamation: **IMPORTANT** Make sure you aren't using an admin/root terminal from here onwards. It **will** mess up your Discord/Vencord instance and you **will** most likely have to reinstall.
Clone Vencord:
```shell
@ -95,4 +101,102 @@ Simply run:
pnpm uninject
```
The above command may ask you to also run:
```shell
pnpm install --frozen-lockfile
pnpm uninject
```
## Manually Installing Vencord
- [Windows](#on-windows)
- [Linux](#on-linux)
- [MacOS](#on-macos)
### On Windows
Press Win+R and enter: `%LocalAppData%` and hit enter. In this page, find the page (Discord, DiscordPTB, DiscordCanary, etc) that you want to patch.
Now follow the instructions at [Manual Patching](#manual-patching)
### On Linux
The Discord folder is usually in one of the following paths:
- /usr/share
- /usr/lib64
- /opt
- /home/$USER/.local/share
If you use flatpak, it will usually be in one of the following paths:
- /var/lib/flatpak/app/com.discordapp.Discord/current/active/files
- /home/$USER/.local/share/flatpak/app/com.discordapp.Discord/current/active/files
You will need to give flatpak access to vencord with one of the following commands:
> :exclamation: If not on stable, replace `com.discordapp.Discord` with your branch name, e.g., `com.discordapp.DiscordCanary`
> :exclamation: Replace `/path/to/vencord/` with the path to your vencord folder (NOT the dist folder)
If Discord flatpak install is in /home/:
```shell
flatpak override --user com.discordapp.Discord --filesystem="/path/to/vencord/"
```
If Discord flatpak install not in /home/:
```shell
sudo flatpak override com.discordapp.Discord --filesystem="/path/to/vencord"
```
Now follow the instructions at [Manual Patching](#manual-patching)
### On MacOS
Open finder and go to your Applications folder. Right-Click on the Discord application you want to patch, and view contents.
Go to the `Contents/Resources` folder.
Now follow the instructions at [Manual Patching](#manual-patching)
### Manual Patching
> :exclamation: If using Flatpak on linux, go to the folder that contains the `app.asar` file, and skip to where we create the `app` folder below.
> :exclamation: On Linux/MacOS, there's a chance there won't be an `app-<number>` folder, but there probably is a `resources` folder, so keep reading :)
Inside there, look for the `app-<number>` folders. If you have multiple, use the highest number. If that doesn't work, do it for the rest of the `app-<number>` folders.
Inside there, go to the `resources` folder. There should be a file called `app.asar`. If there isn't, look at a different `app-<number>` folder instead.
Make a new folder in `resources` called `app`. In here, we will make two files:
`package.json` and `index.js`
In `index.js`:
> :exclamation: Replace the path in the first line with the path to `patcher.js` in your vencord dist folder.
> On Windows, you can get this by shift-rightclicking the patcher.js file and selecting "copy as path"
```js
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
```
And in `package.json`:
```json
{ "name": "discord", "main": "index.js" }
```
Finally, fully close & reopen your Discord client and check to see that `Vencord` appears in settings!
### Manually Uninstalling Vencord
> :exclamation: Do not delete `app.asar` - Only delete the `app` folder we created.
Use the instructions above to find the `app` folder, and delete it. Then Close & Reopen Discord.
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).

View File

@ -1,8 +1,9 @@
{
"name": "vencord",
"private": "true",
"version": "1.2.2",
"version": "1.0.6",
"description": "The cutest Discord client mod",
"keywords": [],
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
"url": "https://github.com/Vendicated/Vencord/issues"
@ -19,10 +20,9 @@
"scripts": {
"build": "node scripts/build/build.mjs",
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
"generatePluginJson": "tsx scripts/generatePluginList.ts",
"inject": "node scripts/runInstaller.mjs",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint-styles": "stylelint \"src/**/*.css\"",
"lint:fix": "pnpm lint --fix",
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc",
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
@ -32,20 +32,18 @@
},
"dependencies": {
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.5",
"fflate": "^0.7.4",
"nanoid": "^4.0.2",
"virtual-merge": "^1.0.1"
"@vap/shiki": "0.10.3",
"fflate": "^0.7.4"
},
"devDependencies": {
"@types/diff": "^5.0.3",
"@types/lodash": "^4.14.194",
"@types/node": "^18.16.3",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@types/diff": "^5.0.2",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.49.0",
"diff": "^5.1.0",
"discord-types": "^1.3.26",
"esbuild": "^0.15.18",
@ -53,19 +51,18 @@
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-path-alias": "^1.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-simple-import-sort": "^8.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"highlight.js": "10.6.0",
"moment": "^2.29.4",
"puppeteer-core": "^19.11.1",
"puppeteer-core": "^19.6.0",
"standalone-electron-types": "^1.0.0",
"stylelint": "^15.6.0",
"stylelint-config-standard": "^33.0.0",
"tsx": "^3.12.7",
"type-fest": "^3.9.0",
"typescript": "^5.0.4"
"stylelint": "^14.16.1",
"stylelint-config-standard": "^29.0.0",
"type-fest": "^3.5.3",
"typescript": "^4.9.4"
},
"packageManager": "pnpm@8.1.1",
"packageManager": "pnpm@7.13.4",
"pnpm": {
"patchedDependencies": {
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
@ -92,7 +89,6 @@
"sourceDir": "./dist/extension-v2-unpacked"
},
"engines": {
"node": ">=18",
"pnpm": ">=8"
"node": ">=18"
}
}

1725
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -48,7 +48,6 @@ const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.j
const sourcemap = watch ? "inline" : "external";
await Promise.all([
// common preload
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
@ -56,19 +55,12 @@ await Promise.all([
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
sourcemap,
}),
// Discord Desktop main & renderer
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/main/index.ts"],
entryPoints: ["src/patcher.ts"],
outfile: "dist/patcher.js",
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: true,
IS_VENCORD_DESKTOP: false
}
}),
esbuild.build({
...commonOpts,
@ -80,48 +72,12 @@ await Promise.all([
globalName: "Vencord",
sourcemap,
plugins: [
globPlugins("discordDesktop"),
globPlugins,
...commonOpts.plugins
],
define: {
...defines,
IS_WEB: false,
IS_DISCORD_DESKTOP: true,
IS_VENCORD_DESKTOP: false
}
}),
// Vencord Desktop main & renderer
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/vencordDesktopMain.js",
footer: { js: "//# sourceURL=VencordDesktopMain\n" + sourceMapFooter("vencordDesktopMain") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: false,
IS_VENCORD_DESKTOP: true
}
}),
esbuild.build({
...commonOpts,
entryPoints: ["src/Vencord.ts"],
outfile: "dist/vencordDesktopRenderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "//# sourceURL=VencordDesktopRenderer\n" + sourceMapFooter("vencordDesktopRenderer") },
globalName: "Vencord",
sourcemap,
plugins: [
globPlugins("vencordDesktop"),
...commonOpts.plugins
],
define: {
...defines,
IS_WEB: false,
IS_DISCORD_DESKTOP: false,
IS_VENCORD_DESKTOP: true
IS_WEB: false
}
}),
]).catch(err => {

View File

@ -36,18 +36,16 @@ const commonOptions = {
entryPoints: ["browser/Vencord.ts"],
globalName: "Vencord",
format: "iife",
external: ["plugins", "git-hash", "/assets/*"],
external: ["plugins", "git-hash"],
plugins: [
globPlugins("web"),
globPlugins,
...commonOpts.plugins,
],
target: ["esnext"],
define: {
IS_WEB: "true",
IS_STANDALONE: "true",
IS_DEV: JSON.stringify(watch),
IS_DISCORD_DESKTOP: "false",
IS_VENCORD_DESKTOP: "false"
IS_DEV: JSON.stringify(watch)
}
};
@ -142,7 +140,6 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
await Promise.all([
appendCssRuntime,
buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true),
buildPluginZip("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
buildPluginZip("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"], false),
buildPluginZip("extension-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
]);

View File

@ -33,8 +33,6 @@ export const banner = {
`.trim()
};
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
/**
* @type {import("esbuild").Plugin}
@ -48,9 +46,9 @@ export const makeAllPackagesExternalPlugin = {
};
/**
* @type {(kind: "web" | "discordDesktop" | "vencordDesktop") => import("esbuild").Plugin}
* @type {import("esbuild").Plugin}
*/
export const globPlugins = kind => ({
export const globPlugins = {
name: "glob-plugins",
setup: build => {
const filter = /^~plugins$/;
@ -71,17 +69,9 @@ export const globPlugins = kind => ({
const files = await readdir(`./src/${dir}`);
for (const file of files) {
if (file.startsWith(".")) continue;
if (file === "index.ts") continue;
const fileBits = file.split(".");
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1))) {
const mod = fileBits.at(-2);
if (mod === "dev" && !watch) continue;
if (mod === "web" && kind === "discordDesktop") continue;
if (mod === "desktop" && kind === "web") continue;
if (mod === "discordDesktop" && kind !== "discordDesktop") continue;
if (mod === "vencordDesktop" && kind !== "vencordDesktop") continue;
if (file === "index.ts") {
continue;
}
const mod = `p${i}`;
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
plugins += `[${mod}.name]:${mod},\n`;
@ -95,7 +85,7 @@ export const globPlugins = kind => ({
};
});
}
});
};
/**
* @type {import("esbuild").Plugin}
@ -195,7 +185,7 @@ export const commonOpts = {
legalComments: "linked",
banner,
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
external: ["~plugins", "~git-hash", "~git-remote"],
inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement",
jsxFragment: "VencordFragment",

View File

@ -1,200 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Dirent, readdirSync, readFileSync, writeFileSync } from "fs";
import { access, readFile } from "fs/promises";
import { join } from "path";
import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
interface Dev {
name: string;
id: string;
}
interface PluginData {
name: string;
description: string;
tags: string[];
authors: Dev[];
dependencies: string[];
hasPatches: boolean;
hasCommands: boolean;
required: boolean;
enabledByDefault: boolean;
target: "discordDesktop" | "vencordDesktop" | "web" | "dev";
}
const devs = {} as Record<string, Dev>;
function getName(node: NamedDeclaration) {
return node.name && isIdentifier(node.name) ? node.name.text : undefined;
}
function hasName(node: NamedDeclaration, name: string) {
return getName(node) === name;
}
function getObjectProp(node: ObjectLiteralExpression, name: string) {
const prop = node.properties.find(p => hasName(p, name));
if (prop && isPropertyAssignment(prop)) return prop.initializer;
return prop;
}
function parseDevs() {
const file = createSourceFile("constants.ts", readFileSync("src/utils/constants.ts", "utf8"), ScriptTarget.Latest);
for (const child of file.getChildAt(0).getChildren()) {
if (!isVariableStatement(child)) continue;
const devsDeclaration = child.declarationList.declarations.find(d => hasName(d, "Devs"));
if (!devsDeclaration?.initializer || !isCallExpression(devsDeclaration.initializer)) continue;
const value = devsDeclaration.initializer.arguments[0];
if (!isObjectLiteralExpression(value)) return;
for (const prop of value.properties) {
const name = (prop.name as Identifier).text;
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
if (!isObjectLiteralExpression(value)) throw new Error(`Failed to parse devs: ${name} is not an object literal`);
devs[name] = {
name: (getObjectProp(value, "name") as StringLiteral).text,
id: (getObjectProp(value, "id") as BigIntLiteral).text.slice(0, -1)
};
}
return;
}
throw new Error("Could not find Devs constant");
}
async function parseFile(fileName: string) {
const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest);
const fail = (reason: string) => {
return new Error(`Invalid plugin ${fileName}, because ${reason}`);
};
for (const node of file.getChildAt(0).getChildren()) {
if (!isExportAssignment(node) || !isCallExpression(node.expression)) continue;
const call = node.expression;
if (!isIdentifier(call.expression) || call.expression.text !== "definePlugin") continue;
const pluginObj = node.expression.arguments[0];
if (!isObjectLiteralExpression(pluginObj)) throw fail("no object literal passed to definePlugin");
const data = {
hasPatches: false,
hasCommands: false,
enabledByDefault: false,
required: false,
tags: [] as string[]
} as PluginData;
for (const prop of pluginObj.properties) {
const key = getName(prop);
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
switch (key) {
case "name":
case "description":
if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);
data[key] = value.text;
break;
case "patches":
data.hasPatches = true;
break;
case "commands":
data.hasCommands = true;
break;
case "authors":
if (!isArrayLiteralExpression(value)) throw fail("authors is not an array literal");
data.authors = value.elements.map(e => {
if (!isPropertyAccessExpression(e)) throw fail("authors array contains non-property access expressions");
return devs[getName(e)!];
});
break;
case "tags":
if (!isArrayLiteralExpression(value)) throw fail("tags is not an array literal");
data.tags = value.elements.map(e => {
if (!isStringLiteral(e)) throw fail("tags array contains non-string literals");
return e.text;
});
break;
case "dependencies":
if (!isArrayLiteralExpression(value)) throw fail("dependencies is not an array literal");
const { elements } = value;
if (elements.some(e => !isStringLiteral(e))) throw fail("dependencies array contains non-string elements");
data.dependencies = (elements as NodeArray<StringLiteral>).map(e => e.text);
break;
case "required":
case "enabledByDefault":
data[key] = value.kind === SyntaxKind.TrueKeyword;
if (!data[key] && value.kind !== SyntaxKind.FalseKeyword) throw fail(`${key} is not a boolean literal`);
break;
}
}
if (!data.name || !data.description || !data.authors) throw fail("name, description or authors are missing");
const fileBits = fileName.split(".");
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1)!)) {
const mod = fileBits.at(-2)!;
if (!["web", "discordDesktop", "vencordDesktop", "dev"].includes(mod)) throw fail(`invalid target ${fileBits.at(-2)}`);
data.target = mod as any;
}
return data;
}
throw fail("no default export called 'definePlugin' found");
}
async function getEntryPoint(dirent: Dirent) {
const base = join("./src/plugins", dirent.name);
if (!dirent.isDirectory()) return base;
for (const name of ["index.ts", "index.tsx"]) {
const full = join(base, name);
try {
await access(full);
return full;
} catch { }
}
throw new Error(`${dirent.name}: Couldn't find entry point`);
}
(async () => {
parseDevs();
const plugins = readdirSync("./src/plugins", { withFileTypes: true }).filter(d => d.name !== "index.ts");
const promises = plugins.map(async dirent => parseFile(await getEntryPoint(dirent)));
const data = JSON.stringify(await Promise.all(promises));
if (process.argv.length > 2) {
writeFileSync(process.argv[2], data);
} else {
console.log(data);
}
})();

View File

@ -27,44 +27,19 @@ export { PlainSettings, Settings };
import "./utils/quickCss";
import "./webpack/patchWebpack";
import { showNotification } from "./api/Notifications";
import { PlainSettings, Settings } from "./api/Settings";
import { popNotice, showNotice } from "./api/Notices";
import { PlainSettings, Settings } from "./api/settings";
import { patches, PMLogger, startAllPlugins } from "./plugins";
import { localStorage } from "./utils/localStorage";
import { relaunch } from "./utils/native";
import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
import { SettingsRouter } from "./webpack/common";
async function syncSettings() {
if (
Settings.cloud.settingsSync && // if it's enabled
Settings.cloud.authenticated // if cloud integrations are enabled
) {
if (localStorage.Vencord_settingsDirty) {
await putCloudSettings();
delete localStorage.Vencord_settingsDirty;
} else if (await getCloudSettings(false)) { // if we synchronized something (false means no sync)
// we show a notification here instead of allowing getCloudSettings() to show one to declutter the amount of
// potential notifications that might occur. getCloudSettings() will always send a notification regardless if
// there was an error to notify the user, but besides that we only want to show one notification instead of all
// of the possible ones it has (such as when your settings are newer).
showNotification({
title: "Cloud Settings",
body: "Your settings have been updated! Click here to restart to fully apply changes!",
color: "var(--green-360)",
onClick: relaunch
});
}
}
}
export let Components: any;
async function init() {
await onceReady;
startAllPlugins();
syncSettings();
Components = await import("./components");
if (!IS_WEB) {
try {
@ -73,27 +48,33 @@ async function init() {
if (Settings.autoUpdate) {
await update();
if (Settings.autoUpdateNotification)
setTimeout(() => showNotification({
title: "Vencord has been updated!",
body: "Click here to restart",
permanent: true,
noPersist: true,
onClick: relaunch
}), 10_000);
const needsFullRestart = await rebuild();
setTimeout(() => {
showNotice(
"Vencord has been updated!",
"Restart",
() => {
if (needsFullRestart)
window.DiscordNative.app.relaunch();
else
location.reload();
}
);
}, 10_000);
return;
}
if (Settings.notifyAboutUpdates)
setTimeout(() => showNotification({
title: "A Vencord update is available!",
body: "Click here to view the update",
permanent: true,
noPersist: true,
onClick() {
setTimeout(() => {
showNotice(
"A Vencord update is available!",
"View Update",
() => {
popNotice();
SettingsRouter.open("VencordUpdater");
}
}), 10_000);
);
}, 10_000);
} catch (err) {
UpdateLogger.error("Failed to check for updates", err);
}
@ -114,12 +95,3 @@ async function init() {
}
init();
if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
document.addEventListener("DOMContentLoaded", () => {
document.head.append(Object.assign(document.createElement("style"), {
id: "vencord-native-titlebar-style",
textContent: "[class*=titleBar-]{display: none!important}"
}));
}, { once: true });
}

View File

@ -16,46 +16,34 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { IpcEvents } from "@utils/IpcEvents";
import { IpcRes } from "@utils/types";
import { ipcRenderer } from "electron";
import IPC_EVENTS from "@utils/IpcEvents";
import { IpcRenderer, ipcRenderer } from "electron";
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.invoke(event, ...args) as Promise<T>;
function assertEventAllowed(event: string) {
if (!(event in IPC_EVENTS)) throw new Error(`Event ${event} not allowed.`);
}
export function sendSync<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.sendSync(event, ...args) as T;
}
export default {
updater: {
getUpdates: () => invoke<IpcRes<Record<"hash" | "author" | "message", string>[]>>(IpcEvents.GET_UPDATES),
update: () => invoke<IpcRes<boolean>>(IpcEvents.UPDATE),
rebuild: () => invoke<IpcRes<boolean>>(IpcEvents.BUILD),
getRepo: () => invoke<IpcRes<string>>(IpcEvents.GET_REPO),
getVersions: () => process.versions,
ipc: {
send(event: string, ...args: any[]) {
assertEventAllowed(event);
ipcRenderer.send(event, ...args);
},
settings: {
get: () => sendSync<string>(IpcEvents.GET_SETTINGS),
set: (settings: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings),
getSettingsDir: () => invoke<string>(IpcEvents.GET_SETTINGS_DIR),
sendSync<T = any>(event: string, ...args: any[]): T {
assertEventAllowed(event);
return ipcRenderer.sendSync(event, ...args);
},
quickCss: {
get: () => invoke<string>(IpcEvents.GET_QUICK_CSS),
set: (css: string) => invoke<void>(IpcEvents.SET_QUICK_CSS, css),
addChangeListener(cb: (newCss: string) => void) {
ipcRenderer.on(IpcEvents.QUICK_CSS_UPDATE, (_, css) => cb(css));
on(event: string, listener: Parameters<IpcRenderer["on"]>[1]) {
assertEventAllowed(event);
ipcRenderer.on(event, listener);
},
openFile: () => invoke<void>(IpcEvents.OPEN_QUICKCSS),
openEditor: () => invoke<void>(IpcEvents.OPEN_MONACO_EDITOR),
},
native: {
getVersions: () => process.versions as Partial<NodeJS.ProcessVersions>,
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url)
off(event: string, listener: Parameters<IpcRenderer["off"]>[1]) {
assertEventAllowed(event);
ipcRenderer.off(event, listener);
},
invoke<T = any>(event: string, ...args: any[]): Promise<T> {
assertEventAllowed(event);
return ipcRenderer.invoke(event, ...args);
}
}
};

View File

@ -29,12 +29,11 @@ export enum BadgePosition {
export interface ProfileBadge {
/** The tooltip to show on hover. Required for image badges */
description?: string;
tooltip?: string;
/** Custom component for the badge (tooltip not included) */
component?: ComponentType<ProfileBadge & BadgeUserArgs>;
/** The custom image to use */
image?: string;
link?: string;
/** Action to perform when you click the badge */
onClick?(): void;
/** Should the user display this badge? */
@ -70,19 +69,17 @@ export function removeBadge(badge: ProfileBadge) {
* Inject badges into the profile badges array.
* You probably don't need to use this.
*/
export function _getBadges(args: BadgeUserArgs) {
const badges = [] as ProfileBadge[];
export function inject(badgeArray: ProfileBadge[], args: BadgeUserArgs) {
for (const badge of Badges) {
if (!badge.shouldShow || badge.shouldShow(args)) {
badge.position === BadgePosition.START
? badges.unshift({ ...badge, ...args })
: badges.push({ ...badge, ...args });
? badgeArray.unshift({ ...badge, ...args })
: badgeArray.push({ ...badge, ...args });
}
}
const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/apiBadges").default).getDonorBadges(args.user.id);
if (donorBadges) badges.unshift(...donorBadges);
(Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id);
return badges;
return badgeArray;
}
export interface BadgeUserArgs {

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { makeCodeblock } from "@utils/text";
import { makeCodeblock } from "@utils/misc";
import { sendBotMessage } from "./commandHelpers";
import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
@ -111,7 +111,6 @@ function registerSubCommands(cmd: Command, plugin: string) {
...o,
type: ApplicationCommandType.CHAT_INPUT,
name: `${cmd.name} ${o.name}`,
id: `${o.name}-${cmd.id}`,
displayName: `${cmd.name} ${o.name}`,
subCommandPath: [{
name: o.name,

View File

@ -1,155 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import type { ReactElement } from "react";
type ContextMenuPatchCallbackReturn = (() => void) | void;
/**
* @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
*/
export type NavContextMenuPatchCallback = (children: Array<React.ReactElement>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
/**
* @param navId The navId of the context menu being patched
* @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
*/
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<React.ReactElement>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
const ContextMenuLogger = new Logger("ContextMenu");
export const navPatches = new Map<string, Set<NavContextMenuPatchCallback>>();
export const globalPatches = new Set<GlobalContextMenuPatchCallback>();
/**
* Add a context menu patch
* @param navId The navId(s) for the context menu(s) to patch
* @param patch The patch to be applied
*/
export function addContextMenuPatch(navId: string | Array<string>, patch: NavContextMenuPatchCallback) {
if (!Array.isArray(navId)) navId = [navId];
for (const id of navId) {
let contextMenuPatches = navPatches.get(id);
if (!contextMenuPatches) {
contextMenuPatches = new Set();
navPatches.set(id, contextMenuPatches);
}
contextMenuPatches.add(patch);
}
}
/**
* Add a global context menu patch that fires the patch for all context menus
* @param patch The patch to be applied
*/
export function addGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback) {
globalPatches.add(patch);
}
/**
* Remove a context menu patch
* @param navId The navId(s) for the context menu(s) to remove the patch
* @param patch The patch to be removed
* @returns Wheter the patch was sucessfully removed from the context menu(s)
*/
export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> {
const navIds = Array.isArray(navId) ? navId : [navId as string];
const results = navIds.map(id => navPatches.get(id)?.delete(patch) ?? false);
return (Array.isArray(navId) ? results : results[0]) as T extends string ? boolean : Array<boolean>;
}
/**
* Remove a global context menu patch
* @param patch The patch to be removed
* @returns Wheter the patch was sucessfully removed
*/
export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean {
return globalPatches.delete(patch);
}
/**
* A helper function for finding the children array of a group nested inside a context menu based on the id of one of its childs
* @param id The id of the child
* @param children The context menu children
*/
export function findGroupChildrenByChildId(id: string, children: Array<React.ReactElement>, _itemsArray?: Array<React.ReactElement>): Array<React.ReactElement> | null {
for (const child of children) {
if (child == null) continue;
if (child.props?.id === id) return _itemsArray ?? null;
let nextChildren = child.props?.children;
if (nextChildren) {
if (!Array.isArray(nextChildren)) {
nextChildren = [nextChildren];
child.props.children = nextChildren;
}
const found = findGroupChildrenByChildId(id, nextChildren, nextChildren);
if (found !== null) return found;
}
}
return null;
}
interface ContextMenuProps {
contextMenuApiArguments?: Array<any>;
navId: string;
children: Array<ReactElement>;
"aria-label": string;
onSelect: (() => void) | undefined;
onClose: (callback: (...args: Array<any>) => any) => void;
}
const patchedMenus = new WeakSet();
export function _patchContextMenu(props: ContextMenuProps) {
props.contextMenuApiArguments ??= [];
const contextMenuPatches = navPatches.get(props.navId);
if (!Array.isArray(props.children)) props.children = [props.children];
if (contextMenuPatches) {
for (const patch of contextMenuPatches) {
try {
const callback = patch(props.children, ...props.contextMenuApiArguments);
if (!patchedMenus.has(props)) callback?.();
} catch (err) {
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
}
}
}
for (const patch of globalPatches) {
try {
const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments);
if (!patchedMenus.has(props)) callback?.();
} catch (err) {
ContextMenuLogger.error("Global patch errored,", err);
}
}
patchedMenus.add(props);
}

View File

@ -16,10 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import Logger from "@utils/Logger";
import { MessageStore } from "@webpack/common";
import type { Channel, Message } from "discord-types/general";
import type { Promisable } from "type-fest";
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
@ -36,63 +35,22 @@ export interface Emoji {
export interface MessageObject {
content: string,
validNonShortcutEmojis: Emoji[];
invalidEmojis: any[];
tts: boolean;
}
export interface Upload {
classification: string;
currentSize: number;
description: string | null;
filename: string;
id: string;
isImage: boolean;
isVideo: boolean;
item: {
file: File;
platform: number;
};
loaded: number;
mimeType: string;
preCompressionSize: number;
responseUrl: string;
sensitive: boolean;
showLargeMessageDialog: boolean;
spoiler: boolean;
status: "NOT_STARTED" | "STARTED" | "UPLOADING" | "ERROR" | "COMPLETED" | "CANCELLED";
uniqueId: string;
uploadedFilename: string;
}
export interface MessageReplyOptions {
messageReference: Message["messageReference"];
allowedMentions?: {
parse: Array<string>;
repliedUser: boolean;
};
}
export interface MessageExtra {
stickers?: string[];
uploads?: Upload[];
replyOptions: MessageReplyOptions;
content: string;
channel: Channel;
type?: any;
openWarningPopout: (props: any) => any;
stickerIds?: string[];
}
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void>;
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => void | { cancel: boolean; };
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
const sendListeners = new Set<SendListener>();
const editListeners = new Set<EditListener>();
export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra, replyOptions: MessageReplyOptions) {
extra.replyOptions = replyOptions;
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
for (const listener of sendListeners) {
try {
const result = await listener(channelId, messageObj, extra);
const result = listener(channelId, messageObj, extra);
if (result && result.cancel === true) {
return true;
}
@ -103,10 +61,10 @@ export async function _handlePreSend(channelId: string, messageObj: MessageObjec
return false;
}
export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
export function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
for (const listener of editListeners) {
try {
await listener(channelId, messageId, messageObj);
listener(channelId, messageId, messageObj);
} catch (e) {
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
}

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import Logger from "@utils/Logger";
import { Channel, Message } from "discord-types/general";
import type { MouseEventHandler } from "react";

View File

@ -18,10 +18,9 @@
import "./styles.css";
import { useSettings } from "@api/Settings";
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { classes } from "@utils/misc";
import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
import { Forms, React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
import { NotificationData } from "./Notifications";
@ -33,11 +32,8 @@ export default ErrorBoundary.wrap(function NotificationComponent({
icon,
onClick,
onClose,
image,
permanent,
className,
dismissOnClick
}: NotificationData & { className?: string; }) {
image
}: NotificationData) {
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
@ -47,7 +43,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);
useEffect(() => {
if (isHover || !hasFocus || timeout === 0 || permanent) return void setElapsed(0);
if (isHover || !hasFocus || timeout === 0) return void setElapsed(0);
const intervalId = setInterval(() => {
const elapsed = Date.now() - start;
@ -64,13 +60,9 @@ export default ErrorBoundary.wrap(function NotificationComponent({
return (
<button
className={classes("vc-notification-root", className)}
className="vc-notification-root"
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
onClick={() => {
onClick?.();
if (dismissOnClick !== false)
onClose!();
}}
onClick={onClick}
onContextMenu={e => {
e.preventDefault();
e.stopPropagation();
@ -82,35 +74,14 @@ export default ErrorBoundary.wrap(function NotificationComponent({
<div className="vc-notification">
{icon && <img className="vc-notification-icon" src={icon} alt="" />}
<div className="vc-notification-content">
<div className="vc-notification-header">
<h2 className="vc-notification-title">{title}</h2>
<button
className="vc-notification-close-btn"
onClick={e => {
e.preventDefault();
e.stopPropagation();
onClose!();
}}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
role="img"
aria-labelledby="vc-notification-dismiss-title"
>
<title id="vc-notification-dismiss-title">Dismiss Notification</title>
<path fill="currentColor" d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" />
</svg>
</button>
</div>
<Forms.FormTitle tag="h2">{title}</Forms.FormTitle>
<div>
{richBody ?? <p className="vc-notification-p">{body}</p>}
</div>
</div>
</div>
{image && <img className="vc-notification-img" src={image} alt="" />}
{timeout !== 0 && !permanent && (
{timeout !== 0 && (
<div
className="vc-notification-progressbar"
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
@ -118,6 +89,4 @@ export default ErrorBoundary.wrap(function NotificationComponent({
)}
</button>
);
}, {
onError: ({ props }) => props.onClose!()
});

View File

@ -16,14 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/Settings";
import { Settings } from "@api/settings";
import { Queue } from "@utils/Queue";
import { ReactDOM } from "@webpack/common";
import type { ReactNode } from "react";
import type { Root } from "react-dom/client";
import NotificationComponent from "./NotificationComponent";
import { persistNotification } from "./notificationLog";
const NotificationQueue = new Queue();
@ -55,12 +54,6 @@ export interface NotificationData {
onClick?(): void;
onClose?(): void;
color?: string;
/** Whether this notification should not have a timeout */
permanent?: boolean;
/** Whether this notification should not be persisted in the Notification Log */
noPersist?: boolean;
/** Whether this notification should be dismissed when clicked (defaults to true) */
dismissOnClick?: boolean;
}
function _showNotification(notification: NotificationData, id: number) {
@ -77,8 +70,6 @@ function _showNotification(notification: NotificationData, id: number) {
}
function shouldBeNative() {
if (typeof Notification === "undefined") return false;
const { useNative } = Settings.notifications;
if (useNative === "always") return true;
if (useNative === "not-focused") return !document.hasFocus();
@ -93,8 +84,6 @@ export async function requestPermission() {
}
export async function showNotification(data: NotificationData) {
persistNotification(data);
if (shouldBeNative() && await requestPermission()) {
const { title, body, icon, image, onClick = null, onClose = null } = data;
const n = new Notification(title, {

View File

@ -1,203 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react";
import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
import { nanoid } from "nanoid";
import type { DispatchWithoutAction } from "react";
import NotificationComponent from "./NotificationComponent";
import type { NotificationData } from "./Notifications";
interface PersistentNotificationData extends Pick<NotificationData, "title" | "body" | "image" | "icon" | "color"> {
timestamp: number;
id: string;
}
const KEY = "notification-log";
const getLog = async () => {
const log = await DataStore.get(KEY) as PersistentNotificationData[] | undefined;
return log ?? [];
};
const cl = classNameFactory("vc-notification-log-");
const signals = new Set<DispatchWithoutAction>();
export async function persistNotification(notification: NotificationData) {
if (notification.noPersist) return;
const limit = Settings.notifications.logLimit;
if (limit === 0) return;
await DataStore.update(KEY, (old: PersistentNotificationData[] | undefined) => {
const log = old ?? [];
// Omit stuff we don't need
const {
onClick, onClose, richBody, permanent, noPersist, dismissOnClick,
...pureNotification
} = notification;
log.unshift({
...pureNotification,
timestamp: Date.now(),
id: nanoid()
});
if (log.length > limit && limit !== 200)
log.length = limit;
return log;
});
signals.forEach(x => x());
}
export async function deleteNotification(timestamp: number) {
const log = await getLog();
const index = log.findIndex(x => x.timestamp === timestamp);
if (index === -1) return;
log.splice(index, 1);
await DataStore.set(KEY, log);
signals.forEach(x => x());
}
export function useLogs() {
const [signal, setSignal] = useReducer(x => x + 1, 0);
useEffect(() => {
signals.add(setSignal);
return () => void signals.delete(setSignal);
}, []);
const [log, _, pending] = useAwaiter(getLog, {
fallbackValue: [],
deps: [signal]
});
return [log, pending] as const;
}
function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
const [removing, setRemoving] = useState(false);
const ref = React.useRef<HTMLDivElement>(null);
useEffect(() => {
const div = ref.current!;
const setHeight = () => {
if (div.clientHeight === 0) return requestAnimationFrame(setHeight);
div.style.height = `${div.clientHeight}px`;
};
setHeight();
}, []);
return (
<div className={cl("wrapper", { removing })} ref={ref}>
<NotificationComponent
{...data}
permanent={true}
dismissOnClick={false}
onClose={() => {
if (removing) return;
setRemoving(true);
setTimeout(() => deleteNotification(data.timestamp), 200);
}}
richBody={
<div className={cl("body")}>
{data.body}
<Timestamp timestamp={moment(data.timestamp)} className={cl("timestamp")} />
</div>
}
/>
</div>
);
}
export function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) {
if (!log.length && !pending)
return (
<div className={cl("container")}>
<div className={cl("empty")} />
<Forms.FormText style={{ textAlign: "center" }}>
No notifications yet
</Forms.FormText>
</div>
);
return (
<div className={cl("container")}>
{log.map(n => <NotificationEntry data={n} key={n.id} />)}
</div>
);
}
function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) {
const [log, pending] = useLogs();
return (
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
<ModalCloseButton onClick={close} />
</ModalHeader>
<ModalContent>
<NotificationLog log={log} pending={pending} />
</ModalContent>
<ModalFooter>
<Button
disabled={log.length === 0}
onClick={() => {
Alerts.show({
title: "Are you sure?",
body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
async onConfirm() {
await DataStore.set(KEY, []);
signals.forEach(x => x());
},
confirmText: "Do it!",
confirmColor: "vc-notification-log-danger-btn",
cancelText: "Nevermind"
});
}}
>
Clear Notification Log
</Button>
</ModalFooter>
</ModalRoot>
);
}
export function openNotificationLogModal() {
const key = openModal(modalProps => (
<LogModal
modalProps={modalProps}
close={() => closeModal(key)}
/>
));
}

View File

@ -3,20 +3,16 @@
all: unset;
display: flex;
flex-direction: column;
width: 25vw;
min-height: 10vh;
color: var(--text-normal);
background-color: var(--background-secondary-alt);
border-radius: 6px;
overflow: hidden;
cursor: pointer;
width: 100%;
}
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
position: absolute;
z-index: 2147483647;
right: 1rem;
width: 25vw;
min-height: 10vh;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
}
.vc-notification {
@ -26,42 +22,17 @@
gap: 1.25rem;
}
.vc-notification-content {
width: 100%;
}
.vc-notification-header {
display: flex;
justify-content: space-between;
}
.vc-notification-title {
color: var(--header-primary);
font-size: 1rem;
font-weight: 600;
line-height: 1.25rem;
text-transform: uppercase;
}
.vc-notification-close-btn {
all: unset;
cursor: pointer;
color: var(--interactive-normal);
opacity: 0.5;
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
}
.vc-notification-close-btn:hover {
color: var(--interactive-hover);
opacity: 1;
}
.vc-notification-icon {
height: 4rem;
width: 4rem;
border-radius: 6px;
}
/* Discord adding 3km margin to generic tags */
.vc-notification h2 {
margin: unset;
}
.vc-notification-progressbar {
height: 0.25rem;
border-radius: 5px;
@ -76,47 +47,3 @@
.vc-notification-img {
width: 100%;
}
.vc-notification-log-empty {
height: 218px;
background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
margin-bottom: 40px;
}
.vc-notification-log-container {
display: flex;
flex-direction: column;
padding: 1em;
overflow: hidden;
}
.vc-notification-log-wrapper {
transition: 200ms ease;
transition-property: height, opacity;
}
.vc-notification-log-wrapper:not(:last-child) {
margin-bottom: 1em;
}
.vc-notification-log-removing {
height: 0 !important;
opacity: 0;
margin-bottom: 1em;
}
.vc-notification-log-body {
display: flex;
flex-direction: column;
}
.vc-notification-log-timestamp {
margin-left: auto;
font-size: 0.8em;
font-weight: lighter;
}
.vc-notification-log-danger-btn {
color: var(--white-500);
background-color: var(--button-danger-background);
}

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import Logger from "@utils/Logger";
const logger = new Logger("ServerListAPI");

View File

@ -1,69 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { findModuleId, wreq } from "@webpack";
import { Settings } from "./Settings";
interface Setting<T> {
/**
* Get the setting value
*/
getSetting(): T;
/**
* Update the setting value
* @param value The new value
*/
updateSetting(value: T | ((old: T) => T)): Promise<void>;
/**
* React hook for automatically updating components when the setting is updated
*/
useSetting(): T;
settingsStoreApiGroup: string;
settingsStoreApiName: string;
}
const SettingsStores: Array<Setting<any>> | undefined = proxyLazy(() => {
const modId = findModuleId('"textAndImages","renderSpoilers"');
if (modId == null) return new Logger("SettingsStoreAPI").error("Didn't find stores module.");
const mod = wreq(modId);
if (mod == null) return;
return Object.values(mod).filter((s: any) => s?.settingsStoreApiGroup) as any;
});
/**
* Get the store for a setting
* @param group The setting group
* @param name The name of the setting
*/
export function getSettingStore<T = any>(group: string, name: string): Setting<T> | undefined {
if (!Settings.plugins.SettingsStoreAPI.enabled) throw new Error("Cannot use SettingsStoreAPI without setting as dependency.");
return SettingsStores?.find(s => s?.settingsStoreApiGroup === group && s?.settingsStoreApiName === name);
}
/**
* getSettingStore but lazy
*/
export function getSettingStoreLazy<T = any>(group: string, name: string) {
return proxyLazy(() => getSettingStore<T>(group, name));
}

View File

@ -18,7 +18,6 @@
import * as $Badges from "./Badges";
import * as $Commands from "./Commands";
import * as $ContextMenu from "./ContextMenu";
import * as $DataStore from "./DataStore";
import * as $MemberListDecorators from "./MemberListDecorators";
import * as $MessageAccessories from "./MessageAccessories";
@ -28,8 +27,6 @@ import * as $MessagePopover from "./MessagePopover";
import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList";
import * as $Settings from "./Settings";
import * as $SettingsStore from "./SettingsStore";
import * as $Styles from "./Styles";
/**
@ -87,14 +84,6 @@ export const MessageDecorations = $MessageDecorations;
* An API allowing you to add components to member list users, in both DM's and servers
*/
export const MemberListDecorators = $MemberListDecorators;
/**
* An API allowing you to persist data
*/
export const Settings = $Settings;
/**
* An API allowing you to read, manipulate and automatically update components based on Discord settings
*/
export const SettingsStore = $SettingsStore;
/**
* An API allowing you to dynamically load styles
* a
@ -104,8 +93,3 @@ export const Styles = $Styles;
* An API allowing you to display notifications
*/
export const Notifications = $Notifications;
/**
* An api allowing you to patch and add/remove items to/from context menus
*/
export const ContextMenu = $ContextMenu;

View File

@ -16,11 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { debounce } from "@utils/debounce";
import { localStorage } from "@utils/localStorage";
import { Logger } from "@utils/Logger";
import IpcEvents from "@utils/IpcEvents";
import Logger from "@utils/Logger";
import { mergeDefaults } from "@utils/misc";
import { putCloudSettings } from "@utils/settingsSync";
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
import { React } from "@webpack/common";
@ -30,16 +28,12 @@ const logger = new Logger("Settings");
export interface Settings {
notifyAboutUpdates: boolean;
autoUpdate: boolean;
autoUpdateNotification: boolean,
useQuickCss: boolean;
enableReactDevtools: boolean;
themeLinks: string[];
frameless: boolean;
transparent: boolean;
winCtrlQ: boolean;
macosTranslucency: boolean;
disableMinSize: boolean;
winNativeTitleBar: boolean;
plugins: {
[plugin: string]: {
enabled: boolean;
@ -51,62 +45,35 @@ export interface Settings {
timeout: number;
position: "top-right" | "bottom-right";
useNative: "always" | "never" | "not-focused";
logLimit: number;
};
cloud: {
authenticated: boolean;
url: string;
settingsSync: boolean;
settingsSyncVersion: number;
};
}
const DefaultSettings: Settings = {
notifyAboutUpdates: true,
autoUpdate: false,
autoUpdateNotification: true,
useQuickCss: true,
themeLinks: [],
enableReactDevtools: false,
frameless: false,
transparent: false,
winCtrlQ: false,
macosTranslucency: false,
disableMinSize: false,
winNativeTitleBar: false,
plugins: {},
notifications: {
timeout: 5000,
position: "bottom-right",
useNative: "not-focused",
logLimit: 50
},
cloud: {
authenticated: false,
url: "https://api.vencord.dev/",
settingsSync: false,
settingsSyncVersion: 0
useNative: "not-focused"
}
};
try {
var settings = JSON.parse(VencordNative.settings.get()) as Settings;
var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings;
mergeDefaults(settings, DefaultSettings);
} catch (err) {
var settings = mergeDefaults({} as Settings, DefaultSettings);
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
}
const saveSettingsOnFrequentAction = debounce(async () => {
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
await putCloudSettings();
delete localStorage.Vencord_settingsDirty;
}
}, 60_000);
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
const subscriptions = new Set<SubscriptionCallback>();
@ -123,7 +90,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
// Return empty for plugins with no settings
if (path === "plugins" && p in plugins)
return target[p] = makeProxy({
enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
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
@ -162,17 +129,13 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
target[p] = v;
// Call any listeners that are listening to a setting of this path
const setPath = `${path}${path && "."}${p}`;
delete proxyCache[setPath];
for (const subscription of subscriptions) {
if (!subscription._path || subscription._path === setPath) {
subscription(v, setPath);
}
}
// And don't forget to persist the settings!
PlainSettings.cloud.settingsSyncVersion = Date.now();
localStorage.Vencord_settingsDirty = true;
saveSettingsOnFrequentAction();
VencordNative.settings.set(JSON.stringify(root, null, 4));
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
return true;
}
});
@ -202,11 +165,11 @@ export const Settings = makeProxy(settings);
* @returns Settings
*/
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
export function useSettings(paths?: UseSettings<Settings>[]) {
export function useSettings(paths?: string[]) {
const [, forceUpdate] = React.useReducer(() => ({}), {});
const onUpdate: SubscriptionCallback = paths
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
? (value, path) => paths.includes(path) && forceUpdate()
: forceUpdate;
React.useEffect(() => {
@ -248,7 +211,10 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
plugins[name] = plugins[oldName];
delete plugins[oldName];
VencordNative.settings.set(JSON.stringify(settings, null, 4));
VencordNative.ipc.invoke(
IpcEvents.SET_SETTINGS,
JSON.stringify(settings, null, 4)
);
break;
}
}
@ -261,7 +227,7 @@ export function definePluginSettings<D extends SettingsDefinition, C extends Set
return Settings.plugins[definedSettings.pluginName] as any;
},
use: settings => useSettings(
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`)
).plugins[definedSettings.pluginName] as any,
def,
checks: checks ?? {},
@ -269,15 +235,3 @@ export function definePluginSettings<D extends SettingsDefinition, C extends Set
};
return definedSettings;
}
type UseSettings<T extends object> = ResolveUseSettings<T>[keyof T];
type ResolveUseSettings<T extends object> = {
[Key in keyof T]:
Key extends string
? T[Key] extends Record<string, unknown>
// @ts-ignore "Type instantiation is excessively deep and possibly infinite"
? UseSettings<T[Key]> extends string ? `${Key}.${UseSettings<T[Key]>}` : never
: Key
: never;
};

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import IpcEvents from "@utils/IpcEvents";
import { Button } from "@webpack/common";
import { Heart } from "./Heart";
@ -26,7 +27,9 @@ export default function DonateButton(props: any) {
{...props}
look={Button.Looks.LINK}
color={Button.Colors.TRANSPARENT}
onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/Vendicated")}
onClick={() =>
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/sponsors/Vendicated")
}
>
<Heart />
Donate

View File

@ -16,25 +16,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { LazyComponent } from "@utils/react";
import { React } from "@webpack/common";
import Logger from "@utils/Logger";
import { LazyComponent } from "@utils/misc";
import { Margins, React } from "@webpack/common";
import { ErrorCard } from "./ErrorCard";
interface Props<T = any> {
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; }>>;
/** called when an error occurs. The props property is only available if using .wrap */
onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void;
/** called when an error occurs */
onError?(error: Error, errorInfo: React.ErrorInfo): void;
/** Custom error message */
message?: string;
/** The props passed to the wrapped component. Only used by wrap */
wrappedProps?: T;
}
const color = "#e78284";
@ -69,7 +65,7 @@ const ErrorBoundary = LazyComponent(() => {
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps });
this.props.onError?.(error, errorInfo);
logger.error("A component threw an Error\n", error);
logger.error("Component Stack", errorInfo.componentStack);
}
@ -88,13 +84,15 @@ const ErrorBoundary = LazyComponent(() => {
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
return (
<ErrorCard style={{ overflow: "hidden" }}>
<ErrorCard style={{
overflow: "hidden",
}}>
<h1>Oh no!</h1>
<p>{msg}</p>
<code>
{this.state.message}
{!!this.state.stack && (
<pre className={Margins.top8}>
<pre className={Margins.marginTop8}>
{this.state.stack}
</pre>
)}
@ -105,11 +103,11 @@ const ErrorBoundary = LazyComponent(() => {
};
}) as
React.ComponentType<React.PropsWithChildren<Props>> & {
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, "wrappedProps">): React.FunctionComponent<T>;
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
};
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
<ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>
<ErrorBoundary {...errorBoundaryProps}>
<Component {...props} />
</ErrorBoundary>
);

View File

@ -1,7 +0,0 @@
.vc-error-card {
padding: 2em;
background-color: #e7828430;
border: 1px solid #e78284;
border-radius: 5px;
color: var(--text-normal, white);
}

View File

@ -16,15 +16,24 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./ErrorCard.css";
import { Card } from "@webpack/common";
import { classes } from "@utils/misc";
import type { HTMLProps } from "react";
export function ErrorCard(props: React.PropsWithChildren<HTMLProps<HTMLDivElement>>) {
interface Props {
style?: React.CSSProperties;
className?: string;
}
export function ErrorCard(props: React.PropsWithChildren<Props>) {
return (
<div {...props} className={classes(props.className, "vc-error-card")}>
<Card className={props.className} style={
{
padding: "2em",
backgroundColor: "#e7828430",
borderColor: "#e78284",
color: "var(--text-normal)",
...props.style
}
}>
{props.children}
</div>
</Card>
);
}

View File

@ -1,83 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classes } from "@utils/misc";
import type { PropsWithChildren } from "react";
interface BaseIconProps extends IconProps {
viewBox: string;
}
interface IconProps {
className?: string;
height?: number;
width?: number;
}
function Icon({ height = 24, width = 24, className, children, viewBox }: PropsWithChildren<BaseIconProps>) {
return (
<svg
className={classes(className, "vc-icon")}
aria-hidden="true"
role="img"
width={width}
height={height}
viewBox={viewBox}
>
{children}
</svg>
);
}
/**
* Discord's link icon, as seen in the Message context menu "Copy Message Link" option
*/
export function LinkIcon({ height = 24, width = 24, className }: IconProps) {
return (
<Icon
height={height}
width={width}
className={classes(className, "vc-link-icon")}
viewBox="0 0 24 24"
>
<g fill="none" fill-rule="evenodd">
<path fill="currentColor" d="M10.59 13.41c.41.39.41 1.03 0 1.42-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0 5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24 2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0 4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0 5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24 2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24.973.973 0 0 1 0-1.42z" />
<rect width={width} height={height} />
</g>
</Icon>
);
}
/**
* Discord's copy icon, as seen in the user popout right of the username when clicking
* your own username in the bottom left user panel
*/
export function CopyIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-copy-icon")}
viewBox="0 0 24 24"
>
<g fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1z" />
<path d="M15 5H8c-1.1 0-1.99.9-1.99 2L6 21c0 1.1.89 2 1.99 2H19c1.1 0 2-.9 2-2V11l-6-6zM8 21V7h6v5h5v9H8z" />
</g>
</Icon>
);
}

51
src/components/Monaco.ts Normal file
View File

@ -0,0 +1,51 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { debounce } from "@utils/debounce";
import IpcEvents from "@utils/IpcEvents";
import { Queue } from "@utils/Queue";
import { find } from "@webpack";
import monacoHtml from "~fileContent/monacoWin.html";
const queue = new Queue();
const setCss = debounce((css: string) => {
queue.push(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css));
});
export async function launchMonacoEditor() {
const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
const win = open("about:blank", "VencordQuickCss", features);
if (!win) {
alert("Failed to open QuickCSS popup. Make sure to allow popups!");
return;
}
win.setCss = setCss;
win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
win.getTheme = () =>
find(m =>
m.ProtoClass?.typeName.endsWith("PreloadedUserSettings")
)?.getCurrentValue()?.appearance?.theme === 2
? "vs-light"
: "vs-dark";
win.document.write(monacoHtml);
window.__VENCORD_MONACO_WIN__ = new WeakRef(win);
}

View File

@ -16,16 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { CheckedTextInput } from "@components/CheckedTextInput";
import { debounce } from "@utils/debounce";
import { Margins } from "@utils/margins";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import { makeCodeblock } from "@utils/text";
import { ReplaceFn } from "@utils/types";
import { makeCodeblock } from "@utils/misc";
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches";
import { search } from "@webpack";
import { Button, Clipboard, Forms, Parser, React, Switch, TextInput } from "@webpack/common";
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared";
import { CheckedTextInput } from "./CheckedTextInput";
import ErrorBoundary from "./ErrorBoundary";
// Do not include diff in non dev builds (side effects import)
if (IS_DEV) {
@ -130,7 +128,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
)}
{!!diff?.length && (
<Button className={Margins.top20} onClick={() => {
<Button className={Margins.marginTop20} onClick={() => {
try {
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
setCompileResult([true, "Compiled successfully"]);
@ -186,10 +184,9 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
error={error ?? replacementError}
/>
{!isFunc && (
<div className="vc-text-selectable">
<>
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
{Object.entries({
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
"$$": "Insert a $",
"$&": "Insert the entire match",
"$`\u200b": "Insert the substring before the match",
@ -201,11 +198,11 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
{Parser.parse("`" + placeholder + "`")}: {desc}
</Forms.FormText>
))}
</div>
</>
)}
<Switch
className={Margins.top8}
className={Margins.marginTop8}
value={isFunc}
onChange={setIsFunc}
note="'replacement' will be evaled if this is toggled"
@ -258,7 +255,8 @@ function PatchHelper() {
}
return (
<SettingsTab title="Patch Helper">
<Forms.FormSection>
<Text variant="heading-md/normal" tag="h2" className={Margins.marginBottom8}>Patch Helper</Text>
<Forms.FormTitle>find</Forms.FormTitle>
<TextInput
type="text"
@ -298,13 +296,13 @@ function PatchHelper() {
{!!(find && match && replacement) && (
<>
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
<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>
</>
)}
</SettingsTab>
</Forms.FormSection>
);
}
export default IS_DEV ? wrapTab(PatchHelper, "PatchHelper") : null;
export default IS_DEV ? ErrorBoundary.wrap(PatchHelper) : null;

View File

@ -17,14 +17,12 @@
*/
import { generateId } from "@api/Commands";
import { useSettings } from "@api/Settings";
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { LazyComponent } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { LazyComponent } from "@utils/react";
import { proxyLazy } from "@utils/proxyLazy";
import { OptionType, Plugin } from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack";
import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
@ -129,8 +127,6 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
} else {
const options = Object.entries(plugin.options).map(([key, setting]) => {
if (setting.hidden) return null;
function onChange(newValue: any) {
setTempSettings(s => ({ ...s, [key]: newValue }));
}
@ -153,7 +149,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
);
});
return <Flex flexDirection="column" style={{ gap: 12, marginBottom: 16 }}>{options}</Flex>;
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
}
}
@ -178,12 +174,12 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
}
return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM} className="vc-text-selectable">
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
<ModalCloseButton onClick={onClose} />
</ModalHeader>
<ModalContent>
<ModalContent style={{ marginBottom: 8, marginTop: 8 }}>
<Forms.FormSection>
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
<Forms.FormText>{plugin.description}</Forms.FormText>
@ -202,7 +198,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
</div>
</Forms.FormSection>
{!!plugin.settingsAboutComponent && (
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
<div style={{ marginBottom: 8 }}>
<Forms.FormSection>
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
<plugin.settingsAboutComponent tempSettings={tempSettings} />

View File

@ -38,12 +38,9 @@ export function SettingNumericComponent({ option, pluginSettings, definedSetting
function handleChange(newValue) {
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
setError(null);
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
setState(`${Number.MAX_SAFE_INTEGER}`);
onChange(serialize(newValue));
} else {

View File

@ -36,7 +36,6 @@ export function SettingSelectComponent({ option, pluginSettings, definedSettings
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
setError(null);
setState(newValue);
onChange(newValue);
}

View File

@ -34,7 +34,6 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
setError(null);
setState(newValue);
onChange(newValue);
}

View File

@ -20,22 +20,21 @@ import "./styles.css";
import * as DataStore from "@api/DataStore";
import { showNotice } from "@api/Notices";
import { Settings, useSettings } from "@api/Settings";
import { useSettings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { Badge } from "@components/PluginSettings/components";
import PluginModal from "@components/PluginSettings/PluginModal";
import { Switch } from "@components/Switch";
import { SettingsTab } from "@components/VencordSettings/shared";
import { ChangeList } from "@utils/ChangeList";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import Logger from "@utils/Logger";
import { classes, LazyComponent, useAwaiter } from "@utils/misc";
import { openModalLazy } from "@utils/modal";
import { LazyComponent, useAwaiter } from "@utils/react";
import { Plugin } from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack";
import { Alerts, Button, Card, Forms, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
import { Alerts, Button, Card, Forms, Margins, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
import Plugins from "~plugins";
@ -46,7 +45,6 @@ const cl = classNameFactory("vc-plugins-");
const logger = new Logger("PluginSettings", "#a6d189");
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
const CogWheel = LazyComponent(() => findByCode("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"));
@ -94,7 +92,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
}
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
const settings = Settings.plugins[plugin.name];
const settings = useSettings([`plugins.${plugin.name}`]).plugins[plugin.name];
const isEnabled = () => settings.enabled ?? false;
@ -125,7 +123,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
}
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
if (plugin.patches?.length) {
if (plugin.patches) {
settings.enabled = !wasEnabled;
onRestartNeeded(plugin.name);
return;
@ -155,7 +153,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
<Text variant="text-md/bold" className={cl("name")}>
{plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
</Text>
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
<button role="switch" onClick={() => openModal()} className={classes("button-12Fmur", cl("info-button"))}>
{plugin.options
? <CogWheel />
: <InfoIcon width="24" height="24" />}
@ -177,7 +175,7 @@ enum SearchStatus {
DISABLED
}
export default function PluginSettings() {
export default ErrorBoundary.wrap(function PluginSettings() {
const settings = useSettings();
const changes = React.useMemo(() => new ChangeList<string>(), []);
@ -228,12 +226,9 @@ export default function PluginSettings() {
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
if (!searchValue.value.length) return true;
const v = searchValue.value.toLowerCase();
return (
plugin.name.toLowerCase().includes(v) ||
plugin.description.toLowerCase().includes(v) ||
plugin.tags?.some(t => t.toLowerCase().includes(v))
plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) ||
plugin.description.toLowerCase().includes(searchValue.value.toLowerCase())
);
};
@ -261,9 +256,6 @@ export default function PluginSettings() {
requiredPlugins = [];
for (const p of sortedPlugins) {
if (!p.options && p.name.endsWith("API") && searchValue.value !== "API")
continue;
if (!pluginFilter(p)) continue;
const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled);
@ -304,15 +296,15 @@ export default function PluginSettings() {
}
return (
<SettingsTab title="Plugins">
<Forms.FormSection className={Margins.marginTop16}>
<ReloadRequiredCard required={changes.hasChanges} />
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
Filters
</Forms.FormTitle>
<div className={cl("filter-controls")}>
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.marginBottom20} />
<div className={InputStyles.inputWrapper}>
<Select
className={InputStyles.inputDefault}
@ -329,23 +321,26 @@ export default function PluginSettings() {
</div>
</div>
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
<div className={cl("grid")}>
{plugins}
</div>
<Forms.FormDivider className={Margins.top20} />
<Forms.FormDivider className={Margins.marginTop20} />
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
Required Plugins
</Forms.FormTitle>
<div className={cl("grid")}>
{requiredPlugins}
</div>
</SettingsTab >
</Forms.FormSection >
);
}
}, {
message: "Failed to render the Plugin Settings. If this persists, try using the installer to reinstall!",
onError: handleComponentFailed,
});
function makeDependencyList(deps: string[]) {
return (

View File

@ -18,7 +18,6 @@
import "./Switch.css";
import { classes } from "@utils/misc";
import { findByPropsLazy } from "@webpack";
interface SwitchProps {
@ -34,7 +33,7 @@ const SwitchClasses = findByPropsLazy("slider", "input", "container");
export function Switch({ checked, onChange, disabled }: SwitchProps) {
return (
<div>
<div className={classes(SwitchClasses.container, "default-colors", checked ? SwitchClasses.checked : void 0)} style={{
<div className={`${SwitchClasses.container} default-colors`} style={{
backgroundColor: checked ? SWITCH_ON : SWITCH_OFF,
opacity: disabled ? 0.3 : 1
}}>

View File

@ -16,33 +16,30 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
import { Button, Card, Text } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared";
import { Button, Card, Forms, Margins, Text } from "@webpack/common";
function BackupRestoreTab() {
return (
<SettingsTab title="Backup & Restore">
<Forms.FormSection title="Settings Sync" className={Margins.marginTop16}>
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
<Flex flexDirection="column">
<strong>Warning</strong>
<span>Importing a settings file will overwrite your current settings.</span>
</Flex>
</Card>
<Text variant="text-md/normal" className={Margins.bottom8}>
<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.bottom8}>
<Text variant="text-md/normal" className={Margins.marginBottom8}>
Settings Export contains:
<ul>
<li>&mdash; Custom QuickCSS</li>
<li>&mdash; Theme Links</li>
<li>&mdash; Plugin Settings</li>
</ul>
</Text>
@ -60,8 +57,8 @@ function BackupRestoreTab() {
Export Settings
</Button>
</Flex>
</SettingsTab>
</Forms.FormSection>
);
}
export default wrapTab(BackupRestoreTab, "Backup & Restore");
export default ErrorBoundary.wrap(BackupRestoreTab);

View File

@ -1,165 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { showNotification } from "@api/Notifications";
import { Settings, useSettings } from "@api/Settings";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Link } from "@components/Link";
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
import { Margins } from "@utils/margins";
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared";
function validateUrl(url: string) {
try {
new URL(url);
return true;
} catch {
return "Invalid URL";
}
}
async function eraseAllData() {
const res = await fetch(new URL("/v1/", getCloudUrl()), {
method: "DELETE",
headers: new Headers({
Authorization: await getCloudAuth()
})
});
if (!res.ok) {
cloudLogger.error(`Failed to erase data, API returned ${res.status}`);
showNotification({
title: "Cloud Integrations",
body: `Could not erase all data (API returned ${res.status}), please contact support.`,
color: "var(--red-360)"
});
return;
}
Settings.cloud.authenticated = false;
await deauthorizeCloud();
showNotification({
title: "Cloud Integrations",
body: "Successfully erased all data.",
color: "var(--green-360)"
});
}
function SettingsSyncSection() {
const { cloud } = useSettings(["cloud.authenticated", "cloud.settingsSync"]);
const sectionEnabled = cloud.authenticated && cloud.settingsSync;
return (
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
Synchronize your settings to the cloud. This allows easy synchronization across multiple devices with
minimal effort.
</Forms.FormText>
<Switch
key="cloud-sync"
disabled={!cloud.authenticated}
value={cloud.settingsSync}
onChange={v => { cloud.settingsSync = v; }}
>
Settings Sync
</Switch>
<div className="vc-cloud-settings-sync-grid">
<Button
size={Button.Sizes.SMALL}
disabled={!sectionEnabled}
onClick={() => putCloudSettings()}
>Sync to Cloud</Button>
<Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!">
{({ onMouseLeave, onMouseEnter }) => (
<Button
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
disabled={!sectionEnabled}
onClick={() => getCloudSettings(true, true)}
>Sync from Cloud</Button>
)}
</Tooltip>
<Button
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
disabled={!sectionEnabled}
onClick={() => deleteCloudSettings()}
>Delete Cloud Settings</Button>
</div>
</Forms.FormSection>
);
}
function CloudTab() {
const settings = useSettings(["cloud.authenticated", "cloud.url"]);
return (
<SettingsTab title="Vencord Cloud">
<Forms.FormSection title="Cloud Settings" className={Margins.top16}>
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
Vencord comes with a cloud integration that adds goodies like settings sync across devices.
It <Link href="https://vencord.dev/cloud/privacy">respects your privacy</Link>, and
the <Link href="https://github.com/Vencord/Backend">source code</Link> is AGPL 3.0 licensed so you
can host it yourself.
</Forms.FormText>
<Switch
key="backend"
value={settings.cloud.authenticated}
onChange={v => { v && authorizeCloud(); if (!v) settings.cloud.authenticated = v; }}
note="This will request authorization if you have not yet set up cloud integrations."
>
Enable Cloud Integrations
</Switch>
<Forms.FormTitle tag="h5">Backend URL</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8}>
Which backend to use when using cloud integrations.
</Forms.FormText>
<CheckedTextInput
key="backendUrl"
value={settings.cloud.url}
onChange={v => { settings.cloud.url = v; settings.cloud.authenticated = false; deauthorizeCloud(); }}
validate={validateUrl}
/>
<Button
className={Margins.top8}
size={Button.Sizes.MEDIUM}
color={Button.Colors.RED}
disabled={!settings.cloud.authenticated}
onClick={() => Alerts.show({
title: "Are you sure?",
body: "Once your data is erased, we cannot recover it. There's no going back!",
onConfirm: eraseAllData,
confirmText: "Erase it!",
confirmColor: "vc-cloud-erase-data-danger-btn",
cancelText: "Nevermind"
})}
>Erase All Data</Button>
<Forms.FormDivider className={Margins.top16} />
</Forms.FormSection >
<SettingsSyncSection />
</SettingsTab>
);
}
export default wrapTab(CloudTab, "Cloud");

View File

@ -16,8 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import PluginSettings from "@components/PluginSettings";
import { wrapTab } from "./shared";
export default wrapTab(PluginSettings, "Plugins");
export default ErrorBoundary.wrap(PluginSettings);

View File

@ -16,14 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useSettings } from "@api/Settings";
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { useAwaiter } from "@utils/react";
import { useAwaiter } from "@utils/misc";
import { findLazy } from "@webpack";
import { Card, Forms, React, TextArea } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared";
import { Card, Forms, Margins, React, TextArea } from "@webpack/common";
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
@ -53,7 +51,7 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
return (
<>
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
<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 => (
@ -75,8 +73,8 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
);
}
function ThemesTab() {
const settings = useSettings(["themeLinks"]);
export default ErrorBoundary.wrap(function () {
const settings = useSettings();
const [themeText, setThemeText] = React.useState(settings.themeLinks.join("\n"));
function onBlur() {
@ -90,12 +88,12 @@ function ThemesTab() {
}
return (
<SettingsTab title="Themes">
<Card className="vc-settings-card vc-text-selectable">
<Forms.FormTitle tag="h5">Paste links to .theme.css files here</Forms.FormTitle>
<>
<Card className="vc-settings-card">
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText><strong>Make sure to use the raw links or github.io links!</strong></Forms.FormText>
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
<div style={{ marginBottom: ".5em" }}>
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
@ -104,7 +102,7 @@ function ThemesTab() {
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
</div>
<Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText>
<Forms.FormText>In the GitHub repository of your theme, find X.theme.css, click on it, then click the "Raw" button</Forms.FormText>
<Forms.FormText>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>
@ -117,16 +115,18 @@ function ThemesTab() {
</Card>
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
<TextArea
style={{
padding: ".5em",
border: "1px solid var(--background-modifier-accent)"
}}
value={themeText}
onChange={setThemeText}
className={`${TextAreaProps.textarea} vc-settings-theme-links`}
onChange={e => setThemeText(e.currentTarget.value)}
className={TextAreaProps.textarea}
placeholder="Theme Links"
spellCheck={false}
onBlur={onBlur}
/>
<Validators themeLinks={settings.themeLinks} />
</SettingsTab>
</>
);
}
export default wrapTab(ThemesTab, "Themes");
});

View File

@ -16,21 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useSettings } from "@api/Settings";
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { relaunch } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
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, Switch, Toasts } from "@webpack/common";
import gitHash from "~git-hash";
import { SettingsTab, wrapTab } from "./shared";
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
return async () => {
dispatcher(true);
@ -112,20 +109,21 @@ function Updatable(props: CommonProps) {
</ErrorCard>
</>
) : (
<Forms.FormText className={Margins.bottom8}>
<Forms.FormText className={Margins.marginBottom8}>
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
</Forms.FormText>
)}
{isOutdated && <Changes updates={updates} {...props} />}
<Flex className={classes(Margins.bottom8, Margins.top8)}>
<Flex className={classes(Margins.marginBottom8, Margins.marginTop8)}>
{isOutdated && <Button
size={Button.Sizes.SMALL}
disabled={isUpdating || isChecking}
onClick={withDispatcher(setIsUpdating, async () => {
if (await update()) {
setUpdates([]);
const needFullRestart = await rebuild();
await new Promise<void>(r => {
Alerts.show({
title: "Update Success!",
@ -133,7 +131,10 @@ function Updatable(props: CommonProps) {
confirmText: "Restart",
cancelText: "Not now!",
onConfirm() {
relaunch();
if (needFullRestart)
window.DiscordNative.app.relaunch();
else
location.reload();
r();
},
onCancel: r
@ -174,7 +175,7 @@ function Updatable(props: CommonProps) {
function Newer(props: CommonProps) {
return (
<>
<Forms.FormText className={Margins.bottom8}>
<Forms.FormText className={Margins.marginBottom8}>
Your local copy has more recent commits. Please stash or reset them.
</Forms.FormText>
<Changes {...props} updates={changes} />
@ -183,7 +184,7 @@ function Newer(props: CommonProps) {
}
function Updater() {
const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
const settings = useSettings(["notifyAboutUpdates", "autoUpdate"]);
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
@ -198,12 +199,12 @@ function Updater() {
};
return (
<SettingsTab title="Vencord Updater">
<Forms.FormSection className={Margins.marginTop16}>
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
<Switch
value={settings.notifyAboutUpdates}
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
note="Shows a notification on startup"
note="Shows a toast on startup"
disabled={settings.autoUpdate}
>
Get notified about new updates
@ -215,38 +216,25 @@ function Updater() {
>
Automatically update
</Switch>
<Switch
value={settings.autoUpdateNotification}
onChange={(v: boolean) => settings.autoUpdateNotification = v}
note="Shows a notification when Vencord automatically updates"
disabled={!settings.autoUpdate}
>
Get notified when an automatic update completes
</Switch>
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
<Forms.FormText className="vc-text-selectable">
{repoPending
? repo
: err
? "Failed to retrieve - check console"
: (
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
<Link href={repo}>
{repo.split("/").slice(-2).join("/")}
</Link>
)
}
{" "}(<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)
</Forms.FormText>
)} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText>
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
{isNewer ? <Newer {...commonProps} /> : <Updatable {...commonProps} />}
</SettingsTab>
</Forms.FormSection >
);
}
export default IS_WEB ? null : wrapTab(Updater, "Updater");
export default IS_WEB ? null : ErrorBoundary.wrap(Updater, {
message: "Failed to render the Updater. If this persists, try using the installer to reinstall!",
onError: handleComponentFailed,
});

View File

@ -17,19 +17,16 @@
*/
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { Settings, useSettings } from "@api/Settings";
import { useSettings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import IpcEvents from "@utils/IpcEvents";
import { Margins } from "@utils/margins";
import { identity } from "@utils/misc";
import { relaunch, showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { identity, useAwaiter } from "@utils/misc";
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared";
const cl = classNameFactory("vc-settings-");
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
@ -40,15 +37,15 @@ type KeysOfType<Object, Type> = {
}[keyof Object];
function VencordSettings() {
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
fallbackValue: "Loading..."
});
const settings = useSettings();
const notifSettings = settings.notifications;
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
const isWindows = navigator.platform.toLowerCase().startsWith("win");
const isMac = navigator.platform.toLowerCase().startsWith("mac");
const Switches: Array<false | {
key: KeysOfType<typeof settings, boolean>;
@ -66,16 +63,12 @@ function VencordSettings() {
title: "Enable React Developer Tools",
note: "Requires a full restart"
},
!IS_WEB && (!IS_DISCORD_DESKTOP || !isWindows ? {
!IS_WEB && !isWindows && {
key: "frameless",
title: "Disable the window frame",
note: "Requires a full restart"
} : {
key: "winNativeTitleBar",
title: "Use Windows' native title bar instead of Discord's custom one",
note: "Requires a full restart"
}),
!IS_WEB && false /* This causes electron to freeze / white screen for some people */ && {
},
!IS_WEB && {
key: "transparent",
title: "Enable window transparency",
note: "Requires a full restart"
@ -84,53 +77,48 @@ function VencordSettings() {
key: "winCtrlQ",
title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
note: "Requires a full restart"
},
IS_DISCORD_DESKTOP && {
key: "disableMinSize",
title: "Disable minimum window size",
note: "Requires a full restart"
},
IS_DISCORD_DESKTOP && isMac && {
key: "macosTranslucency",
title: "Enable translucent window",
note: "Requires a full restart"
}
];
return (
<SettingsTab title="Vencord Settings">
<React.Fragment>
<DonateCard image={donateImage} />
<Forms.FormSection title="Quick Actions">
<Card className={cl("quick-actions-card")}>
<React.Fragment>
{!IS_WEB && (
{IS_WEB ? (
<Button
onClick={relaunch}
size={Button.Sizes.SMALL}>
Restart Client
</Button>
)}
<Button
onClick={() => VencordNative.quickCss.openEditor()}
onClick={() => require("../Monaco").launchMonacoEditor()}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}>
Open QuickCSS File
</Button>
{!IS_WEB && (
) : (
<React.Fragment>
<Button
onClick={() => showItemInFolder(settingsDir)}
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.native.openExternal("https://github.com/Vendicated/Vencord")}
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/Vendicated/Vencord")}
size={Button.Sizes.SMALL}
disabled={settingsDirPending}>
Open in GitHub
</Button>
</React.Fragment>
)}
</Card>
</Forms.FormSection>
@ -153,16 +141,8 @@ function VencordSettings() {
</Forms.FormSection>
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />}
</SettingsTab>
);
}
function NotificationSection({ settings }: { settings: typeof Settings["notifications"]; }) {
return (
<>
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
{settings.useNative !== "never" && Notification?.permission === "denied" && (
{notifSettings.useNative !== "never" && Notification.permission === "denied" && (
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
@ -181,66 +161,44 @@ function NotificationSection({ settings }: { settings: typeof Settings["notifica
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
{ label: "Always use Desktop notifications", value: "always" },
{ label: "Always use Vencord notifications", value: "never" },
] satisfies Array<{ value: typeof settings["useNative"]; } & Record<string, any>>}
]satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
closeOnSelect={true}
select={v => settings.useNative = v}
isSelected={v => v === settings.useNative}
select={v => notifSettings.useNative = v}
isSelected={v => v === notifSettings.useNative}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
<Select
isDisabled={settings.useNative === "always"}
isDisabled={notifSettings.useNative === "always"}
placeholder="Notification Position"
options={[
{ label: "Bottom Right", value: "bottom-right", default: true },
{ label: "Top Right", value: "top-right" },
] satisfies Array<{ value: typeof settings["position"]; } & Record<string, any>>}
select={v => settings.position = v}
isSelected={v => v === settings.position}
]satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
select={v => notifSettings.position = v}
isSelected={v => v === notifSettings.position}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
<Slider
disabled={settings.useNative === "always"}
disabled={notifSettings.useNative === "always"}
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
minValue={0}
maxValue={20_000}
initialValue={settings.timeout}
onValueChange={v => settings.timeout = v}
initialValue={notifSettings.timeout}
onValueChange={v => notifSettings.timeout = v}
onValueRender={v => (v / 1000).toFixed(2) + "s"}
onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>
The amount of notifications to save in the log until old ones are removed.
Set to <code>0</code> to disable Notification log and <code></code> to never automatically remove old Notifications
</Forms.FormText>
<Slider
markers={[0, 25, 50, 75, 100, 200]}
minValue={0}
maxValue={200}
stickToMarkers={true}
initialValue={settings.logLimit}
onValueChange={v => settings.logLimit = v}
onValueRender={v => v === 200 ? "∞" : v}
onMarkerRender={v => v === 200 ? "∞" : v}
/>
<Button
onClick={openNotificationLogModal}
disabled={settings.logLimit === 0}
>
Open Notification Log
</Button>
</>
</React.Fragment>
);
}
interface DonateCardProps {
image: string;
}
@ -264,4 +222,4 @@ function DonateCard({ image }: DonateCardProps) {
);
}
export default wrapTab(VencordSettings, "Vencord Settings");
export default ErrorBoundary.wrap(VencordSettings);

View File

@ -0,0 +1,89 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./settingsStyles.css";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { findByCodeLazy } from "@webpack";
import { Forms, SettingsRouter, Text } from "@webpack/common";
import BackupRestoreTab from "./BackupRestoreTab";
import PluginsTab from "./PluginsTab";
import ThemesTab from "./ThemesTab";
import Updater from "./Updater";
import VencordSettings from "./VencordTab";
const cl = classNameFactory("vc-settings-");
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={cl("tab-bar")}
selectedItem={tab}
onItemSelect={SettingsRouter.open}
>
{Object.entries(SettingsTabs).map(([key, { name, component }]) => {
if (!component) return null;
return <TabBar.Item
id={key}
className={cl("tab-bar-item")}
key={key}>
{name}
</TabBar.Item>;
})}
</TabBar>
<Forms.FormDivider />
{CurrentTab && <CurrentTab />}
</Forms.FormSection >;
}
export default function (props: SettingsProps) {
return <ErrorBoundary>
<Settings tab={props.tab} />
</ErrorBoundary>;
}

View File

@ -15,7 +15,7 @@
display: flex;
gap: 1em;
align-items: center;
justify-content: space-evenly;
justify-content: space-between;
flex-grow: 1;
flex-flow: row wrap;
margin-bottom: 1em;
@ -29,38 +29,12 @@
.vc-settings-card {
padding: 1em;
margin-bottom: 1em;
margin-top: 1em;
}
.vc-backup-restore-card {
background-color: var(--info-warning-background);
border-color: var(--info-warning-foreground);
color: var(--info-warning-text);
}
.vc-settings-theme-links {
/* Needed to fix bad themes that hide certain textarea elements for whatever eldritch reason */
display: inline-block !important;
color: var(--text-normal) !important;
padding: 0.5em;
border: 1px solid var(--background-modifier-accent);
}
.vc-cloud-settings-sync-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1em;
}
.vc-cloud-erase-data-danger-btn {
color: var(--white-500);
background-color: var(--button-danger-background);
}
.vc-text-selectable,
.vc-text-selectable :not(a, button, a *, button *) {
/* make text selectable, silly discord makes the entirety of settings not selectable */
user-select: text;
/* discord also sets cursor: default which prevents the cursor from showing as text */
cursor: initial;
margin-top: 0;
}

View File

@ -1,51 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./settingsStyles.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { Margins } from "@utils/margins";
import { onlyOnce } from "@utils/onlyOnce";
import { Forms, Text } from "@webpack/common";
import type { ComponentType, PropsWithChildren } from "react";
export function SettingsTab({ title, children }: PropsWithChildren<{ title: string; }>) {
return (
<Forms.FormSection>
<Text
variant="heading-lg/semibold"
tag="h2"
className={Margins.bottom16}
>
{title}
</Text>
{children}
</Forms.FormSection>
);
}
const onError = onlyOnce(handleComponentFailed);
export function wrapTab(component: ComponentType, tab: string) {
return ErrorBoundary.wrap(component, {
message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`,
onError,
});
}

View File

@ -16,12 +16,29 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { maybePromptToUpdate } from "@utils/updater";
import { isOutdated, rebuild, update } from "@utils/updater";
export function handleComponentFailed() {
maybePromptToUpdate(
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

@ -1,6 +1,6 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -16,13 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const enum UserType {
Banned = -1,
Normal = 0,
Admin = 1
}
export interface ReviewDBUser {
lastReviewID: number,
type: UserType;
}
export { default as PatchHelper } from "./PatchHelper";
export { default as PluginSettings } from "./PluginSettings";
export { default as VencordSettings } from "./VencordSettings";

View File

@ -1,15 +1,11 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Vencord QuickCSS Editor</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.37.1/min/vs/editor/editor.main.min.css"
integrity="sha512-wB3xfL98hWg1bpkVYSyL0js/Jx9s7FsDg9aYO6nOMSJTgPuk/PFqxXQJKKSUjteZjeYrfgo9NFBOA1r9HwDuZw=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<head>
<meta charset="utf-8">
<title>QuickCss Editor</title>
<link rel="stylesheet" data-name="vs/editor/editor.main"
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.min.css">
<style>
html,
body,
@ -24,34 +20,21 @@
overflow: hidden;
}
</style>
</head>
</head>
<body>
<body>
<div id="container"></div>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.37.1/min/vs/loader.min.js"
integrity="sha512-A+6SvPGkIN9Rf0mUXmW4xh7rDvALXf/f0VtOUiHlDUSPknu2kcfz1KzLpOJyL2pO+nZS13hhIjLqVgiQExLJrw=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/loader.min.js"></script>
<script>
require.config({
paths: {
vs: "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.37.1/min/vs",
},
});
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs' } });
require(["vs/editor/editor.main"], () => {
getCurrentCss().then((css) => {
var editor = monaco.editor.create(
document.getElementById("container"),
{
getCurrentCss().then(css => {
var editor = monaco.editor.create(document.getElementById('container'), {
value: css,
language: "css",
language: 'css',
theme: getTheme(),
}
);
});
editor.onDidChangeModelContent(() =>
setCss(editor.getValue())
);
@ -61,6 +44,9 @@
});
});
});
</script>
</body>
</body>
</html>

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import Logger from "@utils/Logger";
if (IS_DEV) {
var traces = {} as Record<string, [number, any[]]>;

7
src/globals.d.ts vendored
View File

@ -35,8 +35,6 @@ declare global {
export var IS_WEB: boolean;
export var IS_DEV: boolean;
export var IS_STANDALONE: boolean;
export var IS_DISCORD_DESKTOP: boolean;
export var IS_VENCORD_DESKTOP: boolean;
export var VencordNative: typeof import("./VencordNative").default;
export var Vencord: typeof import("./Vencord");
@ -53,11 +51,10 @@ declare global {
* Only available when running in Electron, undefined on web.
* Thus, avoid using this or only use it inside an {@link IS_WEB} guard.
*
* If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)
* If you really must use it, mark your plugin as Desktop App only via
* `target: "DESKTOP"`
*/
export var DiscordNative: any;
export var VencordDesktop: any;
export var VencordDesktopNative: any;
interface Window {
webpackChunkdiscord_app: {

View File

@ -33,5 +33,3 @@ export const ALLOWED_PROTOCOLS = [
"steam:",
"spotify:"
];
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");

View File

@ -19,7 +19,7 @@
import "./updater";
import { debounce } from "@utils/debounce";
import { IpcEvents } from "@utils/IpcEvents";
import IpcEvents from "@utils/IpcEvents";
import { Queue } from "@utils/Queue";
import { BrowserWindow, ipcMain, shell } from "electron";
import { mkdirSync, readFileSync, watch } from "fs";
@ -28,7 +28,7 @@ import { join } from "path";
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
mkdirSync(SETTINGS_DIR, { recursive: true });
@ -44,14 +44,6 @@ export function readSettings() {
}
}
export function getSettings(): typeof import("@api/Settings").Settings {
try {
return JSON.parse(readSettings());
} catch {
return {} as any;
}
}
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
@ -93,7 +85,7 @@ export function initIpc(mainWindow: BrowserWindow) {
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
const win = new BrowserWindow({
title: "Vencord QuickCSS Editor",
title: "QuickCss Editor",
autoHideMenuBar: true,
darkTheme: true,
webPreferences: {

View File

@ -16,13 +16,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { createHash } from "crypto";
import { createReadStream } from "fs";
import { join } from "path";
export const VENCORD_FILES = [
IS_DISCORD_DESKTOP ? "patcher.js" : "vencordDesktopMain.js",
"preload.js",
IS_DISCORD_DESKTOP ? "renderer.js" : "vencordDesktopRenderer.js",
"renderer.css"
];
export async function calculateHashes() {
const hashes = {} as Record<string, string>;
await Promise.all(
["patcher.js", "preload.js", "renderer.js", "renderer.css"].map(file => new Promise<void>(r => {
const fis = createReadStream(join(__dirname, file));
const hash = createHash("sha1", { encoding: "hex" });
fis.once("end", () => {
hash.end();
hashes[file] = hash.read();
r();
});
fis.pipe(hash);
}))
);
return hashes;
}
export function serializeErrors(func: (...args: any[]) => any) {
return async function () {

View File

@ -16,13 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { IpcEvents } from "@utils/IpcEvents";
import IpcEvents from "@utils/IpcEvents";
import { execFile as cpExecFile } from "child_process";
import { ipcMain } from "electron";
import { join } from "path";
import { promisify } from "util";
import { serializeErrors } from "./common";
import { calculateHashes, serializeErrors } from "./common";
const VENCORD_SRC_DIR = join(__dirname, "..");
@ -76,6 +76,7 @@ async function build() {
return !res.stderr.includes("Build failed");
}
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo));
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull));

View File

@ -17,7 +17,7 @@
*/
import { VENCORD_USER_AGENT } from "@utils/constants";
import { IpcEvents } from "@utils/IpcEvents";
import IpcEvents from "@utils/IpcEvents";
import { ipcMain } from "electron";
import { writeFile } from "fs/promises";
import { join } from "path";
@ -25,8 +25,8 @@ import { join } from "path";
import gitHash from "~git-hash";
import gitRemote from "~git-remote";
import { get } from "../utils/simpleGet";
import { serializeErrors, VENCORD_FILES } from "./common";
import { get } from "../simpleGet";
import { calculateHashes, serializeErrors } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
let PendingUpdates = [] as [string, string][];
@ -66,7 +66,7 @@ async function fetchUpdates() {
return false;
data.assets.forEach(({ name, browser_download_url }) => {
if (VENCORD_FILES.some(s => name.startsWith(s))) {
if (["patcher.js", "preload.js", "renderer.js", "renderer.css"].some(s => name.startsWith(s))) {
PendingUpdates.push([name, browser_download_url]);
}
});
@ -75,15 +75,13 @@ async function fetchUpdates() {
async function applyUpdates() {
await Promise.all(PendingUpdates.map(
async ([name, data]) => writeFile(
join(__dirname, name),
await get(data)
)
));
async ([name, data]) => writeFile(join(__dirname, name), await get(data)))
);
PendingUpdates = [];
return true;
}
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(() => `https://github.com/${gitRemote}`));
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(fetchUpdates));

View File

@ -1,110 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { app, protocol, session } from "electron";
import { join } from "path";
import { getSettings } from "./ipcMain";
import { IS_VANILLA } from "./utils/constants";
import { installExt } from "./utils/extensions";
if (IS_VENCORD_DESKTOP || !IS_VANILLA) {
app.whenReady().then(() => {
// Source Maps! Maybe there's a better way but since the renderer is executed
// from a string I don't think any other form of sourcemaps would work
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
let url = unsafeUrl.slice("vencord://".length);
if (url.endsWith("/")) url = url.slice(0, -1);
switch (url) {
case "renderer.js.map":
case "vencordDesktopRenderer.js.map":
case "preload.js.map":
case "patcher.js.map":
case "vencordDesktopMain.js.map":
cb(join(__dirname, url));
break;
default:
cb({ statusCode: 403 });
}
});
try {
if (getSettings().enableReactDevtools)
installExt("fmkadmapgofadopljbjfkapdkoienihi")
.then(() => console.info("[Vencord] Installed React Developer Tools"))
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
} catch { }
// Remove CSP
type PolicyResult = Record<string, string[]>;
const parsePolicy = (policy: string): PolicyResult => {
const result: PolicyResult = {};
policy.split(";").forEach(directive => {
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyResult): string =>
Object.entries(policy)
.filter(([, values]) => values?.length)
.map(directive => directive.flat().join(" "))
.join("; ");
function patchCsp(headers: Record<string, string[]>, header: string) {
if (header in headers) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
}
// TODO: Restrict this to only imported packages with fixed version.
// Perhaps auto generate with esbuild
csp["script-src"] ??= [];
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
}
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders, "content-security-policy");
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet")
responseHeaders["content-type"] = ["text/css"];
}
cb({ cancel: false, responseHeaders });
});
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
// impossible to load css from github raw despite our fix above
session.defaultSession.webRequest.onHeadersReceived = () => { };
});
}
if (IS_DISCORD_DESKTOP) {
require("./patcher");
}

View File

@ -20,8 +20,9 @@ import { onceDefined } from "@utils/onceDefined";
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
import { dirname, join } from "path";
import { getSettings, initIpc } from "./ipcMain";
import { IS_VANILLA } from "./utils/constants";
import { initIpc } from "./ipcMain";
import { installExt } from "./ipcMain/extensions";
import { readSettings } from "./ipcMain/index";
console.log("[Vencord] Starting up...");
@ -40,8 +41,11 @@ require.main!.filename = join(asarPath, discordPkg.main);
// @ts-ignore Untyped method? Dies from cringe
app.setAppPath(asarPath);
if (!IS_VANILLA) {
const settings = getSettings();
if (!process.argv.includes("--vanilla")) {
let settings: typeof import("@api/settings").Settings = {} as any;
try {
settings = JSON.parse(readSettings());
} catch { }
// Repatch after host updates on Windows
if (process.platform === "win32") {
@ -75,21 +79,12 @@ if (!IS_VANILLA) {
options.webPreferences.sandbox = false;
if (settings.frameless) {
options.frame = false;
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
delete options.frame;
}
// This causes electron to freeze / white screen for some people
if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) {
if (settings.transparent) {
options.transparent = true;
options.backgroundColor = "#00000000";
}
if (settings.macosTranslucency && process.platform === "darwin") {
options.backgroundColor = "#00000000";
options.vibrancy = "sidebar";
}
process.env.DISCORD_PRELOAD = original;
super(options);
@ -111,19 +106,85 @@ if (!IS_VANILLA) {
BrowserWindow
};
// Patch appSettings to force enable devtools and optionally disable min size
onceDefined(global, "appSettings", s => {
s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true);
if (settings.disableMinSize) {
s.set("MIN_WIDTH", 0);
s.set("MIN_HEIGHT", 0);
} else {
s.set("MIN_WIDTH", 940);
s.set("MIN_HEIGHT", 500);
// Patch appSettings to force enable devtools
onceDefined(global, "appSettings", s =>
s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true)
);
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
electron.app.whenReady().then(() => {
// Source Maps! Maybe there's a better way but since the renderer is executed
// from a string I don't think any other form of sourcemaps would work
electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
let url = unsafeUrl.slice("vencord://".length);
if (url.endsWith("/")) url = url.slice(0, -1);
switch (url) {
case "renderer.js.map":
case "preload.js.map":
case "patcher.js.map": // doubt
cb(join(__dirname, url));
break;
default:
cb({ statusCode: 403 });
}
});
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
try {
if (settings?.enableReactDevtools)
installExt("fmkadmapgofadopljbjfkapdkoienihi")
.then(() => console.info("[Vencord] Installed React Developer Tools"))
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
} catch { }
// Remove CSP
type PolicyResult = Record<string, string[]>;
const parsePolicy = (policy: string): PolicyResult => {
const result: PolicyResult = {};
policy.split(";").forEach(directive => {
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyResult): string =>
Object.entries(policy)
.filter(([, values]) => values?.length)
.map(directive => directive.flat().join(" "))
.join("; ");
function patchCsp(headers: Record<string, string[]>, header: string) {
if (header in headers) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
}
// TODO: Restrict this to only imported packages with fixed version.
// Perhaps auto generate with esbuild
csp["script-src"] ??= [];
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
}
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders, "content-security-policy");
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet")
responseHeaders["content-type"] = ["text/css"];
}
cb({ cancel: false, responseHeaders });
});
});
} else {
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
}

View File

@ -32,10 +32,10 @@ export default definePlugin({
}
},
{
find: '"7z","ade","adp"',
find: "\"github.com\":new RegExp(\"\\\\/releases\\\\S*\\\\/download\"),",
replacement: {
match: /JSON\.parse\('\[.+?'\)/,
replace: "[]"
match: /const o=JSON.parse\('\[.+?'\)/,
replace: "const o=[]"
}
}
]

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/Settings";
import { Settings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";

View File

@ -16,25 +16,25 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
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 { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import IpcEvents from "@utils/IpcEvents";
import Logger from "@utils/Logger";
import { closeModal, Modals, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { Forms, Toasts } from "@webpack/common";
import { Forms, Margins } from "@webpack/common";
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/attachments/1033680203433660458/1092089947126780035/favicon.png";
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 = {
description: "Vencord Contributor",
tooltip: "Vencord Contributor",
image: CONTRIBUTOR_BADGE,
position: BadgePosition.START,
props: {
@ -44,89 +44,63 @@ const ContributorBadge: ProfileBadge = {
}
},
shouldShow: ({ user }) => contributorIds.includes(user.id),
link: "https://github.com/Vendicated/Vencord"
onClick: () => VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/Vendicated/Vencord")
};
let DonorBadges = {} as Record<string, Pick<ProfileBadge, "image" | "description">[]>;
async function loadBadges(noCache = false) {
DonorBadges = {};
const init = {} as RequestInit;
if (noCache)
init.cache = "no-cache";
const badges = await fetch("https://gist.githubusercontent.com/Vendicated/51a3dd775f6920429ec6e9b735ca7f01/raw/badges.csv", init)
.then(r => r.text());
const lines = badges.trim().split("\n");
if (lines.shift() !== "id,tooltip,image") {
new Logger("BadgeAPI").error("Invalid badges.csv file!");
return;
}
for (const line of lines) {
const [id, description, image] = line.split(",");
(DonorBadges[id] ??= []).push({ image, description });
}
}
const DonorBadges = {} as Record<string, Pick<ProfileBadge, "image" | "tooltip">>;
export default definePlugin({
name: "BadgeAPI",
description: "API to add badges to users.",
authors: [Devs.Megu, Devs.Ven, Devs.TheSun],
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: /(?<=(\i)\.isTryItOutFlow,)(.{0,300})null==\i\?void 0:(\i)\.getBadges\(\)/,
replace: (_, props, restCode, badgesMod) => `vencordProps=${props},${restCode}Vencord.Api.Badges._getBadges(vencordProps).concat(${badgesMod}?.getBadges()??[])`,
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,`
},
{
// alt: "", aria-hidden: false, src: originalSrc
match: /alt:" ","aria-hidden":!0,src:(?=(\i)\.src)/g,
// ...badge.props, ..., src: badge.image ?? ...
replace: "...$1.props,$& $1.image??"
},
{
match: /children:function(?<=(\i)\.(?:tooltip|description),spacing:\d.+?)/g,
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) : function"
},
{
match: /onClick:function(?=.{0,200}href:(\i)\.link)/,
replace: "onClick:$1.onClick??function"
match: /spacing:(\d{1,2}),children:(.{1,40}(\i)\.jsx.+?(\i)\.onClick.+?\)})},/,
// if the badge provides it's own component, render that instead of an image
// the badge also includes info about the user that has it (type BadgeUserArgs), which is why it's passed as props
replace: (_, s, origBadgeComponent, React, badge) =>
`spacing:${s},children:${badge}.component ? () => (0,${React}.jsx)(${badge}.component, { ...${badge} }) : ${origBadgeComponent}},`
}
]
}
],
toolboxActions: {
async "Refetch Badges"() {
await loadBadges(true);
Toasts.show({
id: Toasts.genId(),
message: "Successfully refetched badges!",
type: Toasts.Type.SUCCESS
});
async start() {
Vencord.Api.Badges.addBadge(ContributorBadge);
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 };
}
},
async start() {
Vencord.Api.Badges.addBadge(ContributorBadge);
await loadBadges();
},
renderBadgeComponent: ErrorBoundary.wrap((badge: ProfileBadge & BadgeUserArgs) => {
const Component = badge.component!;
return <Component {...badge} />;
}, { noop: true }),
getDonorBadges(userId: string) {
return DonorBadges[userId]?.map(badge => ({
addDonorBadge(badges: ProfileBadge[], userId: string) {
const badge = DonorBadges[userId];
if (badge) {
badges.unshift({
...badge,
position: BadgePosition.START,
props: {
@ -139,7 +113,7 @@ export default definePlugin({
const modalKey = openModal(props => (
<ErrorBoundary noop onError={() => {
closeModal(modalKey);
VencordNative.native.openExternal("https://github.com/sponsors/Vendicated");
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/sponsors/Vendicated");
}}>
<Modals.ModalRoot {...props}>
<Modals.ModalHeader>
@ -176,7 +150,7 @@ export default definePlugin({
<Forms.FormText>
This Badge is a special perk for Vencord Donors
</Forms.FormText>
<Forms.FormText className={Margins.top20}>
<Forms.FormText className={Margins.marginTop20}>
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
</Forms.FormText>
</div>
@ -190,6 +164,7 @@ export default definePlugin({
</ErrorBoundary>
));
},
}));
});
}
}
});

View File

@ -0,0 +1,82 @@
/*
* 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

@ -22,34 +22,22 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "MessageEventsAPI",
description: "Api required by anything using message events.",
authors: [Devs.Arjix, Devs.hunt, Devs.Ven],
authors: [Devs.Arjix],
patches: [
{
find: '"MessageActionCreators"',
replacement: {
// editMessage: function (...) {
match: /\beditMessage:(function\(.+?\))\{/,
// editMessage: async function (...) { await handlePreEdit(...); ...
replace: "editMessage:async $1{await Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
}
},
{
find: ".handleSendMessage=",
replacement: {
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)
match: /(props\.chatInputType.+?\.then\(\()(function.+?var (\i)=\i\.\i\.parse\((\i),.+?var (\i)=\i\.\i\.getSendMessageOptionsForReply\(\i\);)(?<=\)\(({.+?})\)\.then.+?)/,
// props.chatInputType...then((async function(isMessageValid)... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply); if(await Vencord.api...) return { shoudClear:true, shouldRefocus:true };
replace: (_, rest1, rest2, parsedMessage, channel, replyOptions, extra) => "" +
`${rest1}async ${rest2}` +
`if(await Vencord.Api.MessageEvents._handlePreSend(${channel}.id,${parsedMessage},${extra},${replyOptions}))` +
"return{shoudClear:true,shouldRefocus:true};"
}
find: "sendMessage:function",
replacement: [{
match: /(?<=_sendMessage:function\([^)]+\)){/,
replace: "{if(Vencord.Api.MessageEvents._handlePreSend(...arguments)){return;};"
}, {
match: /(?<=\beditMessage:function\([^)]+\)){/,
replace: "{Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
}]
},
{
find: '("interactionUsernameProfile',
replacement: {
match: /var \i=(\i)\.id,\i=(\i)\.id;return \i\.useCallback\(\(?function\((\i)\){/,
match: /var \w=(\w)\.id,\w=(\w)\.id;return .{1,2}\.useCallback\(\(?function\((.{1,2})\){/,
replace: (m, message, channel, event) =>
// the message param is shadowed by the event param, so need to alias them
`var _msg=${message},_chan=${channel};${m}Vencord.Api.MessageEvents._handleClick(_msg, _chan, ${event});`

View File

@ -22,16 +22,16 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "MessagePopoverAPI",
description: "API to add buttons to message popovers.",
authors: [Devs.KingFish, Devs.Ven, Devs.Nuckyz],
authors: [Devs.KingFish, Devs.Ven],
patches: [{
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: {
// foo && !bar ? createElement(reactionStuffs)... createElement(blah,...makeElement(reply-other))
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderEmojiPicker:.{0,500}\?(\i)\(\{key:"reply-other"/,
replace: (m, makeElement) => {
// foo && !bar ? createElement(blah,...makeElement(addReactionData))
match: /(\i&&!\i)\?\(0,\i\.jsxs?\)\(.{0,20}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/,
replace: (m, bools, makeElement) => {
const msg = m.match(/message:(.{1,3}),/)?.[1];
if (!msg) throw new Error("Could not find message variable");
return `...Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}),${m}`;
return `...(${bools}?Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}):[]),${m}`;
}
}
}],

View File

@ -29,12 +29,13 @@ export default definePlugin({
find: 'displayName="NoticeStore"',
replacement: [
{
match: /(?=;\i=null;.{0,70}getPremiumSubscription)/g,
replace: ";if(Vencord.Api.Notices.currentNotice)return false"
match: /;.{1,2}=null;.{0,70}getPremiumSubscription/g,
replace:
";if(Vencord.Api.Notices.currentNotice)return false$&"
},
{
match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/,
replace: (_, notice) => `if(${notice}.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);`
replace: 'if($1?.id=="VencordNotice")return($1=null,Vencord.Api.Notices.nextNotice(),true);'
}
]
}

View File

@ -32,7 +32,7 @@ export default definePlugin({
}
},
{
find: "Messages.SERVERS,children",
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,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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "SettingsStoreAPI",
description: "Patches Discord's SettingsStores to expose their group and name",
authors: [Devs.Nuckyz],
patches: [
{
find: '"textAndImages","renderSpoilers"',
replacement: [
{
match: /(?<=INFREQUENT_USER_ACTION.{0,20}),useSetting:function/,
replace: ",settingsStoreApiGroup:arguments[0],settingsStoreApiName:arguments[1]$&"
}
]
}
]
});

View File

@ -48,6 +48,7 @@ export default definePlugin({
name: "WebRichPresence (arRPC)",
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
authors: [Devs.Ducko],
target: "WEB",
settingsAboutComponent: () => (
<>
@ -59,9 +60,6 @@ export default definePlugin({
),
async start() {
// ArmCord comes with its own arRPC implementation, so this plugin just confuses users
if ("armcord" in window) return;
if (ws) ws.close();
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket

View File

@ -1,84 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { i18n, React, useStateFromStores } from "@webpack/common";
const cl = classNameFactory("vc-bf-");
const classes = findByPropsLazy("sidebar", "guilds");
const Animations = findByPropsLazy("a", "animated", "useTransition");
const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
function Guilds(props: {
className: string;
bfGuildFolders: any[];
}) {
// @ts-expect-error
const res = Vencord.Plugins.plugins.BetterFolders.Guilds(props);
const scrollerProps = res.props.children?.props?.children?.[1]?.props;
if (scrollerProps?.children) {
const servers = scrollerProps.children.find(c => c?.props?.["aria-label"] === i18n.Messages.SERVERS);
if (servers) scrollerProps.children = servers;
}
return res;
}
export default ErrorBoundary.wrap(() => {
const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders());
const fullscreen = useStateFromStores([ChannelRTCStore], () => ChannelRTCStore.isFullscreenInContext());
const guilds = document.querySelector(`.${classes.guilds}`);
const visible = !!expandedFolders.size;
const className = cl("folder-sidebar", { fullscreen });
const Sidebar = (
<Guilds
className={classes.guilds}
bfGuildFolders={Array.from(expandedFolders)}
/>
);
if (!guilds || !Settings.plugins.BetterFolders.sidebarAnim)
return visible
? <div className={className}>{Sidebar}</div>
: null;
return (
<Animations.Transition
items={visible}
from={{ width: 0 }}
enter={{ width: guilds.getBoundingClientRect().width }}
leave={{ width: 0 }}
config={{ duration: 200 }}
>
{(style, show) => show && (
<Animations.animated.div style={style} className={className}>
{Sidebar}
</Animations.animated.div>
)}
</Animations.Transition>
);
}, { noop: true });

View File

@ -1,17 +0,0 @@
.vc-bf-folder-sidebar [class*="wrapper-"] > [class*="listItem-"]:first-of-type,
.vc-bf-folder-sidebar [class*="unreadMentionsIndicator"] {
display: none;
}
.vc-bf-folder-sidebar [class*="expandedFolderBackground-"] {
background-color: transparent;
}
.vc-bf-folder-sidebar {
display: flex;
}
.vc-bf-fullscreen {
width: 0 !important;
visibility: hidden;
}

View File

@ -1,177 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./betterFolders.css";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher } from "@webpack/common";
import FolderSideBar from "./FolderSideBar";
const GuildsTree = findLazy(m => m.prototype?.convertToFolder);
const GuildFolderStore = findStoreLazy("SortedGuildStore");
const ExpandedFolderStore = findStoreLazy("ExpandedGuildFolderStore");
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
const settings = definePluginSettings({
sidebar: {
type: OptionType.BOOLEAN,
description: "Display servers from folder on dedicated sidebar",
default: true,
},
sidebarAnim: {
type: OptionType.BOOLEAN,
description: "Animate opening the folder sidebar",
default: true,
},
closeAllFolders: {
type: OptionType.BOOLEAN,
description: "Close all folders when selecting a server not in a folder",
default: false,
},
closeAllHomeButton: {
type: OptionType.BOOLEAN,
description: "Close all folders when clicking on the home button",
default: false,
},
closeOthers: {
type: OptionType.BOOLEAN,
description: "Close other folders when opening a folder",
default: false,
},
forceOpen: {
type: OptionType.BOOLEAN,
description: "Force a folder to open when switching to a server of that folder",
default: false,
},
});
export default definePlugin({
name: "BetterFolders",
description: "Shows server folders on dedicated sidebar and adds folder related improvements",
authors: [Devs.juby, Devs.AutumnVN],
patches: [
{
find: '("guildsnav")',
predicate: () => settings.store.sidebar,
replacement: [
{
match: /(\i)\(\){return \i\(\(0,\i\.jsx\)\("div",{className:\i\(\)\.guildSeparator}\)\)}/,
replace: "$&$self.Separator=$1;"
},
// Folder component patch
{
match: /\i\(\(function\(\i,\i,\i\){var \i=\i\.key;return.+\(\i\)},\i\)}\)\)/,
replace: "arguments[0].bfHideServers?null:$&"
},
// BEGIN Guilds component patch
{
match: /(\i)\.themeOverride,(.{15,25}\(function\(\){var \i=)(\i\.\i\.getGuildsTree\(\))/,
replace: "$1.themeOverride,bfPatch=$1.bfGuildFolders,$2bfPatch?$self.getGuildsTree(bfPatch,$3):$3"
},
{
match: /return(\(0,\i\.jsx\))(\(\i,{)(folderNode:\i,setNodeRef:\i\.setNodeRef,draggable:!0,.+},\i\.id\));case/,
replace: "var bfHideServers=typeof bfPatch==='undefined',folder=$1$2bfHideServers,$3;return !bfHideServers&&arguments[1]?[$1($self.Separator,{}),folder]:folder;case"
},
// END
{
match: /\("guildsnav"\);return\(0,\i\.jsx\)\(.{1,6},{navigator:\i,children:\(0,\i\.jsx\)\(/,
replace: "$&$self.Guilds="
}
]
},
{
find: "APPLICATION_LIBRARY,render",
predicate: () => settings.store.sidebar,
replacement: {
match: /(\(0,\i\.jsx\))\(\i\..,{className:\i\(\)\.guilds,themeOverride:\i}\)/,
replace: "$&,$1($self.FolderSideBar,{})"
}
},
{
find: '("guildsnav")',
predicate: () => settings.store.closeAllHomeButton,
replacement: {
match: ",onClick:function(){if(!__OVERLAY__){",
replace: "$&$self.closeFolders();"
}
}
],
settings,
start() {
const getGuildFolder = (id: string) => GuildFolderStore.getGuildFolders().find(f => f.guildIds.includes(id));
FluxDispatcher.subscribe("CHANNEL_SELECT", this.onSwitch = data => {
if (!settings.store.closeAllFolders && !settings.store.forceOpen)
return;
if (this.lastGuildId !== data.guildId) {
this.lastGuildId = data.guildId;
const guildFolder = getGuildFolder(data.guildId);
if (guildFolder?.folderId) {
if (settings.store.forceOpen && !ExpandedFolderStore.isFolderExpanded(guildFolder.folderId))
FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);
} else if (settings.store.closeAllFolders)
this.closeFolders();
}
});
FluxDispatcher.subscribe("TOGGLE_GUILD_FOLDER_EXPAND", this.onToggleFolder = e => {
if (settings.store.closeOthers && !this.dispatching)
FluxDispatcher.wait(() => {
const expandedFolders = ExpandedFolderStore.getExpandedFolders();
if (expandedFolders.size > 1) {
this.dispatching = true;
for (const id of expandedFolders) if (id !== e.folderId)
FolderUtils.toggleGuildFolderExpand(id);
this.dispatching = false;
}
});
});
},
stop() {
FluxDispatcher.unsubscribe("CHANNEL_SELECT", this.onSwitch);
FluxDispatcher.unsubscribe("TOGGLE_GUILD_FOLDER_EXPAND", this.onToggleFolder);
},
FolderSideBar,
getGuildsTree(folders, oldTree) {
const tree = new GuildsTree();
tree.root.children = oldTree.root.children.filter(e => folders.includes(e.id));
tree.nodes = folders.map(id => oldTree.nodes[id]);
return tree;
},
closeFolders() {
for (const id of ExpandedFolderStore.getExpandedFolders())
FolderUtils.toggleGuildFolderExpand(id);
},
});

View File

@ -16,8 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/Settings";
import { Settings } from "@api/settings";
import { Devs } from "@utils/constants";
import { makeLazy } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
export default definePlugin({
@ -29,7 +30,7 @@ export default definePlugin({
{
find: "hideNote:",
all: true,
predicate: () => Vencord.Settings.plugins.BetterNotesBox.hide,
predicate: makeLazy(() => Vencord.Settings.plugins.BetterNotesBox.hide),
replacement: {
match: /hideNote:.+?(?=[,}])/g,
replace: "hideNote:true",

View File

@ -16,11 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/Settings";
import { migratePluginSettings, Settings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Clipboard, Toasts } from "@webpack/common";
migratePluginSettings("BetterRoleDot", "ClickableRoleDot");
export default definePlugin({
name: "BetterRoleDot",
authors: [Devs.Ven],
@ -29,21 +30,21 @@ export default definePlugin({
patches: [
{
find: ".dotBorderBase",
find: "M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V16C20 18.2091 18.2091 20 16 20H4C1.79086 20 0 18.2091 0 16V4Z",
replacement: {
match: /,viewBox:"0 0 20 20"/,
replace: "$&,onClick:()=>$self.copyToClipBoard(arguments[0].color),style:{cursor:'pointer'}",
match: /viewBox:"0 0 20 20"/,
replace: "$&,onClick:()=>$self.copyToClipBoard(e.color),style:{cursor:'pointer'}",
},
},
{
find: '"dot"===',
find: '"username"===',
all: true,
predicate: () => Settings.plugins.BetterRoleDot.bothStyles,
replacement: {
match: /"(?:username|dot)"===\i(?!\.\i)/g,
match: /"(?:username|dot)"===\w(?!\.\w)/g,
replace: "true",
},
}
},
],
options: {

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/Settings";
import { Settings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/Settings";
import { Settings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";

View File

@ -23,6 +23,7 @@ import {
removePreEditListener,
removePreSendListener
} from "@api/MessageEvents";
import { migratePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
@ -32,6 +33,7 @@ import { defaultRules } from "./defaultRules";
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
const reHasRegExpChar = RegExp(reRegExpChar.source);
migratePluginSettings("ClearURLs", "clearURLs");
export default definePlugin({
name: "ClearURLs",
description: "Removes tracking garbage from URLs",

View File

@ -27,16 +27,11 @@ export default definePlugin({
{
find: "Masks.STATUS_ONLINE",
replacement: {
// we can use global replacement here - these are specific to the status icons and are used nowhere else,
// so it keeps the patch and plugin small and simple
match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g,
replace: "Masks.STATUS_ONLINE"
}
},
{
find: ".AVATAR_STATUS_MOBILE_16;",
replacement: {
match: /(\.fromIsMobile,.+?)\i.status/,
replace: (_, rest) => `${rest}"online"`
}
}
]
});

View File

@ -17,13 +17,7 @@
*/
import { Devs } from "@utils/constants";
import { relaunch } from "@utils/native";
import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches";
import definePlugin from "@utils/types";
import * as Webpack from "@webpack";
import { extract, filters, findAll, search } from "@webpack";
import { React, ReactDOM } from "@webpack/common";
import type { ComponentType } from "react";
const WEB_ONLY = (f: string) => () => {
throw new Error(`'${f}' is Discord Desktop only.`);
@ -35,65 +29,23 @@ export default definePlugin({
authors: [Devs.Ven],
getShortcuts() {
function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) {
const cache = new Map<string, unknown>();
return function (...filterProps: unknown[]) {
const cacheKey = String(filterProps);
if (cache.has(cacheKey)) return cache.get(cacheKey);
const matches = findAll(filterFactory(...filterProps));
const result = (() => {
switch (matches.length) {
case 0: return null;
case 1: return matches[0];
default:
const uniqueMatches = [...new Set(matches)];
if (uniqueMatches.length > 1)
console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches);
return matches[0];
}
})();
if (result && cacheKey) cache.set(cacheKey, result);
return result;
};
}
let fakeRenderWin: WeakRef<Window> | undefined;
return {
toClip: IS_WEB ? WEB_ONLY("toClip") : window.DiscordNative.clipboard.copy,
fromClip: IS_WEB ? WEB_ONLY("fromClip") : window.DiscordNative.clipboard.read,
wp: Vencord.Webpack,
wpc: Webpack.wreq.c,
wreq: Webpack.wreq,
wpsearch: search,
wpex: extract,
wpc: Vencord.Webpack.wreq.c,
wreq: Vencord.Webpack.wreq,
wpsearch: Vencord.Webpack.search,
wpex: Vencord.Webpack.extract,
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!),
find: newFindWrapper(f => f),
findAll,
findByProps: newFindWrapper(filters.byProps),
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
findByCode: newFindWrapper(filters.byCode),
findAllByCode: (code: string) => findAll(filters.byCode(code)),
findStore: newFindWrapper(filters.byStoreName),
PluginsApi: Vencord.Plugins,
plugins: Vencord.Plugins.plugins,
React,
findByProps: Vencord.Webpack.findByProps,
find: Vencord.Webpack.find,
Plugins: Vencord.Plugins,
React: Vencord.Webpack.Common.React,
Settings: Vencord.Settings,
Api: Vencord.Api,
reload: () => location.reload(),
restart: IS_WEB ? WEB_ONLY("restart") : relaunch,
canonicalizeMatch,
canonicalizeReplace,
canonicalizeReplacement,
fakeRender: (component: ComponentType, props: any) => {
const prevWin = fakeRenderWin?.deref();
const win = prevWin?.closed === false ? prevWin : window.open("about:blank", "Fake Render", "popup,width=500,height=500")!;
fakeRenderWin = new WeakRef(win);
win.focus();
ReactDOM.render(React.createElement(component, props), win.document.body);
}
restart: IS_WEB ? WEB_ONLY("restart") : window.DiscordNative.app.relaunch
};
},

105
src/plugins/corruptMp4s.ts Normal file
View File

@ -0,0 +1,105 @@
/*
* 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" });
setTimeout(() => promptToUpload([file], ctx.channel, DRAFT_TYPE), 10);
}
}]
});

View File

@ -1,165 +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 { showNotification } from "@api/Notifications";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { closeAllModals } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { maybePromptToUpdate } from "@utils/updater";
import { FluxDispatcher, NavigationRouter } from "@webpack/common";
import type { ReactElement } from "react";
const CrashHandlerLogger = new Logger("CrashHandler");
const settings = definePluginSettings({
attemptToPreventCrashes: {
type: OptionType.BOOLEAN,
description: "Whether to attempt to prevent Discord crashes.",
default: true
},
attemptToNavigateToHome: {
type: OptionType.BOOLEAN,
description: "Whether to attempt to navigate to the home when preventing Discord crashes.",
default: false
}
});
let crashCount: number = 0;
let lastCrashTimestamp: number = 0;
let shouldAttemptNextHandle = false;
export default definePlugin({
name: "CrashHandler",
description: "Utility plugin for handling and possibly recovering from Crashes without a restart",
authors: [Devs.Nuckyz],
enabledByDefault: true,
popAllModals: undefined as (() => void) | undefined,
settings,
patches: [
{
find: ".Messages.ERRORS_UNEXPECTED_CRASH",
replacement: {
match: /(?=this\.setState\()/,
replace: "$self.handleCrash(this)||"
}
},
{
find: 'dispatch({type:"MODAL_POP_ALL"})',
replacement: {
match: /"MODAL_POP_ALL".+?};(?<=(\i)=function.+?)/,
replace: (m, popAll) => `${m}$self.popAllModals=${popAll};`
}
}
],
handleCrash(_this: ReactElement & { forceUpdate: () => void; }) {
if (Date.now() - lastCrashTimestamp <= 1_000 && !shouldAttemptNextHandle) return true;
shouldAttemptNextHandle = false;
if (++crashCount > 5) {
try {
showNotification({
color: "#eed202",
title: "Discord has crashed!",
body: "Awn :( Discord has crashed more than five times, not attempting to recover.",
noPersist: true,
});
} catch { }
lastCrashTimestamp = Date.now();
return false;
}
setTimeout(() => crashCount--, 60_000);
try {
if (crashCount === 1) maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true);
if (settings.store.attemptToPreventCrashes) {
this.handlePreventCrash(_this);
return true;
}
return false;
} catch (err) {
CrashHandlerLogger.error("Failed to handle crash", err);
return false;
} finally {
lastCrashTimestamp = Date.now();
}
},
handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) {
if (Date.now() - lastCrashTimestamp >= 1_000) {
try {
showNotification({
color: "#eed202",
title: "Discord has crashed!",
body: "Attempting to recover...",
noPersist: true,
});
} catch { }
}
try {
FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" });
} catch (err) {
CrashHandlerLogger.debug("Failed to close open context menu.", err);
}
try {
this.popAllModals?.();
} catch (err) {
CrashHandlerLogger.debug("Failed to close old modals.", err);
}
try {
closeAllModals();
} catch (err) {
CrashHandlerLogger.debug("Failed to close all open modals.", err);
}
try {
FluxDispatcher.dispatch({ type: "USER_PROFILE_MODAL_CLOSE" });
} catch (err) {
CrashHandlerLogger.debug("Failed to close user popout.", err);
}
try {
FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
} catch (err) {
CrashHandlerLogger.debug("Failed to pop all layers.", err);
}
if (settings.store.attemptToNavigateToHome) {
try {
NavigationRouter.transitionTo("/channels/@me");
} catch (err) {
CrashHandlerLogger.debug("Failed to navigate to home", err);
}
}
try {
shouldAttemptNextHandle = true;
_this.forceUpdate();
} catch (err) {
CrashHandlerLogger.debug("Failed to update crash handler component.", err);
}
}
});

View File

@ -16,11 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { definePluginSettings } from "@api/Settings";
import { definePluginSettings } from "@api/settings";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards";
import { useAwaiter } from "@utils/react";
import { useAwaiter } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
import {
@ -57,11 +56,11 @@ interface ActivityAssets {
}
interface Activity {
state?: string;
state: string;
details?: string;
timestamps?: {
start?: number;
end?: number;
start?: Number;
end?: Number;
};
assets?: ActivityAssets;
buttons?: Array<string>;
@ -71,7 +70,7 @@ interface Activity {
button_urls?: Array<string>;
};
type: ActivityType;
flags: number;
flags: Number;
}
enum ActivityType {
@ -94,13 +93,13 @@ const numOpt = (description: string) => ({
onChange: setRpc
}) as const;
const choice = (label: string, value: any, _default?: boolean) => ({
const choice = (label: string, value: any, _default?: Boolean) => ({
label,
value,
default: _default
}) as const;
const choiceOpt = <T,>(description: string, options: T) => ({
const choiceOpt = (description: string, options) => ({
type: OptionType.SELECT,
description,
onChange: setRpc,
@ -174,13 +173,13 @@ async function createActivity(): Promise<Activity | undefined> {
activity.buttons = [
buttonOneText,
buttonTwoText
].filter(isTruthy);
].filter(Boolean);
activity.metadata = {
button_urls: [
buttonOneURL,
buttonTwoURL
].filter(isTruthy)
].filter(Boolean)
};
}
@ -207,16 +206,17 @@ async function createActivity(): Promise<Activity | undefined> {
delete activity[k];
}
// WHAT DO YOU WANT FROM ME
// eslint-disable-next-line consistent-return
return activity;
}
async function setRpc(disable?: boolean) {
async function setRpc(disable?: Boolean) {
const activity: Activity | undefined = await createActivity();
FluxDispatcher.dispatch({
type: "LOCAL_ACTIVITY_UPDATE",
activity: !disable ? activity : null,
socketId: "CustomRPC",
activity: !disable ? activity : {}
});
}

View File

@ -1,260 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { showNotification } from "@api/Notifications";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types";
import { filters, findAll, search } from "@webpack";
const PORT = 8485;
const NAV_ID = "dev-companion-reconnect";
const logger = new Logger("DevCompanion");
let socket: WebSocket | undefined;
type Node = StringNode | RegexNode | FunctionNode;
interface StringNode {
type: "string";
value: string;
}
interface RegexNode {
type: "regex";
value: {
pattern: string;
flags: string;
};
}
interface FunctionNode {
type: "function";
value: string;
}
interface PatchData {
find: string;
replacement: {
match: StringNode | RegexNode;
replace: StringNode | FunctionNode;
}[];
}
interface FindData {
type: string;
args: Array<StringNode | FunctionNode>;
}
const settings = definePluginSettings({
notifyOnAutoConnect: {
description: "Whether to notify when Dev Companion has automatically connected.",
type: OptionType.BOOLEAN,
default: true
}
});
function parseNode(node: Node) {
switch (node.type) {
case "string":
return node.value;
case "regex":
return new RegExp(node.value.pattern, node.value.flags);
case "function":
// We LOVE remote code execution
// Safety: This comes from localhost only, which actually means we have less permissions than the source,
// since we're running in the browser sandbox, whereas the sender has host access
return (0, eval)(node.value);
default:
throw new Error("Unknown Node Type " + (node as any).type);
}
}
function initWs(isManual = false) {
let wasConnected = isManual;
let hasErrored = false;
const ws = socket = new WebSocket(`ws://localhost:${PORT}`);
ws.addEventListener("open", () => {
wasConnected = true;
logger.info("Connected to WebSocket");
(settings.store.notifyOnAutoConnect || isManual) && showNotification({
title: "Dev Companion Connected",
body: "Connected to WebSocket",
noPersist: true
});
});
ws.addEventListener("error", e => {
if (!wasConnected) return;
hasErrored = true;
logger.error("Dev Companion Error:", e);
showNotification({
title: "Dev Companion Error",
body: (e as ErrorEvent).message || "No Error Message",
color: "var(--status-danger, red)",
noPersist: true,
});
});
ws.addEventListener("close", e => {
if (!wasConnected || hasErrored) return;
logger.info("Dev Companion Disconnected:", e.code, e.reason);
showNotification({
title: "Dev Companion Disconnected",
body: e.reason || "No Reason provided",
color: "var(--status-danger, red)",
noPersist: true,
});
});
ws.addEventListener("message", e => {
try {
var { nonce, type, data } = JSON.parse(e.data);
} catch (err) {
logger.error("Invalid JSON:", err, "\n" + e.data);
return;
}
function reply(error?: string) {
const data = { nonce, ok: !error } as Record<string, unknown>;
if (error) data.error = error;
ws.send(JSON.stringify(data));
}
logger.info("Received Message:", type, "\n", data);
switch (type) {
case "testPatch": {
const { find, replacement } = data as PatchData;
const candidates = search(find);
const keys = Object.keys(candidates);
if (keys.length !== 1)
return reply("Expected exactly one 'find' matches, found " + keys.length);
const mod = candidates[keys[0]];
let src = String(mod.original ?? mod).replaceAll("\n", "");
if (src.startsWith("function(")) {
src = "0," + src;
}
let i = 0;
for (const { match, replace } of replacement) {
i++;
try {
const matcher = canonicalizeMatch(parseNode(match));
const replacement = canonicalizeReplace(parseNode(replace), "PlaceHolderPluginName");
const newSource = src.replace(matcher, replacement as string);
if (src === newSource) throw "Had no effect";
Function(newSource);
src = newSource;
} catch (err) {
return reply(`Replacement ${i} failed: ${err}`);
}
}
reply();
break;
}
case "testFind": {
const { type, args } = data as FindData;
try {
var parsedArgs = args.map(parseNode);
} catch (err) {
return reply("Failed to parse args: " + err);
}
try {
let results: any[];
switch (type.replace("find", "").replace("Lazy", "")) {
case "":
results = findAll(parsedArgs[0]);
break;
case "ByProps":
results = findAll(filters.byProps(...parsedArgs));
break;
case "Store":
results = findAll(filters.byStoreName(parsedArgs[0]));
break;
case "ByCode":
results = findAll(filters.byCode(...parsedArgs));
break;
case "ModuleId":
results = Object.keys(search(parsedArgs[0]));
break;
default:
return reply("Unknown Find Type " + type);
}
const uniqueResultsCount = new Set(results).size;
if (uniqueResultsCount === 0) throw "No results";
if (uniqueResultsCount > 1) throw "Found more than one result! Make this filter more specific";
} catch (err) {
return reply("Failed to find: " + err);
}
reply();
break;
}
default:
reply("Unknown Type " + type);
break;
}
});
}
export default definePlugin({
name: "DevCompanion",
description: "Dev Companion Plugin",
authors: [Devs.Ven],
settings,
toolboxActions: {
"Reconnect"() {
socket?.close(1000, "Reconnecting");
initWs(true);
}
},
start() {
initWs();
},
stop() {
socket?.close(1000, "Plugin Stopped");
socket = void 0;
}
});

View File

@ -23,7 +23,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "UrbanDictionary",
description: "Search for a word on Urban Dictionary via /urban slash command",
description: "Searches for a word on Urban Dictionary",
authors: [Devs.jewdev],
dependencies: ["CommandsAPI"],
commands: [

Some files were not shown because too many files have changed in this diff Show More