mirror of
https://github.com/Vendicated/Vencord.git
synced 2024-09-20 06:30:35 +00:00
Merge branch 'refs/heads/dev' into plugin/memberListActivities
This commit is contained in:
commit
9475478fbe
251 changed files with 6665 additions and 2566 deletions
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"ignorePatterns": ["dist", "browser"],
|
"ignorePatterns": ["dist", "browser", "packages/vencord-types"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@typescript-eslint",
|
"@typescript-eslint",
|
||||||
"simple-header",
|
"simple-header",
|
||||||
|
|
3
.github/ISSUE_TEMPLATE/blank.yml
vendored
3
.github/ISSUE_TEMPLATE/blank.yml
vendored
|
@ -12,7 +12,8 @@ body:
|
||||||
DO NOT USE THIS FORM, unless
|
DO NOT USE THIS FORM, unless
|
||||||
- you are a vencord contributor
|
- you are a vencord contributor
|
||||||
- you were given explicit permission to use this form by a moderator in our support server
|
- you were given explicit permission to use this form by a moderator in our support server
|
||||||
- you are filing a security related report
|
|
||||||
|
DO NOT USE THIS FORM FOR SECURITY RELATED ISSUES. [CREATE A SECURITY ADVISORY INSTEAD.](https://github.com/Vendicated/Vencord/security/advisories/new)
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: content
|
id: content
|
||||||
|
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
@ -32,7 +32,7 @@ jobs:
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build web
|
- name: Build web
|
||||||
run: pnpm buildWeb --standalone
|
run: pnpm buildWebStandalone
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build --standalone
|
run: pnpm build --standalone
|
||||||
|
|
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
|
@ -32,7 +32,7 @@ jobs:
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build web
|
- name: Build web
|
||||||
run: pnpm buildWeb --standalone
|
run: pnpm buildWebStandalone
|
||||||
|
|
||||||
- name: Publish extension
|
- name: Publish extension
|
||||||
run: |
|
run: |
|
||||||
|
|
4
.github/workflows/reportBrokenPlugins.yml
vendored
4
.github/workflows/reportBrokenPlugins.yml
vendored
|
@ -37,8 +37,8 @@ jobs:
|
||||||
with:
|
with:
|
||||||
chrome-version: stable
|
chrome-version: stable
|
||||||
|
|
||||||
- name: Build web
|
- name: Build Vencord Reporter Version
|
||||||
run: pnpm buildWeb --standalone --dev
|
run: pnpm buildReporter
|
||||||
|
|
||||||
- name: Create Report
|
- name: Create Report
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
1
.npmrc
1
.npmrc
|
@ -1 +1,2 @@
|
||||||
strict-peer-dependencies=false
|
strict-peer-dependencies=false
|
||||||
|
package-manager-strict=false
|
||||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -14,6 +14,8 @@
|
||||||
"typescript.preferences.quoteStyle": "double",
|
"typescript.preferences.quoteStyle": "double",
|
||||||
"javascript.preferences.quoteStyle": "double",
|
"javascript.preferences.quoteStyle": "double",
|
||||||
|
|
||||||
|
"eslint.experimental.useFlatConfig": false,
|
||||||
|
|
||||||
"gitlens.remotes": [
|
"gitlens.remotes": [
|
||||||
{
|
{
|
||||||
"domain": "codeberg.org",
|
"domain": "codeberg.org",
|
||||||
|
|
|
@ -16,5 +16,6 @@ DON'T
|
||||||
|
|
||||||
Repetitive violations of these guidelines might get your access to the repository restricted.
|
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 vigilantism
|
||||||
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!
|
and instead report the issue to a moderator! The best way is joining our [official Discord community](https://vencord.dev/discord)
|
||||||
|
and opening a modmail ticket.
|
||||||
|
|
|
@ -1,82 +1,55 @@
|
||||||
# Contribution Guide
|
# Contributing to Vencord
|
||||||
|
|
||||||
First of all, thank you for contributing! :3
|
Vencord is a community project and welcomes any kind of contribution from anyone!
|
||||||
|
|
||||||
To ensure your contribution is robust, please follow the below guide!
|
We have development documentation for new contributors, which can be found at <https://docs.vencord.dev>.
|
||||||
|
|
||||||
For a friendly introduction to plugins, see [Megu's Plugin Guide!](docs/2_PLUGINS.md)
|
All contributions should be made in accordance with our [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
## Style Guide
|
## How to contribute
|
||||||
|
|
||||||
- This project has a very minimal .editorconfig. Make sure your editor supports this!
|
Contributions can be sent via pull requests. If you're new to Git, check [this guide](https://opensource.com/article/19/7/create-pull-request-github).
|
||||||
If you are using VSCode, it should automatically recommend you the extension; If not,
|
|
||||||
please install the Editorconfig extension
|
|
||||||
- Try to follow the formatting in the rest of the project and stay consistent
|
|
||||||
- Follow the file naming convention. File names should usually be camelCase, unless they export a Class
|
|
||||||
or React Component, in which case they should be PascalCase
|
|
||||||
|
|
||||||
## Contributing a Plugin
|
Pull requests can be made either to the `main` or the `dev` branch. However, unless you're an advanced user, I recommend sticking to `main`. This is because the dev branch might contain unstable changes and be force pushed frequently, which could cause conflicts in your pull request.
|
||||||
|
|
||||||
Because plugins modify code directly, incompatibilities are a problem.
|
## Write a plugin
|
||||||
|
|
||||||
Thus, 3rd party plugins are not supported, instead all plugins are part of Vencord itself.
|
Writing a plugin is the primary way to contribute.
|
||||||
This way we can ensure compatibility and high quality patches.
|
|
||||||
|
|
||||||
Follow the below guide to make your first plugin!
|
Before starting your plugin:
|
||||||
|
- Check existing pull requests to see if someone is already working on a similar plugin
|
||||||
|
- Check our [plugin requests tracker](https://github.com/Vencord/plugin-requests/issues) to see if there is an existing request, or if the same idea has been rejected
|
||||||
|
- If there isn't an existing request, [open one](https://github.com/Vencord/plugin-requests/issues/new?assignees=&labels=&projects=&template=request.yml) yourself
|
||||||
|
and include that you'd like to work on this yourself. Then wait for feedback to see if the idea even has any chance of being accepted. Or maybe others have some ideas to improve it!
|
||||||
|
- Familarise yourself with our plugin rules below to ensure your plugin is not banned
|
||||||
|
|
||||||
### Finding the right module to patch
|
### Plugin Rules
|
||||||
|
|
||||||
If the thing you want to patch is an action performed when interacting with a part of the UI, use React DevTools.
|
- No simple slash command plugins like `/cat`. Instead, make a [user installable Discord bot](https://discord.com/developers/docs/change-log#userinstallable-apps-preview)
|
||||||
They come preinstalled and can be found as the "Components" tab in DevTools.
|
- No simple text replace plugins like Let me Google that for you. The TextReplace plugin can do this
|
||||||
Use the Selector (top left) to select the UI Element. Now you can see all callbacks, props or jump to the source
|
- No raw DOM manipulation. Use proper patches and React
|
||||||
directly.
|
- No FakeDeafen or FakeMute
|
||||||
|
- No StereoMic
|
||||||
|
- No plugins that simply hide or redesign ui elements. This can be done with CSS
|
||||||
|
- No selfbots or API spam (animated status, message pruner, auto reply, nitro snipers, etc)
|
||||||
|
- No untrusted third party APIs. Popular services like Google or GitHub are fine, but absolutely no self hosted ones
|
||||||
|
- No plugins that require the user to enter their own API key
|
||||||
|
- Do not introduce new dependencies unless absolutely necessary and warranted
|
||||||
|
|
||||||
If it is anything else, or you're too lazy to use React DevTools, hit `CTRL + Shift + F` while in DevTools and
|
## Improve Vencord itself
|
||||||
enter a search term, for example "getUser" to search all source files.
|
|
||||||
Look at the results until you find something promising. Set a breakpoint and trigger the execution of that part of Code to inspect arguments, locals, etc...
|
|
||||||
|
|
||||||
### Writing a robust patch
|
If you have any ideas on how to improve Vencord itself, or want to propose a new plugin API, feel free to open a feature request so we can discuss.
|
||||||
|
|
||||||
##### "find"
|
Or if you notice any bugs or typos, feel free to fix them!
|
||||||
|
|
||||||
First you need to find a good `find` value. This should be a string that is unique to your module.
|
## Contribute to our Documentation
|
||||||
If you want to patch the `getUser` function, usually a good first try is `getUser:` or `function getUser()`,
|
|
||||||
depending on how the module is structured. Again, make sure this string is unique to your module and is not
|
|
||||||
found in any other module. To verify this, search for it in all bundles (CTRL + Shift + F)
|
|
||||||
|
|
||||||
##### "match"
|
The source code of our documentation is available at <https://github.com/Vencord/Docs>
|
||||||
|
|
||||||
This is the regex that will operate on the module found with "find". Just like in find, you should make sure
|
If you see anything outdated, incorrect or lacking, please fix it!
|
||||||
this only matches exactly the part you want to patch and no other parts in the file.
|
If you think a new page should be added, feel free to suggest it via an issue and we can discuss.
|
||||||
|
|
||||||
The easiest way to write and test your regex is the following:
|
## Help out users in our Discord community
|
||||||
|
|
||||||
- Get the ID of the module you want to patch. To do this, go to it in the sources tab and scroll up until you
|
We have an open support channel in our [Discord community](https://vencord.dev/discord).
|
||||||
see something like `447887: (e,t,n)=>{` (Obviously the number will differ).
|
Helping out users there is always appreciated! The more, the merrier.
|
||||||
- Now paste the following into the console: `Vencord.Webpack.wreq.m[447887].toString()` (Changing the number to your ID)
|
|
||||||
- Now either test regexes on this string in the console or use a tool like https://regex101.com
|
|
||||||
|
|
||||||
Also pay attention to the following:
|
|
||||||
|
|
||||||
- Never hardcode variable or parameter names or any other minified names. They will change in the future. The only Exception to this rule
|
|
||||||
are the react props parameter which seems to always be `e`, but even then only rely on this if it is necessary.
|
|
||||||
Instead, use one of the following approaches where applicable:
|
|
||||||
- Match 1 or 2 of any character: `.{1,2}`, for example to match the variable name in `var a=b`, `var (.{1,2})=`
|
|
||||||
- Match any but a guaranteed terminating character: `[^;]+`, for example to match the entire assigned value in `var a=b||c||func();`,
|
|
||||||
`var .{1,2}=([^;]+);`
|
|
||||||
- If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters
|
|
||||||
- Additionally, as you might have noticed, all of the above approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
|
|
||||||
|
|
||||||
#### "replace"
|
|
||||||
|
|
||||||
This is the replacement for the match. This is the second argument to [String.replace](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace), so refer to those docs for info.
|
|
||||||
|
|
||||||
Never hardcode minified variable or parameter names here. Instead, use capture groups in your regex to capture the variable names
|
|
||||||
and use those in your replacement
|
|
||||||
|
|
||||||
Make sure your replacement does not introduce any whitespace. While this might seem weird, random whitespace may mess up other patches.
|
|
||||||
This includes spaces, tabs and especially newlines
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
And that's it! Now open a Pull Request with your Plugin
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
The cutest Discord client mod
|
The cutest Discord client mod
|
||||||
|
|
||||||
| ![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334) |
|
| ![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334) |
|
||||||
|:--:|
|
| :--------------------------------------------------------------------------------------------------: |
|
||||||
| A screenshot of vencord showcasing the [vencord-theme](https://github.com/synqat/vencord-theme) |
|
| A screenshot of vencord showcasing the [vencord-theme](https://github.com/synqat/vencord-theme) |
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
@ -33,7 +33,7 @@ https://discord.gg/D9uwnFnqmd
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
| **Thanks a lot to all Vencord [sponsors](https://github.com/sponsors/Vendicated)!!** |
|
| **Thanks a lot to all Vencord [sponsors](https://github.com/sponsors/Vendicated)!!** |
|
||||||
|:--:|
|
| :------------------------------------------------------------------------------------------: |
|
||||||
| [![](https://meow.vendicated.dev/sponsors.png)](https://github.com/sponsors/Vendicated) |
|
| [![](https://meow.vendicated.dev/sponsors.png)](https://github.com/sponsors/Vendicated) |
|
||||||
| *generated using [github-sponsor-graph](https://github.com/Vendicated/github-sponsor-graph)* |
|
| *generated using [github-sponsor-graph](https://github.com/Vendicated/github-sponsor-graph)* |
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,8 @@
|
||||||
/// <reference path="../src/modules.d.ts" />
|
/// <reference path="../src/modules.d.ts" />
|
||||||
/// <reference path="../src/globals.d.ts" />
|
/// <reference path="../src/globals.d.ts" />
|
||||||
|
|
||||||
import monacoHtmlLocal from "~fileContent/monacoWin.html";
|
import monacoHtmlLocal from "file://monacoWin.html?minify";
|
||||||
import monacoHtmlCdn from "~fileContent/../src/main/monacoWin.html";
|
import monacoHtmlCdn from "file://../src/main/monacoWin.html?minify";
|
||||||
import * as DataStore from "../src/api/DataStore";
|
import * as DataStore from "../src/api/DataStore";
|
||||||
import { debounce } from "../src/utils";
|
import { debounce } from "../src/utils";
|
||||||
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
|
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
|
||||||
|
|
|
@ -2,23 +2,22 @@ if (typeof browser === "undefined") {
|
||||||
var browser = chrome;
|
var browser = chrome;
|
||||||
}
|
}
|
||||||
|
|
||||||
const script = document.createElement("script");
|
|
||||||
script.src = browser.runtime.getURL("dist/Vencord.js");
|
|
||||||
script.id = "vencord-script";
|
|
||||||
Object.assign(script.dataset, {
|
|
||||||
extensionBaseUrl: browser.runtime.getURL(""),
|
|
||||||
version: browser.runtime.getManifest().version
|
|
||||||
});
|
|
||||||
|
|
||||||
const style = document.createElement("link");
|
const style = document.createElement("link");
|
||||||
style.type = "text/css";
|
style.type = "text/css";
|
||||||
style.rel = "stylesheet";
|
style.rel = "stylesheet";
|
||||||
style.href = browser.runtime.getURL("dist/Vencord.css");
|
style.href = browser.runtime.getURL("dist/Vencord.css");
|
||||||
|
|
||||||
document.documentElement.append(script);
|
|
||||||
|
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
"DOMContentLoaded",
|
"DOMContentLoaded",
|
||||||
() => document.documentElement.append(style),
|
() => {
|
||||||
|
document.documentElement.append(style);
|
||||||
|
window.postMessage({
|
||||||
|
type: "vencord:meta",
|
||||||
|
meta: {
|
||||||
|
EXTENSION_VERSION: browser.runtime.getManifest().version,
|
||||||
|
EXTENSION_BASE_URL: browser.runtime.getURL(""),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
{ once: true }
|
{ once: true }
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"minimum_chrome_version": "91",
|
"minimum_chrome_version": "111",
|
||||||
|
|
||||||
"name": "Vencord Web",
|
"name": "Vencord Web",
|
||||||
"description": "The cutest Discord mod now in your browser",
|
"description": "The cutest Discord mod now in your browser",
|
||||||
|
@ -22,7 +22,15 @@
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"matches": ["*://*.discord.com/*"],
|
"matches": ["*://*.discord.com/*"],
|
||||||
"js": ["content.js"],
|
"js": ["content.js"],
|
||||||
"all_frames": true
|
"all_frames": true,
|
||||||
|
"world": "ISOLATED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"run_at": "document_start",
|
||||||
|
"matches": ["*://*.discord.com/*"],
|
||||||
|
"js": ["dist/Vencord.js"],
|
||||||
|
"all_frames": true,
|
||||||
|
"world": "MAIN"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,15 @@
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"matches": ["*://*.discord.com/*"],
|
"matches": ["*://*.discord.com/*"],
|
||||||
"js": ["content.js"],
|
"js": ["content.js"],
|
||||||
"all_frames": true
|
"all_frames": true,
|
||||||
|
"world": "ISOLATED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"run_at": "document_start",
|
||||||
|
"matches": ["*://*.discord.com/*"],
|
||||||
|
"js": ["dist/Vencord.js"],
|
||||||
|
"all_frames": true,
|
||||||
|
"world": "MAIN"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -35,7 +43,7 @@
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
"id": "vencord-firefox@vendicated.dev",
|
"id": "vencord-firefox@vendicated.dev",
|
||||||
"strict_min_version": "91.0"
|
"strict_min_version": "128.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
> [!WARNING]
|
|
||||||
> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead.
|
|
||||||
> No support will be provided for installing in this fashion. If you cannot figure it out, you should just stick to a regular install.
|
|
||||||
|
|
||||||
# Installation Guide
|
|
||||||
|
|
||||||
Welcome to Megu's Installation Guide! In this file, you will learn about how to download, install, and uninstall Vencord!
|
|
||||||
|
|
||||||
## Sections
|
|
||||||
|
|
||||||
- [Installation Guide](#installation-guide)
|
|
||||||
- [Sections](#sections)
|
|
||||||
- [Dependencies](#dependencies)
|
|
||||||
- [Installing Vencord](#installing-vencord)
|
|
||||||
- [Updating Vencord](#updating-vencord)
|
|
||||||
- [Uninstalling Vencord](#uninstalling-vencord)
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- Install Git from https://git-scm.com/download
|
|
||||||
- Install Node.JS LTS from here: https://nodejs.dev/en/
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
```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
|
|
||||||
git clone https://github.com/Vendicated/Vencord
|
|
||||||
cd Vencord
|
|
||||||
```
|
|
||||||
|
|
||||||
Install dependencies:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
```
|
|
||||||
|
|
||||||
Build Vencord:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pnpm build
|
|
||||||
```
|
|
||||||
|
|
||||||
Inject vencord into your client:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pnpm inject
|
|
||||||
```
|
|
||||||
|
|
||||||
Then fully close Discord from your taskbar or task manager, and restart it. Vencord should be injected - you can check this by looking for the Vencord section in Discord settings.
|
|
||||||
|
|
||||||
## Updating Vencord
|
|
||||||
|
|
||||||
If you're using Discord already, go into the `Updater` tab in settings.
|
|
||||||
|
|
||||||
Sometimes it may be necessary to manually update if the GUI updater fails.
|
|
||||||
|
|
||||||
To pull latest changes:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git pull
|
|
||||||
```
|
|
||||||
|
|
||||||
If this fails, you likely need to reset your local changes to vencord to resolve merge errors:
|
|
||||||
|
|
||||||
> :exclamation: This command will remove any local changes you've made to vencord. Make sure you back up if you made any code changes you don't want to lose!
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git reset --hard
|
|
||||||
git pull
|
|
||||||
```
|
|
||||||
|
|
||||||
and then to build the changes:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pnpm build
|
|
||||||
```
|
|
||||||
|
|
||||||
Then just refresh your client
|
|
||||||
|
|
||||||
## Uninstalling Vencord
|
|
||||||
|
|
||||||
Simply run:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pnpm uninject
|
|
||||||
```
|
|
|
@ -1,111 +0,0 @@
|
||||||
# Plugins Guide
|
|
||||||
|
|
||||||
Welcome to Megu's Plugin Guide! In this file, you will learn about how to write your own plugin!
|
|
||||||
|
|
||||||
You don't need to run `pnpm build` every time you make a change. Instead, use `pnpm watch` - this will auto-compile Vencord whenever you make a change. If using code patches (recommended), you will need to CTRL+R to load the changes.
|
|
||||||
|
|
||||||
## Plugin Entrypoint
|
|
||||||
|
|
||||||
> If it doesn't already exist, create a folder called `userplugins` in the `src` directory of this repo.
|
|
||||||
|
|
||||||
1. Create a folder in `src/userplugins/` with the name of your plugin. For example, `src/userplugins/epicPlugin/` - All of your plugin files will go here.
|
|
||||||
|
|
||||||
2. Create a file in that folder called `index.ts`
|
|
||||||
|
|
||||||
3. In `index.ts`, copy-paste the following template code:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "Epic Plugin",
|
|
||||||
description: "This plugin is absolutely epic",
|
|
||||||
authors: [
|
|
||||||
{
|
|
||||||
id: 12345n,
|
|
||||||
name: "Your Name",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
patches: [],
|
|
||||||
// Delete these two below if you are only using code patches
|
|
||||||
start() {},
|
|
||||||
stop() {},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Change the name, description, and authors to your own information.
|
|
||||||
|
|
||||||
Replace `12345n` with your user ID ending in `n` (e.g., `545581357812678656n`). If you don't want to share your Discord account, use `0n` instead!
|
|
||||||
|
|
||||||
## How Plugins Work In Vencord
|
|
||||||
|
|
||||||
Vencord uses a different way of making mods than you're used to.
|
|
||||||
Instead of monkeypatching webpack, we directly modify the code before Discord loads it.
|
|
||||||
|
|
||||||
This is _significantly_ more efficient than monkeypatching webpack, and is surprisingly easy, but it may be confusing at first.
|
|
||||||
|
|
||||||
## Making your patch
|
|
||||||
|
|
||||||
For an in-depth guide into patching code, see [CONTRIBUTING.md](../CONTRIBUTING.md)
|
|
||||||
|
|
||||||
in the `index.ts` file we made earlier, you'll see a `patches` array.
|
|
||||||
|
|
||||||
> You'll see examples of how patches are used in all the existing plugins, and it'll be easier to understand by looking at those examples, so do that first, and then return here!
|
|
||||||
|
|
||||||
> For a good example of a plugin using code patches AND runtime patching, check `src/plugins/unindent.ts`, which uses code patches to run custom runtime code.
|
|
||||||
|
|
||||||
One of the patches in the `isStaff` plugin, looks like this:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
{
|
|
||||||
match: /(\w+)\.isStaff=function\(\){return\s*!1};/,
|
|
||||||
replace: "$1.isStaff=function(){return true};",
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
The above regex matches the string in discord that will look something like:
|
|
||||||
|
|
||||||
```js
|
|
||||||
abc.isStaff = function () {
|
|
||||||
return !1;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Remember that Discord code is minified, so there won't be any newlines, and there will only be spaces where necessary. So the source code looks something like:
|
|
||||||
|
|
||||||
```
|
|
||||||
abc.isStaff=function(){return!1;}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can find these snippets by opening the devtools (`ctrl+shift+i`) and pressing `ctrl+shift+f`, searching for what you're looking to modify in there, and beautifying the file to make it more readable.
|
|
||||||
|
|
||||||
In the `match` regex in the example shown above, you'll notice at the start there is a `(\w+)`.
|
|
||||||
Anything in the brackets will be accessible in the `replace` string using `$<number>`. e.g., the first pair of brackets will be `$1`, the second will be `$2`, etc.
|
|
||||||
|
|
||||||
The replacement string we used is:
|
|
||||||
|
|
||||||
```
|
|
||||||
"$1.isStaff=function(){return true;};"
|
|
||||||
```
|
|
||||||
|
|
||||||
Which, using the above example, would replace the code with:
|
|
||||||
|
|
||||||
> **Note**
|
|
||||||
> In this example, `$1` becomes `abc`
|
|
||||||
|
|
||||||
```js
|
|
||||||
abc.isStaff = function () {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
The match value _can_ be a string, rather than regex, however usually regex will be better suited, as it can work with unknown values, whereas strings must be exact matches.
|
|
||||||
|
|
||||||
Once you've made your plugin, make sure you run `pnpm test` and make sure your code is nice and clean!
|
|
||||||
|
|
||||||
If you want to publish your plugin into the Vencord repo, move your plugin from `src/userplugins` into the `src/plugins` folder and open a PR!
|
|
||||||
|
|
||||||
> **Warning**
|
|
||||||
> Make sure you've read [CONTRIBUTING.md](../CONTRIBUTING.md) before opening a PR
|
|
||||||
|
|
||||||
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
|
31
package.json
31
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.8.4",
|
"version": "1.9.3",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "The cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
@ -13,22 +13,25 @@
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"author": "Vendicated",
|
"author": "Vendicated",
|
||||||
"directories": {
|
|
||||||
"doc": "docs"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
|
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
|
||||||
|
"buildStandalone": "pnpm build --standalone",
|
||||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||||
|
"buildWebStandalone": "pnpm buildWeb --standalone",
|
||||||
|
"buildReporter": "pnpm buildWebStandalone --reporter --skip-extension",
|
||||||
|
"buildReporterDesktop": "pnpm build --reporter",
|
||||||
|
"watch": "pnpm build --watch",
|
||||||
|
"watchWeb": "pnpm buildWeb --watch",
|
||||||
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
||||||
|
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
|
||||||
"inject": "node scripts/runInstaller.mjs",
|
"inject": "node scripts/runInstaller.mjs",
|
||||||
|
"uninject": "node scripts/runInstaller.mjs",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
|
||||||
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
||||||
"lint:fix": "pnpm lint --fix",
|
"lint:fix": "pnpm lint --fix",
|
||||||
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
|
"test": "pnpm buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
|
||||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
||||||
"testTsc": "tsc --noEmit",
|
"testTsc": "tsc --noEmit"
|
||||||
"uninject": "node scripts/runInstaller.mjs",
|
|
||||||
"watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
||||||
|
@ -37,7 +40,7 @@
|
||||||
"eslint-plugin-simple-header": "^1.0.2",
|
"eslint-plugin-simple-header": "^1.0.2",
|
||||||
"fflate": "^0.7.4",
|
"fflate": "^0.7.4",
|
||||||
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
|
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
|
||||||
"monaco-editor": "^0.43.0",
|
"monaco-editor": "^0.50.0",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"virtual-merge": "^1.0.1"
|
"virtual-merge": "^1.0.1"
|
||||||
},
|
},
|
||||||
|
@ -60,16 +63,18 @@
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"highlight.js": "10.6.0",
|
"highlight.js": "10.6.0",
|
||||||
|
"html-minifier-terser": "^7.2.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"puppeteer-core": "^19.11.1",
|
"puppeteer-core": "^19.11.1",
|
||||||
"standalone-electron-types": "^1.0.0",
|
"standalone-electron-types": "^1.0.0",
|
||||||
"stylelint": "^15.6.0",
|
"stylelint": "^15.6.0",
|
||||||
"stylelint-config-standard": "^33.0.0",
|
"stylelint-config-standard": "^33.0.0",
|
||||||
|
"ts-patch": "^3.1.2",
|
||||||
"tsx": "^3.12.7",
|
"tsx": "^3.12.7",
|
||||||
"type-fest": "^3.9.0",
|
"type-fest": "^3.9.0",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.4.5",
|
||||||
"zip-local": "^0.3.5",
|
"typescript-transform-paths": "^3.4.7",
|
||||||
"zustand": "^3.7.2"
|
"zip-local": "^0.3.5"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.1.0",
|
"packageManager": "pnpm@9.1.0",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
@ -99,6 +104,6 @@
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18",
|
"node": ">=18",
|
||||||
"pnpm": ">=8"
|
"pnpm": ">=9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
7
packages/vencord-types/.gitignore
vendored
Normal file
7
packages/vencord-types/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
*
|
||||||
|
!.*ignore
|
||||||
|
!package.json
|
||||||
|
!*.md
|
||||||
|
!prepare.ts
|
||||||
|
!index.d.ts
|
||||||
|
!globals.d.ts
|
4
packages/vencord-types/.npmignore
Normal file
4
packages/vencord-types/.npmignore
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules
|
||||||
|
prepare.ts
|
||||||
|
.gitignore
|
||||||
|
HOW2PUB.md
|
5
packages/vencord-types/HOW2PUB.md
Normal file
5
packages/vencord-types/HOW2PUB.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# How to publish
|
||||||
|
|
||||||
|
1. run `pnpm generateTypes` in the project root
|
||||||
|
2. bump package.json version
|
||||||
|
3. npm publish
|
11
packages/vencord-types/README.md
Normal file
11
packages/vencord-types/README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Vencord Types
|
||||||
|
|
||||||
|
Typings for Vencord's api, published to npm
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i @vencord/types
|
||||||
|
|
||||||
|
yarn add @vencord/types
|
||||||
|
|
||||||
|
pnpm add @vencord/types
|
||||||
|
```
|
24
packages/vencord-types/globals.d.ts
vendored
Normal file
24
packages/vencord-types/globals.d.ts
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2022 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
export var VencordNative: typeof import("./VencordNative").default;
|
||||||
|
export var Vencord: typeof import("./Vencord");
|
||||||
|
}
|
||||||
|
|
||||||
|
export { };
|
5
packages/vencord-types/index.d.ts
vendored
Normal file
5
packages/vencord-types/index.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
/// <reference path="Vencord.d.ts" />
|
||||||
|
/// <reference path="globals.d.ts" />
|
||||||
|
/// <reference path="modules.d.ts" />
|
28
packages/vencord-types/package.json
Normal file
28
packages/vencord-types/package.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "@vencord/types",
|
||||||
|
"private": false,
|
||||||
|
"version": "0.1.3",
|
||||||
|
"description": "",
|
||||||
|
"types": "index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"prepublishOnly": "tsx ./prepare.ts",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Vencord",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/fs-extra": "^11.0.4",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
|
"tsx": "^3.12.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/lodash": "^4.14.191",
|
||||||
|
"@types/node": "^18.11.18",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.0.10",
|
||||||
|
"discord-types": "^1.3.26",
|
||||||
|
"standalone-electron-types": "^1.0.0",
|
||||||
|
"type-fest": "^3.5.3"
|
||||||
|
}
|
||||||
|
}
|
47
packages/vencord-types/prepare.ts
Normal file
47
packages/vencord-types/prepare.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a modification for Discord's desktop app
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { cpSync, moveSync, readdirSync, rmSync } from "fs-extra";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
readdirSync(join(__dirname, "src"))
|
||||||
|
.forEach(child => moveSync(join(__dirname, "src", child), join(__dirname, child), { overwrite: true }));
|
||||||
|
|
||||||
|
const VencordSrc = join(__dirname, "..", "..", "src");
|
||||||
|
|
||||||
|
for (const file of ["preload.d.ts", "userplugins", "main", "debug", "src", "browser", "scripts"]) {
|
||||||
|
rmSync(join(__dirname, file), { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyDtsFiles(from: string, to: string) {
|
||||||
|
for (const file of readdirSync(from, { withFileTypes: true })) {
|
||||||
|
// bad
|
||||||
|
if (from === VencordSrc && file.name === "globals.d.ts") continue;
|
||||||
|
|
||||||
|
const fullFrom = join(from, file.name);
|
||||||
|
const fullTo = join(to, file.name);
|
||||||
|
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
copyDtsFiles(fullFrom, fullTo);
|
||||||
|
} else if (file.name.endsWith(".d.ts")) {
|
||||||
|
cpSync(fullFrom, fullTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyDtsFiles(VencordSrc, __dirname);
|
1297
pnpm-lock.yaml
1297
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
packages:
|
||||||
|
- packages/*
|
|
@ -21,19 +21,21 @@ import esbuild from "esbuild";
|
||||||
import { readdir } from "fs/promises";
|
import { readdir } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isDev, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
|
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, watch } from "./common.mjs";
|
||||||
|
|
||||||
const defines = {
|
const defines = {
|
||||||
IS_STANDALONE: isStandalone,
|
IS_STANDALONE,
|
||||||
IS_DEV: JSON.stringify(isDev),
|
IS_DEV,
|
||||||
IS_UPDATER_DISABLED: updaterDisabled,
|
IS_REPORTER,
|
||||||
|
IS_UPDATER_DISABLED,
|
||||||
IS_WEB: false,
|
IS_WEB: false,
|
||||||
IS_EXTENSION: false,
|
IS_EXTENSION: false,
|
||||||
VERSION: JSON.stringify(VERSION),
|
VERSION: JSON.stringify(VERSION),
|
||||||
BUILD_TIMESTAMP,
|
BUILD_TIMESTAMP
|
||||||
};
|
};
|
||||||
if (defines.IS_STANDALONE === "false")
|
|
||||||
// If this is a local build (not standalone), optimise
|
if (defines.IS_STANDALONE === false)
|
||||||
|
// If this is a local build (not standalone), optimize
|
||||||
// for the specific platform we're on
|
// for the specific platform we're on
|
||||||
defines["process.platform"] = JSON.stringify(process.platform);
|
defines["process.platform"] = JSON.stringify(process.platform);
|
||||||
|
|
||||||
|
@ -46,7 +48,7 @@ const nodeCommonOpts = {
|
||||||
platform: "node",
|
platform: "node",
|
||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
|
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
|
||||||
define: defines,
|
define: defines
|
||||||
};
|
};
|
||||||
|
|
||||||
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
|
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
|
||||||
|
@ -73,23 +75,21 @@ const globNativesPlugin = {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const dir of pluginDirs) {
|
for (const dir of pluginDirs) {
|
||||||
const dirPath = join("src", dir);
|
const dirPath = join("src", dir);
|
||||||
if (!await existsAsync(dirPath)) continue;
|
if (!await exists(dirPath)) continue;
|
||||||
const plugins = await readdir(dirPath);
|
const plugins = await readdir(dirPath, { withFileTypes: true });
|
||||||
for (const p of plugins) {
|
for (const file of plugins) {
|
||||||
const nativePath = join(dirPath, p, "native.ts");
|
const fileName = file.name;
|
||||||
const indexNativePath = join(dirPath, p, "native/index.ts");
|
const nativePath = join(dirPath, fileName, "native.ts");
|
||||||
|
const indexNativePath = join(dirPath, fileName, "native/index.ts");
|
||||||
|
|
||||||
if (!(await existsAsync(nativePath)) && !(await existsAsync(indexNativePath)))
|
if (!(await exists(nativePath)) && !(await exists(indexNativePath)))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const nameParts = p.split(".");
|
const pluginName = await resolvePluginName(dirPath, file);
|
||||||
const namePartsWithoutTarget = nameParts.length === 1 ? nameParts : nameParts.slice(0, -1);
|
|
||||||
// pluginName.thing.desktop -> PluginName.thing
|
|
||||||
const cleanPluginName = p[0].toUpperCase() + namePartsWithoutTarget.join(".").slice(1);
|
|
||||||
|
|
||||||
const mod = `p${i}`;
|
const mod = `p${i}`;
|
||||||
code += `import * as ${mod} from "./${dir}/${p}/native";\n`;
|
code += `import * as ${mod} from "./${dir}/${fileName}/native";\n`;
|
||||||
natives += `${JSON.stringify(cleanPluginName)}:${mod},\n`;
|
natives += `${JSON.stringify(pluginName)}:${mod},\n`;
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import Zip from "zip-local";
|
import Zip from "zip-local";
|
||||||
|
|
||||||
import { BUILD_TIMESTAMP, commonOpts, globPlugins, isDev, VERSION } from "./common.mjs";
|
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION } from "./common.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.BuildOptions}
|
* @type {esbuild.BuildOptions}
|
||||||
|
@ -33,22 +33,23 @@ const commonOptions = {
|
||||||
entryPoints: ["browser/Vencord.ts"],
|
entryPoints: ["browser/Vencord.ts"],
|
||||||
globalName: "Vencord",
|
globalName: "Vencord",
|
||||||
format: "iife",
|
format: "iife",
|
||||||
external: ["plugins", "git-hash", "/assets/*"],
|
external: ["~plugins", "~git-hash", "/assets/*"],
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins("web"),
|
globPlugins("web"),
|
||||||
...commonOpts.plugins,
|
...commonOpts.plugins,
|
||||||
],
|
],
|
||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
define: {
|
define: {
|
||||||
IS_WEB: "true",
|
IS_WEB: true,
|
||||||
IS_EXTENSION: "false",
|
IS_EXTENSION: false,
|
||||||
IS_STANDALONE: "true",
|
IS_STANDALONE: true,
|
||||||
IS_DEV: JSON.stringify(isDev),
|
IS_DEV,
|
||||||
IS_DISCORD_DESKTOP: "false",
|
IS_REPORTER,
|
||||||
IS_VESKTOP: "false",
|
IS_DISCORD_DESKTOP: false,
|
||||||
IS_UPDATER_DISABLED: "true",
|
IS_VESKTOP: false,
|
||||||
|
IS_UPDATER_DISABLED: true,
|
||||||
VERSION: JSON.stringify(VERSION),
|
VERSION: JSON.stringify(VERSION),
|
||||||
BUILD_TIMESTAMP,
|
BUILD_TIMESTAMP
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -87,16 +88,16 @@ await Promise.all(
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
outfile: "dist/browser.js",
|
outfile: "dist/browser.js",
|
||||||
footer: { js: "//# sourceURL=VencordWeb" },
|
footer: { js: "//# sourceURL=VencordWeb" }
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
outfile: "dist/extension.js",
|
outfile: "dist/extension.js",
|
||||||
define: {
|
define: {
|
||||||
...commonOptions?.define,
|
...commonOptions?.define,
|
||||||
IS_EXTENSION: "true",
|
IS_EXTENSION: true,
|
||||||
},
|
},
|
||||||
footer: { js: "//# sourceURL=VencordWeb" },
|
footer: { js: "//# sourceURL=VencordWeb" }
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
|
@ -112,7 +113,7 @@ await Promise.all(
|
||||||
footer: {
|
footer: {
|
||||||
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
|
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
|
||||||
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
|
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -165,7 +166,7 @@ async function buildExtension(target, files) {
|
||||||
f.startsWith("manifest") ? "manifest.json" : f,
|
f.startsWith("manifest") ? "manifest.json" : f,
|
||||||
content
|
content
|
||||||
];
|
];
|
||||||
}))),
|
})))
|
||||||
};
|
};
|
||||||
|
|
||||||
await rm(target, { recursive: true, force: true });
|
await rm(target, { recursive: true, force: true });
|
||||||
|
@ -192,6 +193,7 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
|
||||||
return appendFile("dist/Vencord.user.js", cssRuntime);
|
return appendFile("dist/Vencord.user.js", cssRuntime);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!process.argv.includes("--skip-extension")) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
appendCssRuntime,
|
appendCssRuntime,
|
||||||
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
|
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
|
||||||
|
@ -203,3 +205,7 @@ console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
|
||||||
|
|
||||||
Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
|
Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
|
||||||
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
|
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
await appendCssRuntime;
|
||||||
|
}
|
||||||
|
|
|
@ -20,8 +20,10 @@ import "../suppressExperimentalWarnings.js";
|
||||||
import "../checkNodeVersion.js";
|
import "../checkNodeVersion.js";
|
||||||
|
|
||||||
import { exec, execSync } from "child_process";
|
import { exec, execSync } from "child_process";
|
||||||
|
import esbuild from "esbuild";
|
||||||
import { constants as FsConstants, readFileSync } from "fs";
|
import { constants as FsConstants, readFileSync } from "fs";
|
||||||
import { access, readdir, readFile } from "fs/promises";
|
import { access, readdir, readFile } from "fs/promises";
|
||||||
|
import { minify as minifyHtml } from "html-minifier-terser";
|
||||||
import { join, relative } from "path";
|
import { join, relative } from "path";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
@ -33,24 +35,52 @@ const PackageJSON = JSON.parse(readFileSync("package.json"));
|
||||||
export const VERSION = PackageJSON.version;
|
export const VERSION = PackageJSON.version;
|
||||||
// https://reproducible-builds.org/docs/source-date-epoch/
|
// https://reproducible-builds.org/docs/source-date-epoch/
|
||||||
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
|
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
|
||||||
|
|
||||||
export const watch = process.argv.includes("--watch");
|
export const watch = process.argv.includes("--watch");
|
||||||
export const isDev = watch || process.argv.includes("--dev");
|
export const IS_DEV = watch || process.argv.includes("--dev");
|
||||||
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
export const IS_REPORTER = process.argv.includes("--reporter");
|
||||||
export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater"));
|
export const IS_STANDALONE = process.argv.includes("--standalone");
|
||||||
|
|
||||||
|
export const IS_UPDATER_DISABLED = process.argv.includes("--disable-updater");
|
||||||
export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
||||||
|
|
||||||
export const banner = {
|
export const banner = {
|
||||||
js: `
|
js: `
|
||||||
// Vencord ${gitHash}
|
// Vencord ${gitHash}
|
||||||
// Standalone: ${isStandalone}
|
// Standalone: ${IS_STANDALONE}
|
||||||
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
|
// Platform: ${IS_STANDALONE === false ? process.platform : "Universal"}
|
||||||
// Updater disabled: ${updaterDisabled}
|
// Updater Disabled: ${IS_UPDATER_DISABLED}
|
||||||
`.trim()
|
`.trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
|
const PluginDefinitionNameMatcher = /definePlugin\(\{\s*(["'])?name\1:\s*(["'`])(.+?)\2/;
|
||||||
|
/**
|
||||||
|
* @param {string} base
|
||||||
|
* @param {import("fs").Dirent} dirent
|
||||||
|
*/
|
||||||
|
export async function resolvePluginName(base, dirent) {
|
||||||
|
const fullPath = join(base, dirent.name);
|
||||||
|
const content = dirent.isFile()
|
||||||
|
? await readFile(fullPath, "utf-8")
|
||||||
|
: await (async () => {
|
||||||
|
for (const file of ["index.ts", "index.tsx"]) {
|
||||||
|
try {
|
||||||
|
return await readFile(join(fullPath, file), "utf-8");
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Invalid plugin ${fullPath}: could not resolve entry point`);
|
||||||
|
})();
|
||||||
|
|
||||||
export function existsAsync(path) {
|
return PluginDefinitionNameMatcher.exec(content)?.[3]
|
||||||
return access(path, FsConstants.F_OK)
|
?? (() => {
|
||||||
|
throw new Error(`Invalid plugin ${fullPath}: must contain definePlugin call with simple string name property as first property`);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exists(path) {
|
||||||
|
return await access(path, FsConstants.F_OK)
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
}
|
}
|
||||||
|
@ -64,7 +94,7 @@ export const makeAllPackagesExternalPlugin = {
|
||||||
setup(build) {
|
setup(build) {
|
||||||
const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
|
const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
|
||||||
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
|
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,31 +114,48 @@ export const globPlugins = kind => ({
|
||||||
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
|
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
|
||||||
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins"];
|
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins"];
|
||||||
let code = "";
|
let code = "";
|
||||||
let plugins = "\n";
|
let pluginsCode = "\n";
|
||||||
|
let metaCode = "\n";
|
||||||
|
let excludedCode = "\n";
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const dir of pluginDirs) {
|
for (const dir of pluginDirs) {
|
||||||
if (!await existsAsync(`./src/${dir}`)) continue;
|
const userPlugin = dir === "userplugins";
|
||||||
const files = await readdir(`./src/${dir}`);
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.startsWith("_") || file.startsWith(".")) continue;
|
|
||||||
if (file === "index.ts") continue;
|
|
||||||
|
|
||||||
const target = getPluginTarget(file);
|
const fullDir = `./src/${dir}`;
|
||||||
if (target) {
|
if (!await exists(fullDir)) continue;
|
||||||
if (target === "dev" && !watch) continue;
|
const files = await readdir(fullDir, { withFileTypes: true });
|
||||||
if (target === "web" && kind === "discordDesktop") continue;
|
for (const file of files) {
|
||||||
if (target === "desktop" && kind === "web") continue;
|
const fileName = file.name;
|
||||||
if (target === "discordDesktop" && kind !== "discordDesktop") continue;
|
if (fileName.startsWith("_") || fileName.startsWith(".")) continue;
|
||||||
if (target === "vencordDesktop" && kind !== "vencordDesktop") continue;
|
if (fileName === "index.ts") continue;
|
||||||
|
|
||||||
|
const target = getPluginTarget(fileName);
|
||||||
|
|
||||||
|
if (target && !IS_REPORTER) {
|
||||||
|
const excluded =
|
||||||
|
(target === "dev" && !IS_DEV) ||
|
||||||
|
(target === "web" && kind === "discordDesktop") ||
|
||||||
|
(target === "desktop" && kind === "web") ||
|
||||||
|
(target === "discordDesktop" && kind !== "discordDesktop") ||
|
||||||
|
(target === "vencordDesktop" && kind !== "vencordDesktop");
|
||||||
|
|
||||||
|
if (excluded) {
|
||||||
|
const name = await resolvePluginName(fullDir, file);
|
||||||
|
excludedCode += `${JSON.stringify(name)}:${JSON.stringify(target)},\n`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const folderName = `src/${dir}/${fileName}`.replace(/^src\/plugins\//, "");
|
||||||
|
|
||||||
const mod = `p${i}`;
|
const mod = `p${i}`;
|
||||||
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
|
code += `import ${mod} from "./${dir}/${fileName.replace(/\.tsx?$/, "")}";\n`;
|
||||||
plugins += `[${mod}.name]:${mod},\n`;
|
pluginsCode += `[${mod}.name]:${mod},\n`;
|
||||||
|
metaCode += `[${mod}.name]:${JSON.stringify({ folderName, userPlugin })},\n`; // TODO: add excluded plugins to display in the UI?
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
code += `export default {${plugins}};`;
|
code += `export default {${pluginsCode}};export const PluginMeta={${metaCode}};export const ExcludedPlugins={${excludedCode}};`;
|
||||||
return {
|
return {
|
||||||
contents: code,
|
contents: code,
|
||||||
resolveDir: "./src"
|
resolveDir: "./src"
|
||||||
|
@ -161,21 +208,60 @@ export const gitRemotePlugin = {
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
export const fileIncludePlugin = {
|
export const fileUrlPlugin = {
|
||||||
name: "file-include-plugin",
|
name: "file-uri-plugin",
|
||||||
setup: build => {
|
setup: build => {
|
||||||
const filter = /^~fileContent\/.+$/;
|
const filter = /^file:\/\/.+$/;
|
||||||
build.onResolve({ filter }, args => ({
|
build.onResolve({ filter }, args => ({
|
||||||
namespace: "include-file",
|
namespace: "file-uri",
|
||||||
path: args.path,
|
path: args.path,
|
||||||
pluginData: {
|
pluginData: {
|
||||||
path: join(args.resolveDir, args.path.slice("include-file/".length))
|
uri: args.path,
|
||||||
|
path: join(args.resolveDir, args.path.slice("file://".length).split("?")[0])
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
build.onLoad({ filter, namespace: "include-file" }, async ({ pluginData: { path } }) => {
|
build.onLoad({ filter, namespace: "file-uri" }, async ({ pluginData: { path, uri } }) => {
|
||||||
const [name, format] = path.split(";");
|
const { searchParams } = new URL(uri);
|
||||||
|
const base64 = searchParams.has("base64");
|
||||||
|
const minify = IS_STANDALONE === true && searchParams.has("minify");
|
||||||
|
const noTrim = searchParams.get("trim") === "false";
|
||||||
|
|
||||||
|
const encoding = base64 ? "base64" : "utf-8";
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if (!minify) {
|
||||||
|
content = await readFile(path, encoding);
|
||||||
|
if (!noTrim) content = content.trimEnd();
|
||||||
|
} else {
|
||||||
|
if (path.endsWith(".html")) {
|
||||||
|
content = await minifyHtml(await readFile(path, "utf-8"), {
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeComments: true,
|
||||||
|
minifyCSS: true,
|
||||||
|
minifyJS: true,
|
||||||
|
removeEmptyAttributes: true,
|
||||||
|
removeRedundantAttributes: true,
|
||||||
|
removeScriptTypeAttributes: true,
|
||||||
|
removeStyleLinkTypeAttributes: true,
|
||||||
|
useShortDoctype: true
|
||||||
|
});
|
||||||
|
} else if (/[mc]?[jt]sx?$/.test(path)) {
|
||||||
|
const res = await esbuild.build({
|
||||||
|
entryPoints: [path],
|
||||||
|
write: false,
|
||||||
|
minify: true
|
||||||
|
});
|
||||||
|
content = res.outputFiles[0].text;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Don't know how to minify file type: ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (base64)
|
||||||
|
content = Buffer.from(content).toString("base64");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contents: `export default ${JSON.stringify(await readFile(name, format ?? "utf-8"))}`
|
contents: `export default ${JSON.stringify(content)}`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -217,7 +303,7 @@ export const commonOpts = {
|
||||||
sourcemap: watch ? "inline" : "",
|
sourcemap: watch ? "inline" : "",
|
||||||
legalComments: "linked",
|
legalComments: "linked",
|
||||||
banner,
|
banner,
|
||||||
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
||||||
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
||||||
inject: ["./scripts/build/inject/react.mjs"],
|
inject: ["./scripts/build/inject/react.mjs"],
|
||||||
jsxFactory: "VencordCreateElement",
|
jsxFactory: "VencordCreateElement",
|
||||||
|
|
|
@ -39,7 +39,7 @@ interface PluginData {
|
||||||
hasCommands: boolean;
|
hasCommands: boolean;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
enabledByDefault: boolean;
|
enabledByDefault: boolean;
|
||||||
target: "discordDesktop" | "vencordDesktop" | "web" | "dev";
|
target: "discordDesktop" | "vencordDesktop" | "desktop" | "web" | "dev";
|
||||||
filePath: string;
|
filePath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable no-fallthrough */
|
||||||
|
|
||||||
// eslint-disable-next-line spaced-comment
|
// eslint-disable-next-line spaced-comment
|
||||||
/// <reference types="../src/globals" />
|
/// <reference types="../src/globals" />
|
||||||
// eslint-disable-next-line spaced-comment
|
// eslint-disable-next-line spaced-comment
|
||||||
|
@ -40,10 +42,12 @@ const browser = await pup.launch({
|
||||||
|
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
|
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
|
||||||
|
await page.setBypassCSP(true);
|
||||||
|
|
||||||
function maybeGetError(handle: JSHandle) {
|
async function maybeGetError(handle: JSHandle): Promise<string | undefined> {
|
||||||
return (handle as JSHandle<Error>)?.getProperty("message")
|
return await (handle as JSHandle<Error>)?.getProperty("message")
|
||||||
.then(m => m.jsonValue());
|
.then(m => m?.jsonValue())
|
||||||
|
.catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const report = {
|
const report = {
|
||||||
|
@ -59,6 +63,7 @@ const report = {
|
||||||
error: string;
|
error: string;
|
||||||
}[],
|
}[],
|
||||||
otherErrors: [] as string[],
|
otherErrors: [] as string[],
|
||||||
|
ignoredErrors: [] as string[],
|
||||||
badWebpackFinds: [] as string[]
|
badWebpackFinds: [] as string[]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,9 +76,11 @@ const IGNORED_DISCORD_ERRORS = [
|
||||||
"Attempting to set fast connect zstd when unsupported"
|
"Attempting to set fast connect zstd when unsupported"
|
||||||
] as Array<string | RegExp>;
|
] as Array<string | RegExp>;
|
||||||
|
|
||||||
function toCodeBlock(s: string) {
|
function toCodeBlock(s: string, indentation = 0, isDiscord = false) {
|
||||||
s = s.replace(/```/g, "`\u200B`\u200B`");
|
s = s.replace(/```/g, "`\u200B`\u200B`");
|
||||||
return "```" + s + " ```";
|
|
||||||
|
const indentationStr = Array(!isDiscord ? indentation : 0).fill(" ").join("");
|
||||||
|
return `\`\`\`\n${s.split("\n").map(s => indentationStr + s).join("\n")}\n${indentationStr}\`\`\``;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function printReport() {
|
async function printReport() {
|
||||||
|
@ -87,44 +94,35 @@ async function printReport() {
|
||||||
report.badPatches.forEach(p => {
|
report.badPatches.forEach(p => {
|
||||||
console.log(`- ${p.plugin} (${p.type})`);
|
console.log(`- ${p.plugin} (${p.type})`);
|
||||||
console.log(` - ID: \`${p.id}\``);
|
console.log(` - ID: \`${p.id}\``);
|
||||||
console.log(` - Match: ${toCodeBlock(p.match)}`);
|
console.log(` - Match: ${toCodeBlock(p.match, " - Match: ".length)}`);
|
||||||
if (p.error) console.log(` - Error: ${toCodeBlock(p.error)}`);
|
if (p.error) console.log(` - Error: ${toCodeBlock(p.error, " - Error: ".length)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
console.log("## Bad Webpack Finds");
|
console.log("## Bad Webpack Finds");
|
||||||
report.badWebpackFinds.forEach(p => console.log("- " + p));
|
report.badWebpackFinds.forEach(p => console.log("- " + toCodeBlock(p, "- ".length)));
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
console.log("## Bad Starts");
|
console.log("## Bad Starts");
|
||||||
report.badStarts.forEach(p => {
|
report.badStarts.forEach(p => {
|
||||||
console.log(`- ${p.plugin}`);
|
console.log(`- ${p.plugin}`);
|
||||||
console.log(` - Error: ${toCodeBlock(p.error)}`);
|
console.log(` - Error: ${toCodeBlock(p.error, " - Error: ".length)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
const ignoredErrors = [] as string[];
|
|
||||||
report.otherErrors = report.otherErrors.filter(e => {
|
|
||||||
if (IGNORED_DISCORD_ERRORS.some(regex => e.match(regex))) {
|
|
||||||
ignoredErrors.push(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("## Discord Errors");
|
console.log("## Discord Errors");
|
||||||
report.otherErrors.forEach(e => {
|
report.otherErrors.forEach(e => {
|
||||||
console.log(`- ${toCodeBlock(e)}`);
|
console.log(`- ${toCodeBlock(e, "- ".length)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
console.log("## Ignored Discord Errors");
|
console.log("## Ignored Discord Errors");
|
||||||
ignoredErrors.forEach(e => {
|
report.ignoredErrors.forEach(e => {
|
||||||
console.log(`- ${toCodeBlock(e)}`);
|
console.log(`- ${toCodeBlock(e, "- ".length)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
|
@ -138,7 +136,6 @@ async function printReport() {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
description: "Here's the latest Vencord Report!",
|
description: "Here's the latest Vencord Report!",
|
||||||
username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""),
|
username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""),
|
||||||
avatar_url: "https://cdn.discordapp.com/avatars/1017176847865352332/c312b6b44179ae6817de7e4b09e9c6af.webp?size=512",
|
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: "Bad Patches",
|
title: "Bad Patches",
|
||||||
|
@ -146,16 +143,16 @@ async function printReport() {
|
||||||
const lines = [
|
const lines = [
|
||||||
`**__${p.plugin} (${p.type}):__**`,
|
`**__${p.plugin} (${p.type}):__**`,
|
||||||
`ID: \`${p.id}\``,
|
`ID: \`${p.id}\``,
|
||||||
`Match: ${toCodeBlock(p.match)}`
|
`Match: ${toCodeBlock(p.match, "Match: ".length, true)}`
|
||||||
];
|
];
|
||||||
if (p.error) lines.push(`Error: ${toCodeBlock(p.error)}`);
|
if (p.error) lines.push(`Error: ${toCodeBlock(p.error, "Error: ".length, true)}`);
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}).join("\n\n") || "None",
|
}).join("\n\n") || "None",
|
||||||
color: report.badPatches.length ? 0xff0000 : 0x00ff00
|
color: report.badPatches.length ? 0xff0000 : 0x00ff00
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Bad Webpack Finds",
|
title: "Bad Webpack Finds",
|
||||||
description: report.badWebpackFinds.map(toCodeBlock).join("\n") || "None",
|
description: report.badWebpackFinds.map(f => toCodeBlock(f, 0, true)).join("\n") || "None",
|
||||||
color: report.badWebpackFinds.length ? 0xff0000 : 0x00ff00
|
color: report.badWebpackFinds.length ? 0xff0000 : 0x00ff00
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -163,7 +160,7 @@ async function printReport() {
|
||||||
description: report.badStarts.map(p => {
|
description: report.badStarts.map(p => {
|
||||||
const lines = [
|
const lines = [
|
||||||
`**__${p.plugin}:__**`,
|
`**__${p.plugin}:__**`,
|
||||||
toCodeBlock(p.error)
|
toCodeBlock(p.error, 0, true)
|
||||||
];
|
];
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
@ -172,7 +169,7 @@ async function printReport() {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Discord Errors",
|
title: "Discord Errors",
|
||||||
description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n")) : "None",
|
description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n"), 0, true) : "None",
|
||||||
color: report.otherErrors.length ? 0xff0000 : 0x00ff00
|
color: report.otherErrors.length ? 0xff0000 : 0x00ff00
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -188,33 +185,39 @@ page.on("console", async e => {
|
||||||
const level = e.type();
|
const level = e.type();
|
||||||
const rawArgs = e.args();
|
const rawArgs = e.args();
|
||||||
|
|
||||||
const firstArg = await rawArgs[0]?.jsonValue();
|
async function getText() {
|
||||||
if (firstArg === "[PUPPETEER_TEST_DONE_SIGNAL]") {
|
try {
|
||||||
await browser.close();
|
return await Promise.all(
|
||||||
await printReport();
|
e.args().map(async a => {
|
||||||
process.exit();
|
return await maybeGetError(a) || await a.jsonValue();
|
||||||
|
})
|
||||||
|
).then(a => a.join(" ").trim());
|
||||||
|
} catch {
|
||||||
|
return e.text();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstArg = await rawArgs[0]?.jsonValue();
|
||||||
|
|
||||||
const isVencord = firstArg === "[Vencord]";
|
const isVencord = firstArg === "[Vencord]";
|
||||||
const isDebug = firstArg === "[PUP_DEBUG]";
|
const isDebug = firstArg === "[PUP_DEBUG]";
|
||||||
const isWebpackFindFail = firstArg === "[PUP_WEBPACK_FIND_FAIL]";
|
|
||||||
|
|
||||||
if (isWebpackFindFail) {
|
outer:
|
||||||
process.exitCode = 1;
|
if (isVencord) {
|
||||||
report.badWebpackFinds.push(await rawArgs[1].jsonValue() as string);
|
try {
|
||||||
|
var args = await Promise.all(e.args().map(a => a.jsonValue()));
|
||||||
|
} catch {
|
||||||
|
break outer;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVencord) {
|
const [, tag, message, otherMessage] = args as Array<string>;
|
||||||
const args = await Promise.all(e.args().map(a => a.jsonValue()));
|
|
||||||
|
|
||||||
const [, tag, message] = args as Array<string>;
|
|
||||||
const cause = await maybeGetError(e.args()[3]);
|
|
||||||
|
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case "WebpackInterceptor:":
|
case "WebpackInterceptor:":
|
||||||
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
|
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
|
||||||
if (!patchFailMatch) break;
|
if (!patchFailMatch) break;
|
||||||
|
|
||||||
|
console.error(await getText());
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
|
|
||||||
const [, plugin, type, id, regex] = patchFailMatch;
|
const [, plugin, type, id, regex] = patchFailMatch;
|
||||||
|
@ -223,7 +226,7 @@ page.on("console", async e => {
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
|
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
|
||||||
error: cause
|
error: await maybeGetError(e.args()[3])
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -231,176 +234,72 @@ page.on("console", async e => {
|
||||||
const failedToStartMatch = message.match(/Failed to start (.+)/);
|
const failedToStartMatch = message.match(/Failed to start (.+)/);
|
||||||
if (!failedToStartMatch) break;
|
if (!failedToStartMatch) break;
|
||||||
|
|
||||||
|
console.error(await getText());
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
|
|
||||||
const [, name] = failedToStartMatch;
|
const [, name] = failedToStartMatch;
|
||||||
report.badStarts.push({
|
report.badStarts.push({
|
||||||
plugin: name,
|
plugin: name,
|
||||||
error: cause
|
error: await maybeGetError(e.args()[3]) ?? "Unknown error"
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
case "LazyChunkLoader:":
|
||||||
|
console.error(await getText());
|
||||||
|
|
||||||
|
switch (message) {
|
||||||
|
case "A fatal error occurred:":
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "Reporter:":
|
||||||
|
console.error(await getText());
|
||||||
|
|
||||||
|
switch (message) {
|
||||||
|
case "A fatal error occurred:":
|
||||||
|
process.exit(1);
|
||||||
|
case "Webpack Find Fail:":
|
||||||
|
process.exitCode = 1;
|
||||||
|
report.badWebpackFinds.push(otherMessage);
|
||||||
|
break;
|
||||||
|
case "Finished test":
|
||||||
|
await browser.close();
|
||||||
|
await printReport();
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
console.error(e.text());
|
console.error(await getText());
|
||||||
} else if (level === "error") {
|
} else if (level === "error") {
|
||||||
const text = await Promise.all(
|
const text = await getText();
|
||||||
e.args().map(async a => {
|
|
||||||
try {
|
|
||||||
return await maybeGetError(a) || await a.jsonValue();
|
|
||||||
} catch (e) {
|
|
||||||
return a.toString();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
).then(a => a.join(" ").trim());
|
|
||||||
|
|
||||||
|
|
||||||
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
||||||
|
if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) {
|
||||||
|
report.ignoredErrors.push(text);
|
||||||
|
} else {
|
||||||
console.error("[Unexpected Error]", text);
|
console.error("[Unexpected Error]", text);
|
||||||
report.otherErrors.push(text);
|
report.otherErrors.push(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
page.on("error", e => console.error("[Error]", e));
|
page.on("error", e => console.error("[Error]", e.message));
|
||||||
page.on("pageerror", e => console.error("[Page Error]", e));
|
page.on("pageerror", e => {
|
||||||
|
if (e.message.includes("Sentry successfully disabled")) return;
|
||||||
|
|
||||||
await page.setBypassCSP(true);
|
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) {
|
||||||
|
console.error("[Page Error]", e.message);
|
||||||
async function runtime(token: string) {
|
report.otherErrors.push(e.message);
|
||||||
console.log("[PUP_DEBUG]", "Starting test...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Spoof languages to not be suspicious
|
|
||||||
Object.defineProperty(navigator, "languages", {
|
|
||||||
get: function () {
|
|
||||||
return ["en-US", "en"];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Monkey patch Logger to not log with custom css
|
|
||||||
// @ts-ignore
|
|
||||||
const originalLog = Vencord.Util.Logger.prototype._log;
|
|
||||||
// @ts-ignore
|
|
||||||
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
|
|
||||||
if (level === "warn" || level === "error")
|
|
||||||
return console[level]("[Vencord]", this.name + ":", ...args);
|
|
||||||
|
|
||||||
return originalLog.call(this, level, levelColor, args);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Force enable all plugins and patches
|
|
||||||
Vencord.Plugins.patches.length = 0;
|
|
||||||
Object.values(Vencord.Plugins.plugins).forEach(p => {
|
|
||||||
// Needs native server to run
|
|
||||||
if (p.name === "WebRichPresence (arRPC)") return;
|
|
||||||
|
|
||||||
Vencord.Settings.plugins[p.name].enabled = true;
|
|
||||||
p.patches?.forEach(patch => {
|
|
||||||
patch.plugin = p.name;
|
|
||||||
delete patch.predicate;
|
|
||||||
delete patch.group;
|
|
||||||
|
|
||||||
Vencord.Util.canonicalizeFind(patch);
|
|
||||||
if (!Array.isArray(patch.replacement)) {
|
|
||||||
patch.replacement = [patch.replacement];
|
|
||||||
}
|
|
||||||
|
|
||||||
patch.replacement.forEach(r => {
|
|
||||||
delete r.predicate;
|
|
||||||
});
|
|
||||||
|
|
||||||
Vencord.Plugins.patches.push(patch);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let wreq: typeof Vencord.Webpack.wreq;
|
|
||||||
|
|
||||||
const { canonicalizeMatch, Logger } = Vencord.Util;
|
|
||||||
|
|
||||||
const validChunks = new Set<string>();
|
|
||||||
const invalidChunks = new Set<string>();
|
|
||||||
|
|
||||||
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
|
|
||||||
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
|
|
||||||
|
|
||||||
// True if resolved, false otherwise
|
|
||||||
const chunksSearchPromises = [] as Array<() => boolean>;
|
|
||||||
const lazyChunkRegex = canonicalizeMatch(/Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/g);
|
|
||||||
const chunkIdsRegex = canonicalizeMatch(/\("(.+?)"\)/g);
|
|
||||||
|
|
||||||
async function searchAndLoadLazyChunks(factoryCode: string) {
|
|
||||||
const lazyChunks = factoryCode.matchAll(lazyChunkRegex);
|
|
||||||
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
|
|
||||||
|
|
||||||
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
|
|
||||||
const chunkIds = Array.from(rawChunkIds.matchAll(chunkIdsRegex)).map(m => m[1]);
|
|
||||||
if (chunkIds.length === 0) return;
|
|
||||||
|
|
||||||
let invalidChunkGroup = false;
|
|
||||||
|
|
||||||
for (const id of chunkIds) {
|
|
||||||
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
|
|
||||||
|
|
||||||
const isWasm = await fetch(wreq.p + wreq.u(id))
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
|
||||||
|
|
||||||
if (isWasm) {
|
|
||||||
invalidChunks.add(id);
|
|
||||||
invalidChunkGroup = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
validChunks.add(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!invalidChunkGroup) {
|
|
||||||
validChunkGroups.add([chunkIds, entryPoint]);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Loads all found valid chunk groups
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(validChunkGroups)
|
|
||||||
.map(([chunkIds]) =>
|
|
||||||
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Requires the entry points for all valid chunk groups
|
|
||||||
for (const [, entryPoint] of validChunkGroups) {
|
|
||||||
try {
|
|
||||||
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setImmediate to only check if all chunks were loaded after this function resolves
|
|
||||||
// We check if all chunks were loaded every time a factory is loaded
|
|
||||||
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
|
|
||||||
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
|
|
||||||
setTimeout(() => {
|
|
||||||
let allResolved = true;
|
|
||||||
|
|
||||||
for (let i = 0; i < chunksSearchPromises.length; i++) {
|
|
||||||
const isResolved = chunksSearchPromises[i]();
|
|
||||||
|
|
||||||
if (isResolved) {
|
|
||||||
// Remove finished promises to avoid having to iterate through a huge array everytime
|
|
||||||
chunksSearchPromises.splice(i--, 1);
|
|
||||||
} else {
|
} else {
|
||||||
allResolved = false;
|
report.ignoredErrors.push(e.message);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allResolved) chunksSearchingResolve();
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function reporterRuntime(token: string) {
|
||||||
Vencord.Webpack.waitFor(
|
Vencord.Webpack.waitFor(
|
||||||
"loginToken",
|
"loginToken",
|
||||||
m => {
|
m => {
|
||||||
|
@ -408,120 +307,13 @@ async function runtime(token: string) {
|
||||||
m.loginToken(token);
|
m.loginToken(token);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
Vencord.Webpack.beforeInitListeners.add(async webpackRequire => {
|
|
||||||
console.log("[PUP_DEBUG]", "Loading all chunks...");
|
|
||||||
|
|
||||||
wreq = webpackRequire;
|
|
||||||
|
|
||||||
Vencord.Webpack.factoryListeners.add(factory => {
|
|
||||||
let isResolved = false;
|
|
||||||
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
|
|
||||||
|
|
||||||
chunksSearchPromises.push(() => isResolved);
|
|
||||||
});
|
|
||||||
|
|
||||||
// setImmediate to only search the initial factories after Discord initialized the app
|
|
||||||
// our beforeInitListeners are called before Discord initializes the app
|
|
||||||
setTimeout(() => {
|
|
||||||
for (const factoryId in wreq.m) {
|
|
||||||
let isResolved = false;
|
|
||||||
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
|
|
||||||
|
|
||||||
chunksSearchPromises.push(() => isResolved);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
await chunksSearchingDone;
|
|
||||||
|
|
||||||
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
|
||||||
const allChunks = [] as string[];
|
|
||||||
|
|
||||||
// Matches "id" or id:
|
|
||||||
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
|
|
||||||
const id = currentMatch[1] ?? currentMatch[2];
|
|
||||||
if (id == null) continue;
|
|
||||||
|
|
||||||
allChunks.push(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
|
||||||
|
|
||||||
// Chunks that are not loaded (not used) by Discord code anymore
|
|
||||||
const chunksLeft = allChunks.filter(id => {
|
|
||||||
return !(validChunks.has(id) || invalidChunks.has(id));
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(chunksLeft.map(async id => {
|
|
||||||
const isWasm = await fetch(wreq.p + wreq.u(id))
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
|
||||||
|
|
||||||
// Loads and requires a chunk
|
|
||||||
if (!isWasm) {
|
|
||||||
await wreq.e(id as any);
|
|
||||||
if (wreq.m[id]) wreq(id as any);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log("[PUP_DEBUG]", "Finished loading all chunks!");
|
|
||||||
|
|
||||||
for (const patch of Vencord.Plugins.patches) {
|
|
||||||
if (!patch.all) {
|
|
||||||
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) {
|
|
||||||
let method = searchType;
|
|
||||||
|
|
||||||
if (searchType === "findComponent") method = "find";
|
|
||||||
if (searchType === "findExportedComponent") method = "findByProps";
|
|
||||||
if (searchType === "waitFor" || searchType === "waitForComponent") {
|
|
||||||
if (typeof args[0] === "string") method = "findByProps";
|
|
||||||
else method = "find";
|
|
||||||
}
|
|
||||||
if (searchType === "waitForStore") method = "findStore";
|
|
||||||
|
|
||||||
try {
|
|
||||||
let result: any;
|
|
||||||
|
|
||||||
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
|
|
||||||
const [factory] = args;
|
|
||||||
result = factory();
|
|
||||||
} else if (method === "extractAndLoadChunks") {
|
|
||||||
const [code, matcher] = args;
|
|
||||||
|
|
||||||
const module = Vencord.Webpack.findModuleFactory(...code);
|
|
||||||
if (module) result = module.toString().match(canonicalizeMatch(matcher));
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
result = Vencord.Webpack[method](...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
|
|
||||||
} catch (e) {
|
|
||||||
let logMessage = searchType;
|
|
||||||
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
|
||||||
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
|
||||||
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
|
||||||
|
|
||||||
console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
|
|
||||||
} catch (e) {
|
|
||||||
console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.evaluateOnNewDocument(`
|
await page.evaluateOnNewDocument(`
|
||||||
${readFileSync("./dist/browser.js", "utf-8")}
|
if (location.host.endsWith("discord.com")) {
|
||||||
|
${readFileSync("./dist/browser.js", "utf-8")};
|
||||||
;(${runtime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
(${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
|
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * as Api from "./api";
|
export * as Api from "./api";
|
||||||
|
export * as Components from "./components";
|
||||||
export * as Plugins from "./plugins";
|
export * as Plugins from "./plugins";
|
||||||
export * as Util from "./utils";
|
export * as Util from "./utils";
|
||||||
export * as QuickCss from "./utils/quickCss";
|
export * as QuickCss from "./utils/quickCss";
|
||||||
|
@ -41,6 +42,10 @@ import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
|
||||||
import { onceReady } from "./webpack";
|
import { onceReady } from "./webpack";
|
||||||
import { SettingsRouter } from "./webpack/common";
|
import { SettingsRouter } from "./webpack/common";
|
||||||
|
|
||||||
|
if (IS_REPORTER) {
|
||||||
|
require("./debug/runReporter");
|
||||||
|
}
|
||||||
|
|
||||||
async function syncSettings() {
|
async function syncSettings() {
|
||||||
// pre-check for local shared settings
|
// pre-check for local shared settings
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { User } from "discord-types/general";
|
|
||||||
import { ComponentType, HTMLProps } from "react";
|
import { ComponentType, HTMLProps } from "react";
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
@ -45,6 +44,11 @@ export interface ProfileBadge {
|
||||||
position?: BadgePosition;
|
position?: BadgePosition;
|
||||||
/** The badge name to display, Discord uses this. Required for component badges */
|
/** The badge name to display, Discord uses this. Required for component badges */
|
||||||
key?: string;
|
key?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows dynamically returning multiple badges
|
||||||
|
*/
|
||||||
|
getBadges?(userInfo: BadgeUserArgs): ProfileBadge[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Badges = new Set<ProfileBadge>();
|
const Badges = new Set<ProfileBadge>();
|
||||||
|
@ -74,19 +78,26 @@ export function _getBadges(args: BadgeUserArgs) {
|
||||||
const badges = [] as ProfileBadge[];
|
const badges = [] as ProfileBadge[];
|
||||||
for (const badge of Badges) {
|
for (const badge of Badges) {
|
||||||
if (!badge.shouldShow || badge.shouldShow(args)) {
|
if (!badge.shouldShow || badge.shouldShow(args)) {
|
||||||
|
const b = badge.getBadges
|
||||||
|
? badge.getBadges(args).map(b => {
|
||||||
|
b.component &&= ErrorBoundary.wrap(b.component, { noop: true });
|
||||||
|
return b;
|
||||||
|
})
|
||||||
|
: [{ ...badge, ...args }];
|
||||||
|
|
||||||
badge.position === BadgePosition.START
|
badge.position === BadgePosition.START
|
||||||
? badges.unshift({ ...badge, ...args })
|
? badges.unshift(...b)
|
||||||
: badges.push({ ...badge, ...args });
|
: badges.push(...b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/_api/badges").default).getDonorBadges(args.user.id);
|
const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/_api/badges").default).getDonorBadges(args.userId);
|
||||||
if (donorBadges) badges.unshift(...donorBadges);
|
if (donorBadges) badges.unshift(...donorBadges);
|
||||||
|
|
||||||
return badges;
|
return badges;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BadgeUserArgs {
|
export interface BadgeUserArgs {
|
||||||
user: User;
|
userId: string;
|
||||||
guildId: string;
|
guildId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,14 +17,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mergeDefaults } from "@utils/mergeDefaults";
|
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByCodeLazy } from "@webpack";
|
||||||
import { MessageActions, SnowflakeUtils } from "@webpack/common";
|
import { MessageActions, SnowflakeUtils } from "@webpack/common";
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
import type { PartialDeep } from "type-fest";
|
import type { PartialDeep } from "type-fest";
|
||||||
|
|
||||||
import { Argument } from "./types";
|
import { Argument } from "./types";
|
||||||
|
|
||||||
const MessageCreator = findByPropsLazy("createBotMessage");
|
const createBotMessage = findByCodeLazy('username:"Clyde"');
|
||||||
|
|
||||||
export function generateId() {
|
export function generateId() {
|
||||||
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
|
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
|
||||||
|
@ -37,7 +37,7 @@ export function generateId() {
|
||||||
* @returns {Message}
|
* @returns {Message}
|
||||||
*/
|
*/
|
||||||
export function sendBotMessage(channelId: string, message: PartialDeep<Message>): Message {
|
export function sendBotMessage(channelId: string, message: PartialDeep<Message>): Message {
|
||||||
const botMessage = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] });
|
const botMessage = createBotMessage({ channelId, content: "", embeds: [] });
|
||||||
|
|
||||||
MessageActions.receiveMessage(channelId, mergeDefaults(message, botMessage));
|
MessageActions.receiveMessage(channelId, mergeDefaults(message, botMessage));
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ let defaultGetStoreFunc: UseStore | undefined;
|
||||||
|
|
||||||
function defaultGetStore() {
|
function defaultGetStore() {
|
||||||
if (!defaultGetStoreFunc) {
|
if (!defaultGetStoreFunc) {
|
||||||
defaultGetStoreFunc = createStore("VencordData", "VencordStore");
|
defaultGetStoreFunc = createStore(!IS_REPORTER ? "VencordData" : "VencordDataReporter", "VencordStore");
|
||||||
}
|
}
|
||||||
return defaultGetStoreFunc;
|
return defaultGetStoreFunc;
|
||||||
}
|
}
|
||||||
|
|
29
src/api/MessageUpdater.ts
Normal file
29
src/api/MessageUpdater.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MessageCache, MessageStore } from "@webpack/common";
|
||||||
|
import { FluxStore } from "@webpack/types";
|
||||||
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update and re-render a message
|
||||||
|
* @param channelId The channel id of the message
|
||||||
|
* @param messageId The message id
|
||||||
|
* @param fields The fields of the message to change. Leave empty if you just want to re-render
|
||||||
|
*/
|
||||||
|
export function updateMessage(channelId: string, messageId: string, fields?: Partial<Message & Record<string, any>>) {
|
||||||
|
const channelMessageCache = MessageCache.getOrCreate(channelId);
|
||||||
|
if (!channelMessageCache.has(messageId)) return;
|
||||||
|
|
||||||
|
// To cause a message to re-render, we basically need to create a new instance of the message and obtain a new reference
|
||||||
|
// If we have fields to modify we can use the merge method of the class, otherwise we just create a new instance with the old fields
|
||||||
|
const newChannelMessageCache = channelMessageCache.update(messageId, (oldMessage: any) => {
|
||||||
|
return fields ? oldMessage.merge(fields) : new oldMessage.constructor(oldMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
MessageCache.commit(newChannelMessageCache);
|
||||||
|
(MessageStore as unknown as FluxStore).emitChange();
|
||||||
|
}
|
|
@ -113,7 +113,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
||||||
{timeout !== 0 && !permanent && (
|
{timeout !== 0 && !permanent && (
|
||||||
<div
|
<div
|
||||||
className="vc-notification-progressbar"
|
className="vc-notification-progressbar"
|
||||||
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
|
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-500)" }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -100,6 +100,7 @@ export async function showNotification(data: NotificationData) {
|
||||||
const n = new Notification(title, {
|
const n = new Notification(title, {
|
||||||
body,
|
body,
|
||||||
icon,
|
icon,
|
||||||
|
// @ts-expect-error ts is drunk
|
||||||
image
|
image
|
||||||
});
|
});
|
||||||
n.onclick = onClick;
|
n.onclick = onClick;
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
import * as DataStore from "@api/DataStore";
|
import * as DataStore from "@api/DataStore";
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import { Flex } from "@components/Flex";
|
||||||
|
import { openNotificationSettingsModal } from "@components/VencordSettings/NotificationSettings";
|
||||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
|
||||||
|
@ -170,8 +172,14 @@ function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
<Flex>
|
||||||
|
<Button onClick={openNotificationSettingsModal}>
|
||||||
|
Notification Settings
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={log.length === 0}
|
disabled={log.length === 0}
|
||||||
|
color={Button.Colors.RED}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
Alerts.show({
|
Alerts.show({
|
||||||
title: "Are you sure?",
|
title: "Are you sure?",
|
||||||
|
@ -188,6 +196,7 @@ function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void
|
||||||
>
|
>
|
||||||
Clear Notification Log
|
Clear Notification Log
|
||||||
</Button>
|
</Button>
|
||||||
|
</Flex>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
);
|
);
|
||||||
|
|
|
@ -106,7 +106,7 @@ const DefaultSettings: Settings = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const settings = VencordNative.settings.get();
|
const settings = !IS_REPORTER ? VencordNative.settings.get() : {} as Settings;
|
||||||
mergeDefaults(settings, DefaultSettings);
|
mergeDefaults(settings, DefaultSettings);
|
||||||
|
|
||||||
const saveSettingsOnFrequentAction = debounce(async () => {
|
const saveSettingsOnFrequentAction = debounce(async () => {
|
||||||
|
@ -129,7 +129,7 @@ export const SettingsStore = new SettingsStoreClass(settings, {
|
||||||
|
|
||||||
if (path === "plugins" && key in plugins)
|
if (path === "plugins" && key in plugins)
|
||||||
return target[key] = {
|
return target[key] = {
|
||||||
enabled: plugins[key].required ?? plugins[key].enabledByDefault ?? false
|
enabled: IS_REPORTER || plugins[key].required || plugins[key].enabledByDefault || false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||||
|
@ -156,12 +156,14 @@ export const SettingsStore = new SettingsStoreClass(settings, {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!IS_REPORTER) {
|
||||||
SettingsStore.addGlobalChangeListener((_, path) => {
|
SettingsStore.addGlobalChangeListener((_, path) => {
|
||||||
SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
|
SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
|
||||||
localStorage.Vencord_settingsDirty = true;
|
localStorage.Vencord_settingsDirty = true;
|
||||||
saveSettingsOnFrequentAction();
|
saveSettingsOnFrequentAction();
|
||||||
VencordNative.settings.set(SettingsStore.plain, path);
|
VencordNative.settings.set(SettingsStore.plain, path);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as {@link Settings} but unproxied. You should treat this as readonly,
|
* Same as {@link Settings} but unproxied. You should treat this as readonly,
|
||||||
|
|
81
src/api/UserSettings.ts
Normal file
81
src/api/UserSettings.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* 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, proxyLazyWebpack, wreq } from "@webpack";
|
||||||
|
|
||||||
|
interface UserSettingDefinition<T> {
|
||||||
|
/**
|
||||||
|
* Get the setting value
|
||||||
|
*/
|
||||||
|
getSetting(): T;
|
||||||
|
/**
|
||||||
|
* Update the setting value
|
||||||
|
* @param value The new value
|
||||||
|
*/
|
||||||
|
updateSetting(value: T): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Update the setting value
|
||||||
|
* @param value A callback that accepts the old value as the first argument, and returns the new value
|
||||||
|
*/
|
||||||
|
updateSetting(value: (old: T) => T): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Stateful React hook for this setting value
|
||||||
|
*/
|
||||||
|
useSetting(): T;
|
||||||
|
userSettingsAPIGroup: string;
|
||||||
|
userSettingsAPIName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserSettings: Record<PropertyKey, UserSettingDefinition<any>> | undefined = proxyLazyWebpack(() => {
|
||||||
|
const modId = findModuleId('"textAndImages","renderSpoilers"');
|
||||||
|
if (modId == null) return new Logger("UserSettingsAPI ").error("Didn't find settings module.");
|
||||||
|
|
||||||
|
return wreq(modId as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the setting with the given setting group and name.
|
||||||
|
*
|
||||||
|
* @param group The setting group
|
||||||
|
* @param name The name of the setting
|
||||||
|
*/
|
||||||
|
export function getUserSetting<T = any>(group: string, name: string): UserSettingDefinition<T> | undefined {
|
||||||
|
if (!Vencord.Plugins.isPluginEnabled("UserSettingsAPI")) throw new Error("Cannot use UserSettingsAPI without setting as dependency.");
|
||||||
|
|
||||||
|
for (const key in UserSettings) {
|
||||||
|
const userSetting = UserSettings[key];
|
||||||
|
|
||||||
|
if (userSetting.userSettingsAPIGroup === group && userSetting.userSettingsAPIName === name) {
|
||||||
|
return userSetting;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link getUserSettingDefinition}, lazy.
|
||||||
|
*
|
||||||
|
* Get the setting with the given setting group and name.
|
||||||
|
*
|
||||||
|
* @param group The setting group
|
||||||
|
* @param name The name of the setting
|
||||||
|
*/
|
||||||
|
export function getUserSettingLazy<T = any>(group: string, name: string) {
|
||||||
|
return proxyLazy(() => getUserSetting<T>(group, name));
|
||||||
|
}
|
|
@ -26,11 +26,13 @@ import * as $MessageAccessories from "./MessageAccessories";
|
||||||
import * as $MessageDecorations from "./MessageDecorations";
|
import * as $MessageDecorations from "./MessageDecorations";
|
||||||
import * as $MessageEventsAPI from "./MessageEvents";
|
import * as $MessageEventsAPI from "./MessageEvents";
|
||||||
import * as $MessagePopover from "./MessagePopover";
|
import * as $MessagePopover from "./MessagePopover";
|
||||||
|
import * as $MessageUpdater from "./MessageUpdater";
|
||||||
import * as $Notices from "./Notices";
|
import * as $Notices from "./Notices";
|
||||||
import * as $Notifications from "./Notifications";
|
import * as $Notifications from "./Notifications";
|
||||||
import * as $ServerList from "./ServerList";
|
import * as $ServerList from "./ServerList";
|
||||||
import * as $Settings from "./Settings";
|
import * as $Settings from "./Settings";
|
||||||
import * as $Styles from "./Styles";
|
import * as $Styles from "./Styles";
|
||||||
|
import * as $UserSettings from "./UserSettings";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An API allowing you to listen to Message Clicks or run your own logic
|
* An API allowing you to listen to Message Clicks or run your own logic
|
||||||
|
@ -110,3 +112,13 @@ export const ContextMenu = $ContextMenu;
|
||||||
* An API allowing you to add buttons to the chat input
|
* An API allowing you to add buttons to the chat input
|
||||||
*/
|
*/
|
||||||
export const ChatButtons = $ChatButtons;
|
export const ChatButtons = $ChatButtons;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An API allowing you to update and re-render messages
|
||||||
|
*/
|
||||||
|
export const MessageUpdater = $MessageUpdater;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An API allowing you to get an user setting
|
||||||
|
*/
|
||||||
|
export const UserSettings = $UserSettings;
|
||||||
|
|
|
@ -16,10 +16,12 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import "./ExpandableHeader.css";
|
||||||
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { Text, Tooltip, useState } from "@webpack/common";
|
import { Text, Tooltip, useState } from "@webpack/common";
|
||||||
export const cl = classNameFactory("vc-expandableheader-");
|
|
||||||
import "./ExpandableHeader.css";
|
const cl = classNameFactory("vc-expandableheader-");
|
||||||
|
|
||||||
export interface ExpandableHeaderProps {
|
export interface ExpandableHeaderProps {
|
||||||
onMoreClick?: () => void;
|
onMoreClick?: () => void;
|
||||||
|
@ -29,10 +31,20 @@ export interface ExpandableHeaderProps {
|
||||||
headerText: string;
|
headerText: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
buttons?: React.ReactNode[];
|
buttons?: React.ReactNode[];
|
||||||
|
forceOpen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
|
export function ExpandableHeader({
|
||||||
const [showContent, setShowContent] = useState(defaultState);
|
children,
|
||||||
|
onMoreClick,
|
||||||
|
buttons,
|
||||||
|
moreTooltipText,
|
||||||
|
onDropDownClick,
|
||||||
|
headerText,
|
||||||
|
defaultState = false,
|
||||||
|
forceOpen = false,
|
||||||
|
}: ExpandableHeaderProps) {
|
||||||
|
const [showContent, setShowContent] = useState(defaultState || forceOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -88,6 +100,7 @@ export default function ExpandableHeader({ children, onMoreClick, buttons, moreT
|
||||||
setShowContent(v => !v);
|
setShowContent(v => !v);
|
||||||
onDropDownClick?.(showContent);
|
onDropDownClick?.(showContent);
|
||||||
}}
|
}}
|
||||||
|
disabled={forceOpen}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="24"
|
||||||
|
|
28
src/components/Grid.tsx
Normal file
28
src/components/Grid.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CSSProperties } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
columns: number;
|
||||||
|
gap?: string;
|
||||||
|
inline?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Grid(props: Props & JSX.IntrinsicElements["div"]) {
|
||||||
|
const style: CSSProperties = {
|
||||||
|
display: props.inline ? "inline-grid" : "grid",
|
||||||
|
gridTemplateColumns: `repeat(${props.columns}, 1fr)`,
|
||||||
|
gap: props.gap,
|
||||||
|
...props.style
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props} style={style}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -18,19 +18,17 @@
|
||||||
|
|
||||||
import "./iconStyles.css";
|
import "./iconStyles.css";
|
||||||
|
|
||||||
|
import { getTheme, Theme } from "@utils/discord";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { i18n } from "@webpack/common";
|
import { i18n } from "@webpack/common";
|
||||||
import type { PropsWithChildren, SVGProps } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
|
|
||||||
interface BaseIconProps extends IconProps {
|
interface BaseIconProps extends IconProps {
|
||||||
viewBox: string;
|
viewBox: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IconProps extends SVGProps<SVGSVGElement> {
|
type IconProps = JSX.IntrinsicElements["svg"];
|
||||||
className?: string;
|
type ImageProps = JSX.IntrinsicElements["img"];
|
||||||
height?: string | number;
|
|
||||||
width?: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
|
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
|
||||||
return (
|
return (
|
||||||
|
@ -290,3 +288,127 @@ export function NoEntrySignIcon(props: IconProps) {
|
||||||
</Icon>
|
</Icon>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SafetyIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
{...props}
|
||||||
|
className={classes(props.className, "vc-safety-icon")}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M4.27 5.22A2.66 2.66 0 0 0 3 7.5v2.3c0 5.6 3.3 10.68 8.42 12.95.37.17.79.17 1.16 0A14.18 14.18 0 0 0 21 9.78V7.5c0-.93-.48-1.78-1.27-2.27l-6.17-3.76a3 3 0 0 0-3.12 0L4.27 5.22ZM6 7.68l6-3.66V12H6.22C6.08 11.28 6 10.54 6 9.78v-2.1Zm6 12.01V12h5.78A11.19 11.19 0 0 1 12 19.7Z"
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotesIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
{...props}
|
||||||
|
className={classes(props.className, "vc-notes-icon")}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M8 3C7.44771 3 7 3.44772 7 4V5C7 5.55228 7.44772 6 8 6H16C16.5523 6 17 5.55228 17 5V4C17 3.44772 16.5523 3 16 3H15.1245C14.7288 3 14.3535 2.82424 14.1002 2.52025L13.3668 1.64018C13.0288 1.23454 12.528 1 12 1C11.472 1 10.9712 1.23454 10.6332 1.64018L9.8998 2.52025C9.64647 2.82424 9.27121 3 8.8755 3H8Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M19 4.49996V4.99996C19 6.65681 17.6569 7.99996 16 7.99996H8C6.34315 7.99996 5 6.65681 5 4.99996V4.49996C5 4.22382 4.77446 3.99559 4.50209 4.04109C3.08221 4.27826 2 5.51273 2 6.99996V19C2 20.6568 3.34315 22 5 22H19C20.6569 22 22 20.6568 22 19V6.99996C22 5.51273 20.9178 4.27826 19.4979 4.04109C19.2255 3.99559 19 4.22382 19 4.49996ZM8 12C7.44772 12 7 12.4477 7 13C7 13.5522 7.44772 14 8 14H16C16.5523 14 17 13.5522 17 13C17 12.4477 16.5523 12 16 12H8ZM7 17C7 16.4477 7.44772 16 8 16H13C13.5523 16 14 16.4477 14 17C14 17.5522 13.5523 18 13 18H8C7.44772 18 7 17.5522 7 17Z"
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FolderIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
{...props}
|
||||||
|
className={classes(props.className, "vc-folder-icon")}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M2 5a3 3 0 0 1 3-3h3.93a2 2 0 0 1 1.66.9L12 5h7a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5Z"
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
{...props}
|
||||||
|
className={classes(props.className, "vc-log-icon")}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M3.11 8H6v10.82c0 .86.37 1.68 1 2.27.46.43 1.02.71 1.63.84A1 1 0 0 0 9 22h10a4 4 0 0 0 4-4v-1a2 2 0 0 0-2-2h-1V5a3 3 0 0 0-3-3H4.67c-.87 0-1.7.32-2.34.9-.63.6-1 1.42-1 2.28 0 .71.3 1.35.52 1.75a5.35 5.35 0 0 0 .48.7l.01.01h.01L3.11 7l-.76.65a1 1 0 0 0 .76.35Zm1.56-4c-.38 0-.72.14-.97.37-.24.23-.37.52-.37.81a1.69 1.69 0 0 0 .3.82H6v-.83c0-.29-.13-.58-.37-.8C5.4 4.14 5.04 4 4.67 4Zm5 13a3.58 3.58 0 0 1 0 3H19a2 2 0 0 0 2-2v-1H9.66ZM3.86 6.35ZM11 8a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5Zm-1 5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1Z"
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RestartIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
{...props}
|
||||||
|
className={classes(props.className, "vc-restart-icon")}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 0 1 14.93-4H15a1 1 0 1 0 0 2h6a1 1 0 0 0 1-1V3a1 1 0 1 0-2 0v3a9.98 9.98 0 0 0-18 6 10 10 0 0 0 16.29 7.78 1 1 0 0 0-1.26-1.56A8 8 0 0 1 4 12Z"
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PaintbrushIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
{...props}
|
||||||
|
className={classes(props.className, "vc-paintbrush-icon")}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M15.35 7.24C15.9 6.67 16 5.8 16 5a3 3 0 1 1 3 3c-.8 0-1.67.09-2.24.65a1.5 1.5 0 0 0 0 2.11l1.12 1.12a3 3 0 0 1 0 4.24l-5 5a3 3 0 0 1-4.25 0l-5.76-5.75a3 3 0 0 1 0-4.24l4.04-4.04.97-.97a3 3 0 0 1 4.24 0l1.12 1.12c.58.58 1.52.58 2.1 0ZM6.9 9.9 4.3 12.54a1 1 0 0 0 0 1.42l2.17 2.17.83-.84a1 1 0 0 1 1.42 1.42l-.84.83.59.59 1.83-1.84a1 1 0 0 1 1.42 1.42l-1.84 1.83.17.17a1 1 0 0 0 1.42 0l2.63-2.62L6.9 9.9Z"
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
|
||||||
|
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
|
||||||
|
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
|
||||||
|
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
|
||||||
|
|
||||||
|
export function GithubIcon(props: ImageProps) {
|
||||||
|
const src = getTheme() === Theme.Light
|
||||||
|
? GithubIconLight
|
||||||
|
: GithubIconDark;
|
||||||
|
|
||||||
|
return <img {...props} src={src} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WebsiteIcon(props: ImageProps) {
|
||||||
|
const src = getTheme() === Theme.Light
|
||||||
|
? WebsiteIconLight
|
||||||
|
: WebsiteIconDark;
|
||||||
|
|
||||||
|
return <img {...props} src={src} />;
|
||||||
|
}
|
||||||
|
|
|
@ -11,20 +11,16 @@ import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { DevsById } from "@utils/constants";
|
import { DevsById } from "@utils/constants";
|
||||||
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
|
import { fetchUserProfile } from "@utils/discord";
|
||||||
import { pluralise } from "@utils/misc";
|
import { classes, pluralise } from "@utils/misc";
|
||||||
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
||||||
import { Forms, MaskedLink, showToast, Tooltip, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
import { PluginCard } from ".";
|
import { PluginCard } from ".";
|
||||||
|
import { GithubButton, WebsiteButton } from "./LinkIconButton";
|
||||||
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
|
|
||||||
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
|
|
||||||
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
|
|
||||||
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
|
|
||||||
|
|
||||||
const cl = classNameFactory("vc-author-modal-");
|
const cl = classNameFactory("vc-author-modal-");
|
||||||
|
|
||||||
|
@ -40,16 +36,6 @@ export function openContributorModal(user: User) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GithubIcon() {
|
|
||||||
const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark;
|
|
||||||
return <img src={src} alt="GitHub" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function WebsiteIcon() {
|
|
||||||
const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark;
|
|
||||||
return <img src={src} alt="Website" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContributorModal({ user }: { user: User; }) {
|
function ContributorModal({ user }: { user: User; }) {
|
||||||
useSettings();
|
useSettings();
|
||||||
|
|
||||||
|
@ -86,24 +72,18 @@ function ContributorModal({ user }: { user: User; }) {
|
||||||
/>
|
/>
|
||||||
<Forms.FormTitle tag="h2" className={cl("name")}>{user.username}</Forms.FormTitle>
|
<Forms.FormTitle tag="h2" className={cl("name")}>{user.username}</Forms.FormTitle>
|
||||||
|
|
||||||
<div className={cl("links")}>
|
<div className={classes("vc-settings-modal-links", cl("links"))}>
|
||||||
{website && (
|
{website && (
|
||||||
<Tooltip text={website}>
|
<WebsiteButton
|
||||||
{props => (
|
text={website}
|
||||||
<MaskedLink {...props} href={"https://" + website}>
|
href={`https://${website}`}
|
||||||
<WebsiteIcon />
|
/>
|
||||||
</MaskedLink>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
{githubName && (
|
{githubName && (
|
||||||
<Tooltip text={githubName}>
|
<GithubButton
|
||||||
{props => (
|
text={githubName}
|
||||||
<MaskedLink {...props} href={`https://github.com/${githubName}`}>
|
href={`https://github.com/${githubName}`}
|
||||||
<GithubIcon />
|
/>
|
||||||
</MaskedLink>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
12
src/components/PluginSettings/LinkIconButton.css
Normal file
12
src/components/PluginSettings/LinkIconButton.css
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.vc-settings-modal-link-icon {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 4px solid var(--background-tertiary);
|
||||||
|
box-sizing: border-box
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-settings-modal-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.2em;
|
||||||
|
}
|
39
src/components/PluginSettings/LinkIconButton.tsx
Normal file
39
src/components/PluginSettings/LinkIconButton.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./LinkIconButton.css";
|
||||||
|
|
||||||
|
import { MaskedLink, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
|
import { GithubIcon, WebsiteIcon } from "..";
|
||||||
|
|
||||||
|
export function GithubLinkIcon() {
|
||||||
|
return <GithubIcon aria-hidden className={"vc-settings-modal-link-icon"} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WebsiteLinkIcon() {
|
||||||
|
return <WebsiteIcon aria-hidden className={"vc-settings-modal-link-icon"} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinkIcon({ text, href, Icon }: Props & { Icon: React.ComponentType; }) {
|
||||||
|
return (
|
||||||
|
<Tooltip text={text}>
|
||||||
|
{props => (
|
||||||
|
<MaskedLink {...props} href={href}>
|
||||||
|
<Icon />
|
||||||
|
</MaskedLink>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WebsiteButton = (props: Props) => <LinkIcon {...props} Icon={WebsiteLinkIcon} />;
|
||||||
|
export const GithubButton = (props: Props) => <LinkIcon {...props} Icon={GithubLinkIcon} />;
|
7
src/components/PluginSettings/PluginModal.css
Normal file
7
src/components/PluginSettings/PluginModal.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.vc-plugin-modal-info {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-plugin-modal-description {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
|
@ -16,20 +16,26 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import "./PluginModal.css";
|
||||||
|
|
||||||
import { generateId } from "@api/Commands";
|
import { generateId } from "@api/Commands";
|
||||||
import { useSettings } from "@api/Settings";
|
import { useSettings } from "@api/Settings";
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
|
import { gitRemote } from "@shared/vencordUserAgent";
|
||||||
import { proxyLazy } from "@utils/lazy";
|
import { proxyLazy } from "@utils/lazy";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes, isObjectEmpty } from "@utils/misc";
|
import { classes, isObjectEmpty } from "@utils/misc";
|
||||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import { OptionType, Plugin } from "@utils/types";
|
import { OptionType, Plugin } from "@utils/types";
|
||||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||||
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
import { Constructor } from "type-fest";
|
import { Constructor } from "type-fest";
|
||||||
|
|
||||||
|
import { PluginMeta } from "~plugins";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ISettingElementProps,
|
ISettingElementProps,
|
||||||
SettingBooleanComponent,
|
SettingBooleanComponent,
|
||||||
|
@ -40,6 +46,9 @@ import {
|
||||||
SettingTextComponent
|
SettingTextComponent
|
||||||
} from "./components";
|
} from "./components";
|
||||||
import { openContributorModal } from "./ContributorModal";
|
import { openContributorModal } from "./ContributorModal";
|
||||||
|
import { GithubButton, WebsiteButton } from "./LinkIconButton";
|
||||||
|
|
||||||
|
const cl = classNameFactory("vc-plugin-modal-");
|
||||||
|
|
||||||
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
|
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
|
||||||
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
|
||||||
|
@ -180,16 +189,54 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
function switchToPopout() {
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
const PopoutKey = `DISCORD_VENCORD_PLUGIN_SETTINGS_MODAL_${plugin.name}`;
|
||||||
|
PopoutActions.open(
|
||||||
|
PopoutKey,
|
||||||
|
() => <PluginModal
|
||||||
|
transitionState={transitionState}
|
||||||
|
plugin={plugin}
|
||||||
|
onRestartNeeded={onRestartNeeded}
|
||||||
|
onClose={() => PopoutActions.close(PopoutKey)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const pluginMeta = PluginMeta[plugin.name];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM} className="vc-text-selectable">
|
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM} className="vc-text-selectable">
|
||||||
<ModalHeader separator={false}>
|
<ModalHeader separator={false}>
|
||||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
|
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
<Button look={Button.Looks.BLANK} onClick={switchToPopout}>
|
||||||
|
<OpenExternalIcon aria-label="Open in Popout" />
|
||||||
|
</Button>
|
||||||
|
*/}
|
||||||
<ModalCloseButton onClick={onClose} />
|
<ModalCloseButton onClick={onClose} />
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
|
<Flex className={cl("info")}>
|
||||||
<Forms.FormText>{plugin.description}</Forms.FormText>
|
<Forms.FormText className={cl("description")}>{plugin.description}</Forms.FormText>
|
||||||
|
{!pluginMeta.userPlugin && (
|
||||||
|
<div className="vc-settings-modal-links">
|
||||||
|
<WebsiteButton
|
||||||
|
text="View more info"
|
||||||
|
href={`https://vencord.dev/plugins/${plugin.name}`}
|
||||||
|
/>
|
||||||
|
<GithubButton
|
||||||
|
text="View source code"
|
||||||
|
href={`https://github.com/${gitRemote}/tree/main/src/plugins/${pluginMeta.folderName}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
<Forms.FormTitle tag="h3" style={{ marginTop: 8, marginBottom: 0 }}>Authors</Forms.FormTitle>
|
<Forms.FormTitle tag="h3" style={{ marginTop: 8, marginBottom: 0 }}>Authors</Forms.FormTitle>
|
||||||
<div style={{ width: "fit-content", marginBottom: 8 }}>
|
<div style={{ width: "fit-content", marginBottom: 8 }}>
|
||||||
<UserSummaryItem
|
<UserSummaryItem
|
||||||
|
@ -263,3 +310,13 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openPluginModal(plugin: Plugin, onRestartNeeded?: (pluginName: string) => void) {
|
||||||
|
openModal(modalProps => (
|
||||||
|
<PluginModal
|
||||||
|
{...modalProps}
|
||||||
|
plugin={plugin}
|
||||||
|
onRestartNeeded={() => onRestartNeeded?.(plugin.name)}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
|
@ -42,16 +42,6 @@
|
||||||
|
|
||||||
.vc-author-modal-links {
|
.vc-author-modal-links {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
display: flex;
|
|
||||||
gap: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-author-modal-links img {
|
|
||||||
height: 32px;
|
|
||||||
width: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 4px solid var(--background-tertiary);
|
|
||||||
box-sizing: border-box
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-author-modal-plugins {
|
.vc-author-modal-plugins {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { showNotice } from "@api/Notices";
|
||||||
import { Settings, useSettings } from "@api/Settings";
|
import { Settings, useSettings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { CogWheel, InfoIcon } from "@components/Icons";
|
import { CogWheel, InfoIcon } from "@components/Icons";
|
||||||
import PluginModal from "@components/PluginSettings/PluginModal";
|
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||||
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
||||||
import { SettingsTab } from "@components/VencordSettings/shared";
|
import { SettingsTab } from "@components/VencordSettings/shared";
|
||||||
import { ChangeList } from "@utils/ChangeList";
|
import { ChangeList } from "@utils/ChangeList";
|
||||||
|
@ -31,13 +31,12 @@ import { proxyLazy } from "@utils/lazy";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes, isObjectEmpty } from "@utils/misc";
|
import { classes, isObjectEmpty } from "@utils/misc";
|
||||||
import { openModalLazy } from "@utils/modal";
|
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import { Plugin } from "@utils/types";
|
import { Plugin } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip, useMemo } from "@webpack/common";
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins, { ExcludedPlugins } from "~plugins";
|
||||||
|
|
||||||
// Avoid circular dependency
|
// Avoid circular dependency
|
||||||
const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() => require("../../plugins"));
|
const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() => require("../../plugins"));
|
||||||
|
@ -45,7 +44,7 @@ const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() =>
|
||||||
const cl = classNameFactory("vc-plugins-");
|
const cl = classNameFactory("vc-plugins-");
|
||||||
const logger = new Logger("PluginSettings", "#a6d189");
|
const logger = new Logger("PluginSettings", "#a6d189");
|
||||||
|
|
||||||
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
|
const InputStyles = findByPropsLazy("inputWrapper", "inputDefault", "error");
|
||||||
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
|
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,7 +68,7 @@ function ReloadRequiredCard({ required }: { required: boolean; }) {
|
||||||
<Forms.FormText className={cl("dep-text")}>
|
<Forms.FormText className={cl("dep-text")}>
|
||||||
Restart now to apply new plugins and their settings
|
Restart now to apply new plugins and their settings
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Button color={Button.Colors.YELLOW} onClick={() => location.reload()}>
|
<Button onClick={() => location.reload()}>
|
||||||
Restart
|
Restart
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
@ -96,14 +95,6 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
|
||||||
|
|
||||||
const isEnabled = () => settings.enabled ?? false;
|
const isEnabled = () => settings.enabled ?? false;
|
||||||
|
|
||||||
function openModal() {
|
|
||||||
openModalLazy(async () => {
|
|
||||||
return modalProps => {
|
|
||||||
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleEnabled() {
|
function toggleEnabled() {
|
||||||
const wasEnabled = isEnabled();
|
const wasEnabled = isEnabled();
|
||||||
|
|
||||||
|
@ -160,7 +151,11 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
infoButton={
|
infoButton={
|
||||||
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
|
<button
|
||||||
|
role="switch"
|
||||||
|
onClick={() => openPluginModal(plugin, onRestartNeeded)}
|
||||||
|
className={classes(ButtonClasses.button, cl("info-button"))}
|
||||||
|
>
|
||||||
{plugin.options && !isObjectEmpty(plugin.options)
|
{plugin.options && !isObjectEmpty(plugin.options)
|
||||||
? <CogWheel />
|
? <CogWheel />
|
||||||
: <InfoIcon />}
|
: <InfoIcon />}
|
||||||
|
@ -177,6 +172,37 @@ const enum SearchStatus {
|
||||||
NEW
|
NEW
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ExcludedPluginsList({ search }: { search: string; }) {
|
||||||
|
const matchingExcludedPlugins = Object.entries(ExcludedPlugins)
|
||||||
|
.filter(([name]) => name.toLowerCase().includes(search));
|
||||||
|
|
||||||
|
const ExcludedReasons: Record<"web" | "discordDesktop" | "vencordDesktop" | "desktop" | "dev", string> = {
|
||||||
|
desktop: "Discord Desktop app or Vesktop",
|
||||||
|
discordDesktop: "Discord Desktop app",
|
||||||
|
vencordDesktop: "Vesktop app",
|
||||||
|
web: "Vesktop app and the Web version of Discord",
|
||||||
|
dev: "Developer version of Vencord"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text variant="text-md/normal" className={Margins.top16}>
|
||||||
|
{matchingExcludedPlugins.length
|
||||||
|
? <>
|
||||||
|
<Forms.FormText>Are you looking for:</Forms.FormText>
|
||||||
|
<ul>
|
||||||
|
{matchingExcludedPlugins.map(([name, reason]) => (
|
||||||
|
<li key={name}>
|
||||||
|
<b>{name}</b>: Only available on the {ExcludedReasons[reason]}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
: "No plugins meet the search criteria."
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function PluginSettings() {
|
export default function PluginSettings() {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
const changes = React.useMemo(() => new ChangeList<string>(), []);
|
||||||
|
@ -215,26 +241,27 @@ export default function PluginSettings() {
|
||||||
return o;
|
return o;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
|
const sortedPlugins = useMemo(() => Object.values(Plugins)
|
||||||
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL });
|
const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL });
|
||||||
|
|
||||||
|
const search = searchValue.value.toLowerCase();
|
||||||
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
|
||||||
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
|
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
|
||||||
|
|
||||||
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
|
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
|
||||||
const enabled = settings.plugins[plugin.name]?.enabled;
|
const { status } = searchValue;
|
||||||
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
|
const enabled = Vencord.Plugins.isPluginEnabled(plugin.name);
|
||||||
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
|
if (enabled && status === SearchStatus.DISABLED) return false;
|
||||||
if (searchValue.status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false;
|
if (!enabled && status === SearchStatus.ENABLED) return false;
|
||||||
if (!searchValue.value.length) return true;
|
if (status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false;
|
||||||
|
if (!search.length) return true;
|
||||||
|
|
||||||
const v = searchValue.value.toLowerCase();
|
|
||||||
return (
|
return (
|
||||||
plugin.name.toLowerCase().includes(v) ||
|
plugin.name.toLowerCase().includes(search) ||
|
||||||
plugin.description.toLowerCase().includes(v) ||
|
plugin.description.toLowerCase().includes(search) ||
|
||||||
plugin.tags?.some(t => t.toLowerCase().includes(v))
|
plugin.tags?.some(t => t.toLowerCase().includes(search))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -255,14 +282,12 @@ export default function PluginSettings() {
|
||||||
return lodash.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
|
return lodash.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type P = JSX.Element | JSX.Element[];
|
const plugins = [] as JSX.Element[];
|
||||||
let plugins: P, requiredPlugins: P;
|
const requiredPlugins = [] as JSX.Element[];
|
||||||
if (sortedPlugins?.length) {
|
|
||||||
plugins = [];
|
|
||||||
requiredPlugins = [];
|
|
||||||
|
|
||||||
|
const showApi = searchValue.value.includes("API");
|
||||||
for (const p of sortedPlugins) {
|
for (const p of sortedPlugins) {
|
||||||
if (!p.options && p.name.endsWith("API") && searchValue.value !== "API")
|
if (p.hidden || (!p.options && p.name.endsWith("API") && !showApi))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!pluginFilter(p)) continue;
|
if (!pluginFilter(p)) continue;
|
||||||
|
@ -283,6 +308,7 @@ export default function PluginSettings() {
|
||||||
onRestartNeeded={name => changes.handleChange(name)}
|
onRestartNeeded={name => changes.handleChange(name)}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
plugin={p}
|
plugin={p}
|
||||||
|
key={p.name}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -298,10 +324,6 @@ export default function PluginSettings() {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
plugins = requiredPlugins = <Text variant="text-md/normal">No plugins meet search criteria.</Text>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -312,8 +334,8 @@ export default function PluginSettings() {
|
||||||
Filters
|
Filters
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
|
|
||||||
<div className={cl("filter-controls")}>
|
<div className={classes(Margins.bottom20, 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} />
|
||||||
<div className={InputStyles.inputWrapper}>
|
<div className={InputStyles.inputWrapper}>
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={[
|
||||||
|
@ -326,15 +348,25 @@ export default function PluginSettings() {
|
||||||
select={onStatusChange}
|
select={onStatusChange}
|
||||||
isSelected={v => v === searchValue.status}
|
isSelected={v => v === searchValue.status}
|
||||||
closeOnSelect={true}
|
closeOnSelect={true}
|
||||||
|
className={InputStyles.inputDefault}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
|
||||||
|
|
||||||
|
{plugins.length || requiredPlugins.length
|
||||||
|
? (
|
||||||
<div className={cl("grid")}>
|
<div className={cl("grid")}>
|
||||||
{plugins}
|
{plugins.length
|
||||||
|
? plugins
|
||||||
|
: <Text variant="text-md/normal">No plugins meet the search criteria.</Text>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
: <ExcludedPluginsList search={search} />
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
<Forms.FormDivider className={Margins.top20} />
|
<Forms.FormDivider className={Margins.top20} />
|
||||||
|
|
||||||
|
@ -342,7 +374,10 @@ export default function PluginSettings() {
|
||||||
Required Plugins
|
Required Plugins
|
||||||
</Forms.FormTitle>
|
</Forms.FormTitle>
|
||||||
<div className={cl("grid")}>
|
<div className={cl("grid")}>
|
||||||
{requiredPlugins}
|
{requiredPlugins.length
|
||||||
|
? requiredPlugins
|
||||||
|
: <Text variant="text-md/normal">No plugins meet the search criteria.</Text>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</SettingsTab >
|
</SettingsTab >
|
||||||
);
|
);
|
||||||
|
|
|
@ -78,6 +78,7 @@
|
||||||
|
|
||||||
.vc-plugins-restart-card button {
|
.vc-plugins-restart-card button {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
|
background: var(--info-warning-foreground) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-plugins-info-button svg:not(:hover, :focus) {
|
.vc-plugins-info-button svg:not(:hover, :focus) {
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
import { showNotification } from "@api/Notifications";
|
import { showNotification } from "@api/Notifications";
|
||||||
import { Settings, useSettings } from "@api/Settings";
|
import { Settings, useSettings } from "@api/Settings";
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||||
|
import { Grid } from "@components/Grid";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
|
@ -85,7 +86,9 @@ function SettingsSyncSection() {
|
||||||
size={Button.Sizes.SMALL}
|
size={Button.Sizes.SMALL}
|
||||||
disabled={!sectionEnabled}
|
disabled={!sectionEnabled}
|
||||||
onClick={() => putCloudSettings(true)}
|
onClick={() => putCloudSettings(true)}
|
||||||
>Sync to Cloud</Button>
|
>
|
||||||
|
Sync to Cloud
|
||||||
|
</Button>
|
||||||
<Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!">
|
<Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!">
|
||||||
{({ onMouseLeave, onMouseEnter }) => (
|
{({ onMouseLeave, onMouseEnter }) => (
|
||||||
<Button
|
<Button
|
||||||
|
@ -95,7 +98,9 @@ function SettingsSyncSection() {
|
||||||
color={Button.Colors.RED}
|
color={Button.Colors.RED}
|
||||||
disabled={!sectionEnabled}
|
disabled={!sectionEnabled}
|
||||||
onClick={() => getCloudSettings(true, true)}
|
onClick={() => getCloudSettings(true, true)}
|
||||||
>Sync from Cloud</Button>
|
>
|
||||||
|
Sync from Cloud
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Button
|
<Button
|
||||||
|
@ -103,7 +108,9 @@ function SettingsSyncSection() {
|
||||||
color={Button.Colors.RED}
|
color={Button.Colors.RED}
|
||||||
disabled={!sectionEnabled}
|
disabled={!sectionEnabled}
|
||||||
onClick={() => deleteCloudSettings()}
|
onClick={() => deleteCloudSettings()}
|
||||||
>Delete Cloud Settings</Button>
|
>
|
||||||
|
Delete Cloud Settings
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
);
|
);
|
||||||
|
@ -124,7 +131,12 @@ function CloudTab() {
|
||||||
<Switch
|
<Switch
|
||||||
key="backend"
|
key="backend"
|
||||||
value={settings.cloud.authenticated}
|
value={settings.cloud.authenticated}
|
||||||
onChange={v => { v && authorizeCloud(); if (!v) settings.cloud.authenticated = v; }}
|
onChange={v => {
|
||||||
|
if (v)
|
||||||
|
authorizeCloud();
|
||||||
|
else
|
||||||
|
settings.cloud.authenticated = v;
|
||||||
|
}}
|
||||||
note="This will request authorization if you have not yet set up cloud integrations."
|
note="This will request authorization if you have not yet set up cloud integrations."
|
||||||
>
|
>
|
||||||
Enable Cloud Integrations
|
Enable Cloud Integrations
|
||||||
|
@ -136,11 +148,27 @@ function CloudTab() {
|
||||||
<CheckedTextInput
|
<CheckedTextInput
|
||||||
key="backendUrl"
|
key="backendUrl"
|
||||||
value={settings.cloud.url}
|
value={settings.cloud.url}
|
||||||
onChange={v => { settings.cloud.url = v; settings.cloud.authenticated = false; deauthorizeCloud(); }}
|
onChange={async v => {
|
||||||
|
settings.cloud.url = v;
|
||||||
|
settings.cloud.authenticated = false;
|
||||||
|
deauthorizeCloud();
|
||||||
|
}}
|
||||||
validate={validateUrl}
|
validate={validateUrl}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Grid columns={2} gap="1em" className={Margins.top8}>
|
||||||
|
<Button
|
||||||
|
size={Button.Sizes.MEDIUM}
|
||||||
|
disabled={!settings.cloud.authenticated}
|
||||||
|
onClick={async () => {
|
||||||
|
await deauthorizeCloud();
|
||||||
|
settings.cloud.authenticated = false;
|
||||||
|
await authorizeCloud();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reauthorise
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={Margins.top8}
|
|
||||||
size={Button.Sizes.MEDIUM}
|
size={Button.Sizes.MEDIUM}
|
||||||
color={Button.Colors.RED}
|
color={Button.Colors.RED}
|
||||||
disabled={!settings.cloud.authenticated}
|
disabled={!settings.cloud.authenticated}
|
||||||
|
@ -152,7 +180,11 @@ function CloudTab() {
|
||||||
confirmColor: "vc-cloud-erase-data-danger-btn",
|
confirmColor: "vc-cloud-erase-data-danger-btn",
|
||||||
cancelText: "Nevermind"
|
cancelText: "Nevermind"
|
||||||
})}
|
})}
|
||||||
>Erase All Data</Button>
|
>
|
||||||
|
Erase All Data
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Forms.FormDivider className={Margins.top16} />
|
<Forms.FormDivider className={Margins.top16} />
|
||||||
</Forms.FormSection >
|
</Forms.FormSection >
|
||||||
<SettingsSyncSection />
|
<SettingsSyncSection />
|
||||||
|
|
106
src/components/VencordSettings/NotificationSettings.tsx
Normal file
106
src/components/VencordSettings/NotificationSettings.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useSettings } from "@api/Settings";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { identity } from "@utils/misc";
|
||||||
|
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
|
import { Forms, Select, Slider, Text } from "@webpack/common";
|
||||||
|
|
||||||
|
import { ErrorCard } from "..";
|
||||||
|
|
||||||
|
export function NotificationSettings() {
|
||||||
|
const settings = useSettings().notifications;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "1em 0" }}>
|
||||||
|
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
|
||||||
|
{settings.useNative !== "never" && Notification?.permission === "denied" && (
|
||||||
|
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
|
||||||
|
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
|
||||||
|
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
|
||||||
|
</ErrorCard>
|
||||||
|
)}
|
||||||
|
<Forms.FormText className={Margins.bottom8}>
|
||||||
|
Some plugins may show you notifications. These come in two styles:
|
||||||
|
<ul>
|
||||||
|
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
|
||||||
|
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
|
||||||
|
</ul>
|
||||||
|
</Forms.FormText>
|
||||||
|
<Select
|
||||||
|
placeholder="Notification Style"
|
||||||
|
options={[
|
||||||
|
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
|
||||||
|
{ label: "Always use Desktop notifications", value: "always" },
|
||||||
|
{ label: "Always use Vencord notifications", value: "never" },
|
||||||
|
] satisfies Array<{ value: typeof settings["useNative"]; } & Record<string, any>>}
|
||||||
|
closeOnSelect={true}
|
||||||
|
select={v => settings.useNative = v}
|
||||||
|
isSelected={v => v === settings.useNative}
|
||||||
|
serialize={identity}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
|
||||||
|
<Select
|
||||||
|
isDisabled={settings.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}
|
||||||
|
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"}
|
||||||
|
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
|
||||||
|
minValue={0}
|
||||||
|
maxValue={20_000}
|
||||||
|
initialValue={settings.timeout}
|
||||||
|
onValueChange={v => settings.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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openNotificationSettingsModal() {
|
||||||
|
openModal(props => (
|
||||||
|
<ModalRoot {...props} size={ModalSize.MEDIUM}>
|
||||||
|
<ModalHeader>
|
||||||
|
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Settings</Text>
|
||||||
|
<ModalCloseButton onClick={props.onClose} />
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalContent>
|
||||||
|
<NotificationSettings />
|
||||||
|
</ModalContent>
|
||||||
|
</ModalRoot>
|
||||||
|
));
|
||||||
|
}
|
|
@ -16,7 +16,6 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
|
||||||
import { CodeBlock } from "@components/CodeBlock";
|
import { CodeBlock } from "@components/CodeBlock";
|
||||||
import { debounce } from "@shared/debounce";
|
import { debounce } from "@shared/debounce";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
|
@ -47,7 +46,7 @@ const findCandidates = debounce(function ({ find, setModule, setError }) {
|
||||||
|
|
||||||
interface ReplacementComponentProps {
|
interface ReplacementComponentProps {
|
||||||
module: [id: number, factory: Function];
|
module: [id: number, factory: Function];
|
||||||
match: string | RegExp;
|
match: string;
|
||||||
replacement: string | ReplaceFn;
|
replacement: string | ReplaceFn;
|
||||||
setReplacementError(error: any): void;
|
setReplacementError(error: any): void;
|
||||||
}
|
}
|
||||||
|
@ -58,7 +57,13 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
||||||
|
|
||||||
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
||||||
const src: string = fact.toString().replaceAll("\n", "");
|
const src: string = fact.toString().replaceAll("\n", "");
|
||||||
const canonicalMatch = canonicalizeMatch(match);
|
|
||||||
|
try {
|
||||||
|
new RegExp(match);
|
||||||
|
} catch (e) {
|
||||||
|
return ["", [], []];
|
||||||
|
}
|
||||||
|
const canonicalMatch = canonicalizeMatch(new RegExp(match));
|
||||||
try {
|
try {
|
||||||
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
||||||
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
||||||
|
@ -286,6 +291,7 @@ function PatchHelper() {
|
||||||
|
|
||||||
const [module, setModule] = React.useState<[number, Function]>();
|
const [module, setModule] = React.useState<[number, Function]>();
|
||||||
const [findError, setFindError] = React.useState<string>();
|
const [findError, setFindError] = React.useState<string>();
|
||||||
|
const [matchError, setMatchError] = React.useState<string>();
|
||||||
|
|
||||||
const code = React.useMemo(() => {
|
const code = React.useMemo(() => {
|
||||||
return `
|
return `
|
||||||
|
@ -300,20 +306,16 @@ function PatchHelper() {
|
||||||
}, [parsedFind, match, replacement]);
|
}, [parsedFind, match, replacement]);
|
||||||
|
|
||||||
function onFindChange(v: string) {
|
function onFindChange(v: string) {
|
||||||
setFindError(void 0);
|
|
||||||
setFind(v);
|
setFind(v);
|
||||||
}
|
|
||||||
|
|
||||||
function onFindBlur() {
|
|
||||||
try {
|
try {
|
||||||
let parsedFind = find as string | RegExp;
|
let parsedFind = v as string | RegExp;
|
||||||
if (/^\/.+?\/$/.test(find)) parsedFind = new RegExp(find.slice(1, -1));
|
if (/^\/.+?\/$/.test(v)) parsedFind = new RegExp(v.slice(1, -1));
|
||||||
|
|
||||||
setFindError(void 0);
|
setFindError(void 0);
|
||||||
setFind(find);
|
|
||||||
setParsedFind(parsedFind);
|
setParsedFind(parsedFind);
|
||||||
|
|
||||||
if (find.length) {
|
if (v.length) {
|
||||||
findCandidates({ find: parsedFind, setModule, setError: setFindError });
|
findCandidates({ find: parsedFind, setModule, setError: setFindError });
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -322,12 +324,13 @@ function PatchHelper() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMatchChange(v: string) {
|
function onMatchChange(v: string) {
|
||||||
|
setMatch(v);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
new RegExp(v);
|
new RegExp(v);
|
||||||
setFindError(void 0);
|
setMatchError(void 0);
|
||||||
setMatch(v);
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setFindError((e as Error).message);
|
setMatchError((e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,21 +349,15 @@ function PatchHelper() {
|
||||||
type="text"
|
type="text"
|
||||||
value={find}
|
value={find}
|
||||||
onChange={onFindChange}
|
onChange={onFindChange}
|
||||||
onBlur={onFindBlur}
|
|
||||||
error={findError}
|
error={findError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
|
||||||
<CheckedTextInput
|
<TextInput
|
||||||
|
type="text"
|
||||||
value={match}
|
value={match}
|
||||||
onChange={onMatchChange}
|
onChange={onMatchChange}
|
||||||
validate={v => {
|
error={matchError}
|
||||||
try {
|
|
||||||
return (new RegExp(v), true);
|
|
||||||
} catch (e) {
|
|
||||||
return (e as Error).message;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={Margins.top8} />
|
<div className={Margins.top8} />
|
||||||
|
@ -374,7 +371,7 @@ function PatchHelper() {
|
||||||
{module && (
|
{module && (
|
||||||
<ReplacementComponent
|
<ReplacementComponent
|
||||||
module={module}
|
module={module}
|
||||||
match={new RegExp(match)}
|
match={match}
|
||||||
replacement={replacement}
|
replacement={replacement}
|
||||||
setReplacementError={setReplacementError}
|
setReplacementError={setReplacementError}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -17,16 +17,19 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
||||||
import { Settings, useSettings } from "@api/Settings";
|
import { useSettings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import DonateButton from "@components/DonateButton";
|
import DonateButton from "@components/DonateButton";
|
||||||
import { ErrorCard } from "@components/ErrorCard";
|
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { identity } from "@utils/misc";
|
import { identity } from "@utils/misc";
|
||||||
import { relaunch, showItemInFolder } from "@utils/native";
|
import { relaunch, showItemInFolder } from "@utils/native";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
|
import { Button, Card, Forms, React, Select, Switch, TooltipContainer } from "@webpack/common";
|
||||||
|
import { ComponentType } from "react";
|
||||||
|
|
||||||
|
import { Flex, FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "..";
|
||||||
|
import { openNotificationSettingsModal } from "./NotificationSettings";
|
||||||
import { SettingsTab, wrapTab } from "./shared";
|
import { SettingsTab, wrapTab } from "./shared";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-settings-");
|
const cl = classNameFactory("vc-settings-");
|
||||||
|
@ -38,6 +41,18 @@ type KeysOfType<Object, Type> = {
|
||||||
[K in keyof Object]: Object[K] extends Type ? K : never;
|
[K in keyof Object]: Object[K] extends Type ? K : never;
|
||||||
}[keyof Object];
|
}[keyof Object];
|
||||||
|
|
||||||
|
const iconWithTooltip = (Icon: ComponentType<{ className?: string; }>, tooltip: string) => () => (
|
||||||
|
<TooltipContainer text={tooltip}>
|
||||||
|
<Icon className={cl("quick-actions-img")} />
|
||||||
|
</TooltipContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
const NotificationLogIcon = iconWithTooltip(LogIcon, "Open Notification Log");
|
||||||
|
const QuickCssIcon = iconWithTooltip(PaintbrushIcon, "Edit QuickCSS");
|
||||||
|
const RelaunchIcon = iconWithTooltip(RestartIcon, "Relaunch Discord");
|
||||||
|
const OpenSettingsDirIcon = iconWithTooltip(FolderIcon, "Open Settings Directory");
|
||||||
|
const OpenGithubIcon = iconWithTooltip(GithubIcon, "View Vencord's GitHub Repository");
|
||||||
|
|
||||||
function VencordSettings() {
|
function VencordSettings() {
|
||||||
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
|
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
|
||||||
fallbackValue: "Loading..."
|
fallbackValue: "Loading..."
|
||||||
|
@ -78,7 +93,7 @@ function VencordSettings() {
|
||||||
!IS_WEB && {
|
!IS_WEB && {
|
||||||
key: "transparent",
|
key: "transparent",
|
||||||
title: "Enable window transparency.",
|
title: "Enable window transparency.",
|
||||||
note: "You need a theme that supports transparency or this will do nothing. Will stop the window from being resizable. Requires a full restart"
|
note: "You need a theme that supports transparency or this will do nothing. WILL STOP THE WINDOW FROM BEING RESIZABLE!! Requires a full restart"
|
||||||
},
|
},
|
||||||
!IS_WEB && isWindows && {
|
!IS_WEB && isWindows && {
|
||||||
key: "winCtrlQ",
|
key: "winCtrlQ",
|
||||||
|
@ -97,44 +112,59 @@ function VencordSettings() {
|
||||||
<DonateCard image={donateImage} />
|
<DonateCard image={donateImage} />
|
||||||
<Forms.FormSection title="Quick Actions">
|
<Forms.FormSection title="Quick Actions">
|
||||||
<Card className={cl("quick-actions-card")}>
|
<Card className={cl("quick-actions-card")}>
|
||||||
<React.Fragment>
|
<Button
|
||||||
|
onClick={openNotificationLogModal}
|
||||||
|
look={Button.Looks.BLANK}
|
||||||
|
>
|
||||||
|
<NotificationLogIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => VencordNative.quickCss.openEditor()}
|
||||||
|
look={Button.Looks.BLANK}
|
||||||
|
>
|
||||||
|
<QuickCssIcon />
|
||||||
|
</Button>
|
||||||
{!IS_WEB && (
|
{!IS_WEB && (
|
||||||
<Button
|
<Button
|
||||||
onClick={relaunch}
|
onClick={relaunch}
|
||||||
size={Button.Sizes.SMALL}>
|
look={Button.Looks.BLANK}
|
||||||
Restart Client
|
>
|
||||||
|
<RelaunchIcon />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
|
||||||
onClick={() => VencordNative.quickCss.openEditor()}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
disabled={settingsDir === "Loading..."}>
|
|
||||||
Open QuickCSS File
|
|
||||||
</Button>
|
|
||||||
{!IS_WEB && (
|
{!IS_WEB && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => showItemInFolder(settingsDir)}
|
onClick={() => showItemInFolder(settingsDir)}
|
||||||
size={Button.Sizes.SMALL}
|
look={Button.Looks.BLANK}
|
||||||
disabled={settingsDirPending}>
|
disabled={settingsDirPending}
|
||||||
Open Settings Folder
|
>
|
||||||
|
<OpenSettingsDirIcon />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => VencordNative.native.openExternal("https://github.com/Vendicated/Vencord")}
|
onClick={() => VencordNative.native.openExternal("https://github.com/Vendicated/Vencord")}
|
||||||
size={Button.Sizes.SMALL}
|
look={Button.Looks.BLANK}
|
||||||
disabled={settingsDirPending}>
|
disabled={settingsDirPending}
|
||||||
Open in GitHub
|
>
|
||||||
|
<OpenGithubIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</React.Fragment>
|
|
||||||
</Card>
|
</Card>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
|
|
||||||
<Forms.FormDivider />
|
<Forms.FormDivider />
|
||||||
|
|
||||||
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
|
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
|
||||||
<Forms.FormText className={Margins.bottom20}>
|
<Forms.FormText className={Margins.bottom20} style={{ color: "var(--text-muted)" }}>
|
||||||
Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
|
Hint: You can change the position of this settings section in the
|
||||||
|
{" "}<Button
|
||||||
|
look={Button.Looks.BLANK}
|
||||||
|
style={{ color: "var(--text-link)", display: "inline-block" }}
|
||||||
|
onClick={() => openPluginModal(Vencord.Plugins.plugins.Settings)}
|
||||||
|
>
|
||||||
|
settings of the Settings plugin
|
||||||
|
</Button>!
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
|
|
||||||
{Switches.map(s => s && (
|
{Switches.map(s => s && (
|
||||||
<Switch
|
<Switch
|
||||||
key={s.key}
|
key={s.key}
|
||||||
|
@ -212,91 +242,17 @@ function VencordSettings() {
|
||||||
serialize={identity} />
|
serialize={identity} />
|
||||||
</>}
|
</>}
|
||||||
|
|
||||||
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />}
|
<Forms.FormSection className={Margins.top16} title="Vencord Notifications" tag="h5">
|
||||||
</SettingsTab>
|
<Flex>
|
||||||
);
|
<Button onClick={openNotificationSettingsModal}>
|
||||||
}
|
Notification Settings
|
||||||
|
|
||||||
function NotificationSection({ settings }: { settings: typeof Settings["notifications"]; }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
|
|
||||||
{settings.useNative !== "never" && Notification?.permission === "denied" && (
|
|
||||||
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
|
|
||||||
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
|
|
||||||
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
|
|
||||||
</ErrorCard>
|
|
||||||
)}
|
|
||||||
<Forms.FormText className={Margins.bottom8}>
|
|
||||||
Some plugins may show you notifications. These come in two styles:
|
|
||||||
<ul>
|
|
||||||
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
|
|
||||||
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
|
|
||||||
</ul>
|
|
||||||
</Forms.FormText>
|
|
||||||
<Select
|
|
||||||
placeholder="Notification Style"
|
|
||||||
options={[
|
|
||||||
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
|
|
||||||
{ label: "Always use Desktop notifications", value: "always" },
|
|
||||||
{ label: "Always use Vencord notifications", value: "never" },
|
|
||||||
] satisfies Array<{ value: typeof settings["useNative"]; } & Record<string, any>>}
|
|
||||||
closeOnSelect={true}
|
|
||||||
select={v => settings.useNative = v}
|
|
||||||
isSelected={v => v === settings.useNative}
|
|
||||||
serialize={identity}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
|
|
||||||
<Select
|
|
||||||
isDisabled={settings.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}
|
|
||||||
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"}
|
|
||||||
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
|
|
||||||
minValue={0}
|
|
||||||
maxValue={20_000}
|
|
||||||
initialValue={settings.timeout}
|
|
||||||
onValueChange={v => settings.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>
|
</Button>
|
||||||
</>
|
<Button onClick={openNotificationLogModal}>
|
||||||
|
View Notification Log
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Forms.FormSection>
|
||||||
|
</SettingsTab>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,16 +11,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-settings-quick-actions-card {
|
.vc-settings-quick-actions-card {
|
||||||
|
color: var(--text-normal);
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1em;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
flex-grow: 1;
|
gap: 1em;
|
||||||
flex-flow: row wrap;
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vc-settings-quick-actions-card button {
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-settings-quick-actions-img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.vc-settings-donate {
|
.vc-settings-donate {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
18
src/components/index.ts
Normal file
18
src/components/index.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./Badge";
|
||||||
|
export * from "./CheckedTextInput";
|
||||||
|
export * from "./CodeBlock";
|
||||||
|
export * from "./DonateButton";
|
||||||
|
export { default as ErrorBoundary } from "./ErrorBoundary";
|
||||||
|
export * from "./ErrorCard";
|
||||||
|
export * from "./ExpandableHeader";
|
||||||
|
export * from "./Flex";
|
||||||
|
export * from "./Heart";
|
||||||
|
export * from "./Icons";
|
||||||
|
export * from "./Link";
|
||||||
|
export * from "./Switch";
|
|
@ -18,14 +18,14 @@
|
||||||
|
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
|
|
||||||
if (IS_DEV) {
|
if (IS_DEV || IS_REPORTER) {
|
||||||
var traces = {} as Record<string, [number, any[]]>;
|
var traces = {} as Record<string, [number, any[]]>;
|
||||||
var logger = new Logger("Tracer", "#FFD166");
|
var logger = new Logger("Tracer", "#FFD166");
|
||||||
}
|
}
|
||||||
|
|
||||||
const noop = function () { };
|
const noop = function () { };
|
||||||
|
|
||||||
export const beginTrace = !IS_DEV ? noop :
|
export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
|
||||||
function beginTrace(name: string, ...args: any[]) {
|
function beginTrace(name: string, ...args: any[]) {
|
||||||
if (name in traces)
|
if (name in traces)
|
||||||
throw new Error(`Trace ${name} already exists!`);
|
throw new Error(`Trace ${name} already exists!`);
|
||||||
|
@ -33,7 +33,7 @@ export const beginTrace = !IS_DEV ? noop :
|
||||||
traces[name] = [performance.now(), args];
|
traces[name] = [performance.now(), args];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const finishTrace = !IS_DEV ? noop : function finishTrace(name: string) {
|
export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) {
|
||||||
const end = performance.now();
|
const end = performance.now();
|
||||||
|
|
||||||
const [start, args] = traces[name];
|
const [start, args] = traces[name];
|
||||||
|
@ -48,7 +48,7 @@ type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;
|
||||||
const noopTracer =
|
const noopTracer =
|
||||||
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
|
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
|
||||||
|
|
||||||
export const traceFunction = !IS_DEV
|
export const traceFunction = !(IS_DEV || IS_REPORTER)
|
||||||
? noopTracer
|
? noopTracer
|
||||||
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
|
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
|
||||||
return function (this: any, ...args: Parameters<F>) {
|
return function (this: any, ...args: Parameters<F>) {
|
||||||
|
|
169
src/debug/loadLazyChunks.ts
Normal file
169
src/debug/loadLazyChunks.ts
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
|
import { canonicalizeMatch } from "@utils/patches";
|
||||||
|
import * as Webpack from "@webpack";
|
||||||
|
import { wreq } from "@webpack";
|
||||||
|
|
||||||
|
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
|
||||||
|
|
||||||
|
export async function loadLazyChunks() {
|
||||||
|
try {
|
||||||
|
LazyChunkLoaderLogger.log("Loading all chunks...");
|
||||||
|
|
||||||
|
const validChunks = new Set<string>();
|
||||||
|
const invalidChunks = new Set<string>();
|
||||||
|
const deferredRequires = new Set<string>();
|
||||||
|
|
||||||
|
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
|
||||||
|
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
|
||||||
|
|
||||||
|
// True if resolved, false otherwise
|
||||||
|
const chunksSearchPromises = [] as Array<() => boolean>;
|
||||||
|
|
||||||
|
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
|
||||||
|
|
||||||
|
async function searchAndLoadLazyChunks(factoryCode: string) {
|
||||||
|
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
|
||||||
|
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
|
||||||
|
|
||||||
|
// Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
|
||||||
|
// the chunk containing the component
|
||||||
|
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
|
||||||
|
|
||||||
|
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
|
||||||
|
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
|
||||||
|
|
||||||
|
if (chunkIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let invalidChunkGroup = false;
|
||||||
|
|
||||||
|
for (const id of chunkIds) {
|
||||||
|
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
|
||||||
|
|
||||||
|
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(t => t.includes("importScripts("));
|
||||||
|
|
||||||
|
if (isWorkerAsset) {
|
||||||
|
invalidChunks.add(id);
|
||||||
|
invalidChunkGroup = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validChunks.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!invalidChunkGroup) {
|
||||||
|
validChunkGroups.add([chunkIds, entryPoint]);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Loads all found valid chunk groups
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(validChunkGroups)
|
||||||
|
.map(([chunkIds]) =>
|
||||||
|
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Requires the entry points for all valid chunk groups
|
||||||
|
for (const [, entryPoint] of validChunkGroups) {
|
||||||
|
try {
|
||||||
|
if (shouldForceDefer) {
|
||||||
|
deferredRequires.add(entryPoint);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setImmediate to only check if all chunks were loaded after this function resolves
|
||||||
|
// We check if all chunks were loaded every time a factory is loaded
|
||||||
|
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
|
||||||
|
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
|
||||||
|
setTimeout(() => {
|
||||||
|
let allResolved = true;
|
||||||
|
|
||||||
|
for (let i = 0; i < chunksSearchPromises.length; i++) {
|
||||||
|
const isResolved = chunksSearchPromises[i]();
|
||||||
|
|
||||||
|
if (isResolved) {
|
||||||
|
// Remove finished promises to avoid having to iterate through a huge array everytime
|
||||||
|
chunksSearchPromises.splice(i--, 1);
|
||||||
|
} else {
|
||||||
|
allResolved = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allResolved) chunksSearchingResolve();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Webpack.factoryListeners.add(factory => {
|
||||||
|
let isResolved = false;
|
||||||
|
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
|
||||||
|
|
||||||
|
chunksSearchPromises.push(() => isResolved);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const factoryId in wreq.m) {
|
||||||
|
let isResolved = false;
|
||||||
|
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
|
||||||
|
|
||||||
|
chunksSearchPromises.push(() => isResolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
await chunksSearchingDone;
|
||||||
|
|
||||||
|
// Require deferred entry points
|
||||||
|
for (const deferredRequire of deferredRequires) {
|
||||||
|
wreq!(deferredRequire as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
||||||
|
const allChunks = [] as string[];
|
||||||
|
|
||||||
|
// Matches "id" or id:
|
||||||
|
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
|
||||||
|
const id = currentMatch[1] ?? currentMatch[2];
|
||||||
|
if (id == null) continue;
|
||||||
|
|
||||||
|
allChunks.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
||||||
|
|
||||||
|
// Chunks that are not loaded (not used) by Discord code anymore
|
||||||
|
const chunksLeft = allChunks.filter(id => {
|
||||||
|
return !(validChunks.has(id) || invalidChunks.has(id));
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(chunksLeft.map(async id => {
|
||||||
|
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(t => t.includes("importScripts("));
|
||||||
|
|
||||||
|
// Loads and requires a chunk
|
||||||
|
if (!isWorkerAsset) {
|
||||||
|
await wreq.e(id as any);
|
||||||
|
// Technically, the id of the chunk does not match the entry point
|
||||||
|
// But, still try it because we have no way to get the actual entry point
|
||||||
|
if (wreq.m[id]) wreq(id as any);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
LazyChunkLoaderLogger.log("Finished loading all chunks!");
|
||||||
|
} catch (e) {
|
||||||
|
LazyChunkLoaderLogger.log("A fatal error occurred:", e);
|
||||||
|
}
|
||||||
|
}
|
84
src/debug/runReporter.ts
Normal file
84
src/debug/runReporter.ts
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
|
import * as Webpack from "@webpack";
|
||||||
|
import { patches } from "plugins";
|
||||||
|
|
||||||
|
import { loadLazyChunks } from "./loadLazyChunks";
|
||||||
|
|
||||||
|
const ReporterLogger = new Logger("Reporter");
|
||||||
|
|
||||||
|
async function runReporter() {
|
||||||
|
try {
|
||||||
|
ReporterLogger.log("Starting test...");
|
||||||
|
|
||||||
|
let loadLazyChunksResolve: (value: void | PromiseLike<void>) => void;
|
||||||
|
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);
|
||||||
|
|
||||||
|
Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
|
||||||
|
await loadLazyChunksDone;
|
||||||
|
|
||||||
|
for (const patch of patches) {
|
||||||
|
if (!patch.all) {
|
||||||
|
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) {
|
||||||
|
let method = searchType;
|
||||||
|
|
||||||
|
if (searchType === "findComponent") method = "find";
|
||||||
|
if (searchType === "findExportedComponent") method = "findByProps";
|
||||||
|
if (searchType === "waitFor" || searchType === "waitForComponent") {
|
||||||
|
if (typeof args[0] === "string") method = "findByProps";
|
||||||
|
else method = "find";
|
||||||
|
}
|
||||||
|
if (searchType === "waitForStore") method = "findStore";
|
||||||
|
|
||||||
|
let result: any;
|
||||||
|
try {
|
||||||
|
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
|
||||||
|
const [factory] = args;
|
||||||
|
result = factory();
|
||||||
|
} else if (method === "extractAndLoadChunks") {
|
||||||
|
const [code, matcher] = args;
|
||||||
|
|
||||||
|
result = await Webpack.extractAndLoadChunks(code, matcher);
|
||||||
|
if (result === false) result = null;
|
||||||
|
} else if (method === "mapMangledModule") {
|
||||||
|
const [code, mapper] = args;
|
||||||
|
|
||||||
|
result = Webpack.mapMangledModule(code, mapper);
|
||||||
|
if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail");
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
result = Webpack[method](...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail");
|
||||||
|
} catch (e) {
|
||||||
|
let logMessage = searchType;
|
||||||
|
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
||||||
|
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
||||||
|
else if (method === "mapMangledModule") {
|
||||||
|
const failedMappings = Object.keys(args[1]).filter(key => result?.[key] == null);
|
||||||
|
|
||||||
|
logMessage += `("${args[0]}", {\n${failedMappings.map(mapping => `\t${mapping}: ${args[1][mapping].toString().slice(0, 147)}...`).join(",\n")}\n})`;
|
||||||
|
}
|
||||||
|
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
||||||
|
|
||||||
|
ReporterLogger.log("Webpack Find Fail:", logMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReporterLogger.log("Finished test");
|
||||||
|
} catch (e) {
|
||||||
|
ReporterLogger.log("A fatal error occurred:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runReporter();
|
3
src/globals.d.ts
vendored
3
src/globals.d.ts
vendored
|
@ -34,9 +34,10 @@ declare global {
|
||||||
*/
|
*/
|
||||||
export var IS_WEB: boolean;
|
export var IS_WEB: boolean;
|
||||||
export var IS_EXTENSION: boolean;
|
export var IS_EXTENSION: boolean;
|
||||||
export var IS_DEV: boolean;
|
|
||||||
export var IS_STANDALONE: boolean;
|
export var IS_STANDALONE: boolean;
|
||||||
export var IS_UPDATER_DISABLED: boolean;
|
export var IS_UPDATER_DISABLED: boolean;
|
||||||
|
export var IS_DEV: boolean;
|
||||||
|
export var IS_REPORTER: boolean;
|
||||||
export var IS_DISCORD_DESKTOP: boolean;
|
export var IS_DISCORD_DESKTOP: boolean;
|
||||||
export var IS_VESKTOP: boolean;
|
export var IS_VESKTOP: boolean;
|
||||||
export var VERSION: string;
|
export var VERSION: string;
|
||||||
|
|
|
@ -23,12 +23,11 @@ import "./settings";
|
||||||
import { debounce } from "@shared/debounce";
|
import { debounce } from "@shared/debounce";
|
||||||
import { IpcEvents } from "@shared/IpcEvents";
|
import { IpcEvents } from "@shared/IpcEvents";
|
||||||
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
|
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
|
||||||
|
import monacoHtml from "file://monacoWin.html?minify&base64";
|
||||||
import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
|
import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
|
||||||
import { open, readdir, readFile } from "fs/promises";
|
import { open, readdir, readFile } from "fs/promises";
|
||||||
import { join, normalize } from "path";
|
import { join, normalize } from "path";
|
||||||
|
|
||||||
import monacoHtml from "~fileContent/monacoWin.html;base64";
|
|
||||||
|
|
||||||
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
||||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
|
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
|
||||||
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
<title>Vencord QuickCSS Editor</title>
|
<title>Vencord QuickCSS Editor</title>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.40.0/min/vs/editor/editor.main.min.css"
|
href="https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs/editor/editor.main.css"
|
||||||
integrity="sha512-MOoQ02h80hklccfLrXFYkCzG+WVjORflOp9Zp8dltiaRP+35LYnO4LKOklR64oMGfGgJDLO8WJpkM1o5gZXYZQ=="
|
integrity="sha256-tiJPQ2O04z/pZ/AwdyIghrOMzewf+PIvEl1YKbQvsZk="
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
|
@ -29,8 +29,8 @@
|
||||||
<body>
|
<body>
|
||||||
<div id="container"></div>
|
<div id="container"></div>
|
||||||
<script
|
<script
|
||||||
src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.40.0/min/vs/loader.min.js"
|
src="https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs/loader.js"
|
||||||
integrity="sha512-QzMpXeCPciAHP4wbYlV2PYgrQcaEkDQUjzkPU4xnjyVSD9T36/udamxtNBqb4qK4/bMQMPZ8ayrBe9hrGdBFjQ=="
|
integrity="sha256-KcU48TGr84r7unF7J5IgBo95aeVrEbrGe04S7TcFUjs="
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
></script>
|
></script>
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
<script>
|
<script>
|
||||||
require.config({
|
require.config({
|
||||||
paths: {
|
paths: {
|
||||||
vs: "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.40.0/min/vs",
|
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,9 @@ if (!IS_VANILLA) {
|
||||||
const original = options.webPreferences.preload;
|
const original = options.webPreferences.preload;
|
||||||
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
|
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
|
||||||
options.webPreferences.sandbox = false;
|
options.webPreferences.sandbox = false;
|
||||||
|
// work around discord unloading when in background
|
||||||
|
options.webPreferences.backgroundThrottling = false;
|
||||||
|
|
||||||
if (settings.frameless) {
|
if (settings.frameless) {
|
||||||
options.frame = false;
|
options.frame = false;
|
||||||
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
|
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
|
||||||
|
@ -128,14 +131,28 @@ if (!IS_VANILLA) {
|
||||||
|
|
||||||
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
||||||
|
|
||||||
// Monkey patch commandLine to disable WidgetLayering: Fix DevTools context menus https://github.com/electron/electron/issues/38790
|
// Monkey patch commandLine to:
|
||||||
|
// - disable WidgetLayering: Fix DevTools context menus https://github.com/electron/electron/issues/38790
|
||||||
|
// - disable UseEcoQoSForBackgroundProcess: Work around Discord unloading when in background
|
||||||
const originalAppend = app.commandLine.appendSwitch;
|
const originalAppend = app.commandLine.appendSwitch;
|
||||||
app.commandLine.appendSwitch = function (...args) {
|
app.commandLine.appendSwitch = function (...args) {
|
||||||
if (args[0] === "disable-features" && !args[1]?.includes("WidgetLayering")) {
|
if (args[0] === "disable-features") {
|
||||||
args[1] += ",WidgetLayering";
|
const disabledFeatures = new Set((args[1] ?? "").split(","));
|
||||||
|
disabledFeatures.add("WidgetLayering");
|
||||||
|
disabledFeatures.add("UseEcoQoSForBackgroundProcess");
|
||||||
|
args[1] += [...disabledFeatures].join(",");
|
||||||
}
|
}
|
||||||
return originalAppend.apply(this, args);
|
return originalAppend.apply(this, args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// disable renderer backgrounding to prevent the app from unloading when in the background
|
||||||
|
// https://github.com/electron/electron/issues/2822
|
||||||
|
// https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling
|
||||||
|
// Work around discord unloading when in background
|
||||||
|
// Discord also recently started adding these flags but only on windows for some reason dunno why, it happens on Linux too
|
||||||
|
app.commandLine.appendSwitch("disable-renderer-backgrounding");
|
||||||
|
app.commandLine.appendSwitch("disable-background-timer-throttling");
|
||||||
|
app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");
|
||||||
} else {
|
} else {
|
||||||
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,4 +17,4 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (!IS_UPDATER_DISABLED)
|
if (!IS_UPDATER_DISABLED)
|
||||||
import(IS_STANDALONE ? "./http" : "./git");
|
require(IS_STANDALONE ? "./http" : "./git");
|
||||||
|
|
9
src/modules.d.ts
vendored
9
src/modules.d.ts
vendored
|
@ -20,8 +20,13 @@
|
||||||
/// <reference types="standalone-electron-types"/>
|
/// <reference types="standalone-electron-types"/>
|
||||||
|
|
||||||
declare module "~plugins" {
|
declare module "~plugins" {
|
||||||
const plugins: Record<string, import("@utils/types").Plugin>;
|
const plugins: Record<string, import("./utils/types").Plugin>;
|
||||||
export default plugins;
|
export default plugins;
|
||||||
|
export const PluginMeta: Record<string, {
|
||||||
|
folderName: string;
|
||||||
|
userPlugin: boolean;
|
||||||
|
}>;
|
||||||
|
export const ExcludedPlugins: Record<string, "web" | "discordDesktop" | "vencordDesktop" | "desktop" | "dev">;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "~pluginNatives" {
|
declare module "~pluginNatives" {
|
||||||
|
@ -38,7 +43,7 @@ declare module "~git-remote" {
|
||||||
export default remote;
|
export default remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "~fileContent/*" {
|
declare module "file://*" {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,18 +18,20 @@
|
||||||
|
|
||||||
import "./fixBadgeOverflow.css";
|
import "./fixBadgeOverflow.css";
|
||||||
|
|
||||||
import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
|
import { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
|
||||||
import DonateButton from "@components/DonateButton";
|
import DonateButton from "@components/DonateButton";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { Heart } from "@components/Heart";
|
import { Heart } from "@components/Heart";
|
||||||
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
|
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { isPluginDev } from "@utils/misc";
|
import { isPluginDev } from "@utils/misc";
|
||||||
import { closeModal, Modals, openModal } from "@utils/modal";
|
import { closeModal, Modals, openModal } from "@utils/modal";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Forms, Toasts } from "@webpack/common";
|
import { Forms, Toasts, UserStore } from "@webpack/common";
|
||||||
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png";
|
const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png";
|
||||||
|
|
||||||
|
@ -37,8 +39,8 @@ const ContributorBadge: ProfileBadge = {
|
||||||
description: "Vencord Contributor",
|
description: "Vencord Contributor",
|
||||||
image: CONTRIBUTOR_BADGE,
|
image: CONTRIBUTOR_BADGE,
|
||||||
position: BadgePosition.START,
|
position: BadgePosition.START,
|
||||||
shouldShow: ({ user }) => isPluginDev(user.id),
|
shouldShow: ({ userId }) => isPluginDev(userId),
|
||||||
onClick: (_, { user }) => openContributorModal(user)
|
onClick: (_, { userId }) => openContributorModal(UserStore.getUser(userId))
|
||||||
};
|
};
|
||||||
|
|
||||||
let DonorBadges = {} as Record<string, Array<Record<"tooltip" | "badge", string>>>;
|
let DonorBadges = {} as Record<string, Array<Record<"tooltip" | "badge", string>>>;
|
||||||
|
@ -66,7 +68,7 @@ export default definePlugin({
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
|
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
|
||||||
replace: "$&$1.unshift(...Vencord.Api.Badges._getBadges(arguments[0]));",
|
replace: "$&$1.unshift(...$self.getBadges(arguments[0]));",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// alt: "", aria-hidden: false, src: originalSrc
|
// alt: "", aria-hidden: false, src: originalSrc
|
||||||
|
@ -82,7 +84,36 @@ export default definePlugin({
|
||||||
// conditionally override their onClick with badge.onClick if it exists
|
// conditionally override their onClick with badge.onClick if it exists
|
||||||
{
|
{
|
||||||
match: /href:(\i)\.link/,
|
match: /href:(\i)\.link/,
|
||||||
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, arguments[0]) }),$&"
|
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, $1) }),$&"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* new profiles */
|
||||||
|
{
|
||||||
|
find: ".PANEL]:14",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=(\i)=\(0,\i\.\i\)\(\i\);)return 0===\i.length\?/,
|
||||||
|
replace: "$1.unshift(...$self.getBadges(arguments[0].displayProfile));$&"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: ".description,delay:",
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
// alt: "", aria-hidden: false, src: originalSrc
|
||||||
|
match: /alt:" ","aria-hidden":!0,src:(?=.{0,20}(\i)\.icon)/,
|
||||||
|
// ...badge.props, ..., src: badge.image ?? ...
|
||||||
|
replace: "...$1.props,$& $1.image??"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /(?<=text:(\i)\.description,.{0,50})children:/,
|
||||||
|
replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :"
|
||||||
|
},
|
||||||
|
// conditionally override their onClick with badge.onClick if it exists
|
||||||
|
{
|
||||||
|
match: /href:(\i)\.link/,
|
||||||
|
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, $1) }),$&"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -104,6 +135,19 @@ export default definePlugin({
|
||||||
await loadBadges();
|
await loadBadges();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getBadges(props: { userId: string; user?: User; guildId: string; }) {
|
||||||
|
if (!props) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
props.userId ??= props.user?.id!;
|
||||||
|
|
||||||
|
return _getBadges(props);
|
||||||
|
} catch (e) {
|
||||||
|
new Logger("BadgeAPI#hasBadges").error(e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
renderBadgeComponent: ErrorBoundary.wrap((badge: ProfileBadge & BadgeUserArgs) => {
|
renderBadgeComponent: ErrorBoundary.wrap((badge: ProfileBadge & BadgeUserArgs) => {
|
||||||
const Component = badge.component!;
|
const Component = badge.component!;
|
||||||
return <Component {...badge} />;
|
return <Component {...badge} />;
|
||||||
|
|
|
@ -15,8 +15,8 @@ export default definePlugin({
|
||||||
patches: [{
|
patches: [{
|
||||||
find: '"sticker")',
|
find: '"sticker")',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /!\i\.isMobile(?=.+?(\i)\.push\(.{0,50}"gift")/,
|
match: /return\(!\i\.\i&&(?=\(\i\.isDM.+?(\i)\.push\(.{0,50}"gift")/,
|
||||||
replace: "$& &&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)"
|
replace: "$&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)&&"
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: ".handleSendMessage",
|
find: ".handleSendMessage,onResize",
|
||||||
replacement: {
|
replacement: {
|
||||||
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
|
// 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)
|
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)
|
||||||
|
|
37
src/plugins/_api/messageUpdater.ts
Normal file
37
src/plugins/_api/messageUpdater.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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: "MessageUpdaterAPI",
|
||||||
|
description: "API for updating and re-rendering messages.",
|
||||||
|
authors: [Devs.Nuckyz],
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
// Message accessories have a custom logic to decide if they should render again, so we need to make it not ignore changed message reference
|
||||||
|
find: "}renderEmbeds(",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=this.props,\i,\[)"message",/,
|
||||||
|
replace: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
|
@ -29,7 +29,7 @@ export default definePlugin({
|
||||||
find: '"NoticeStore"',
|
find: '"NoticeStore"',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
|
match: /(?<=!1;)\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
|
||||||
replace: "if(Vencord.Api.Notices.currentNotice)return false;$&"
|
replace: "if(Vencord.Api.Notices.currentNotice)return false;$&"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
50
src/plugins/_api/userSettings.ts
Normal file
50
src/plugins/_api/userSettings.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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: "UserSettingsAPI",
|
||||||
|
description: "Patches Discord's UserSettings to expose their group and name.",
|
||||||
|
authors: [Devs.Nuckyz],
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ",updateSetting:",
|
||||||
|
replacement: [
|
||||||
|
// Main setting definition
|
||||||
|
{
|
||||||
|
match: /(?<=INFREQUENT_USER_ACTION.{0,20},)useSetting:/,
|
||||||
|
replace: "userSettingsAPIGroup:arguments[0],userSettingsAPIName:arguments[1],$&"
|
||||||
|
},
|
||||||
|
// Selective wrapper
|
||||||
|
{
|
||||||
|
match: /updateSetting:.{0,100}SELECTIVELY_SYNCED_USER_SETTINGS_UPDATE/,
|
||||||
|
replace: "userSettingsAPIGroup:arguments[0].userSettingsAPIGroup,userSettingsAPIName:arguments[0].userSettingsAPIName,$&"
|
||||||
|
},
|
||||||
|
// Override wrapper
|
||||||
|
{
|
||||||
|
match: /updateSetting:.{0,60}USER_SETTINGS_OVERRIDE_CLEAR/,
|
||||||
|
replace: "userSettingsAPIGroup:arguments[0].userSettingsAPIGroup,userSettingsAPIName:arguments[0].userSettingsAPIName,$&"
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
|
@ -18,7 +18,8 @@
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import { Logger } from "@utils/Logger";
|
||||||
|
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
disableAnalytics: {
|
disableAnalytics: {
|
||||||
|
@ -46,13 +47,6 @@ export default definePlugin({
|
||||||
replace: "()=>{}",
|
replace: "()=>{}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
find: "window.DiscordSentry=",
|
|
||||||
replacement: {
|
|
||||||
match: /^.+$/,
|
|
||||||
replace: "()=>{}",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
find: ".METRICS,",
|
find: ".METRICS,",
|
||||||
replacement: [
|
replacement: [
|
||||||
|
@ -74,5 +68,66 @@ export default definePlugin({
|
||||||
replace: "getDebugLogging(){return false;"
|
replace: "getDebugLogging(){return false;"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
|
|
||||||
|
startAt: StartAt.Init,
|
||||||
|
start() {
|
||||||
|
// Sentry is initialized in its own WebpackInstance.
|
||||||
|
// It has everything it needs preloaded, so, it doesn't include any chunk loading functionality.
|
||||||
|
// Because of that, its WebpackInstance doesnt export wreq.m or wreq.c
|
||||||
|
|
||||||
|
// To circuvent this and disable Sentry we are gonna hook when wreq.g of its WebpackInstance is set.
|
||||||
|
// When that happens we are gonna forcefully throw an error and abort everything.
|
||||||
|
Object.defineProperty(Function.prototype, "g", {
|
||||||
|
configurable: true,
|
||||||
|
|
||||||
|
set(v: any) {
|
||||||
|
Object.defineProperty(this, "g", {
|
||||||
|
value: v,
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
writable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure this is most likely the Sentry WebpackInstance.
|
||||||
|
// Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: <g></g>) to include it
|
||||||
|
const { stack } = new Error();
|
||||||
|
if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || !String(this).includes("exports:{}") || this.c != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetPath = stack?.match(/\/assets\/.+?\.js/)?.[0];
|
||||||
|
if (!assetPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcRequest = new XMLHttpRequest();
|
||||||
|
srcRequest.open("GET", assetPath, false);
|
||||||
|
srcRequest.send();
|
||||||
|
|
||||||
|
// Final condition to see if this is the Sentry WebpackInstance
|
||||||
|
if (!srcRequest.responseText.includes("window.DiscordSentry=")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new Logger("NoTrack", "#8caaee").info("Disabling Sentry by erroring its WebpackInstance");
|
||||||
|
|
||||||
|
Reflect.deleteProperty(Function.prototype, "g");
|
||||||
|
Reflect.deleteProperty(window, "DiscordSentry");
|
||||||
|
|
||||||
|
throw new Error("Sentry successfully disabled");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(window, "DiscordSentry", {
|
||||||
|
configurable: true,
|
||||||
|
|
||||||
|
set() {
|
||||||
|
new Logger("NoTrack", "#8caaee").error("Failed to disable Sentry. Falling back to deleting window.DiscordSentry");
|
||||||
|
|
||||||
|
Reflect.deleteProperty(Function.prototype, "g");
|
||||||
|
Reflect.deleteProperty(window, "DiscordSentry");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -56,26 +56,24 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// Discord Canary
|
|
||||||
{
|
{
|
||||||
find: "Messages.ACTIVITY_SETTINGS",
|
find: "Messages.ACTIVITY_SETTINGS",
|
||||||
replacement: {
|
replacement: [
|
||||||
|
{
|
||||||
match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
|
match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
|
||||||
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
|
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "useDefaultUserSettingsSections:function",
|
match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,30}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
|
||||||
replacement: {
|
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
|
||||||
match: /(?<=useDefaultUserSettingsSections:function\(\){return )(\i)\}/,
|
|
||||||
replace: "$self.wrapSettingsHook($1)}"
|
|
||||||
}
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
|
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.\i\).*?(\i\.\i)\.open\()/,
|
||||||
replace: "$2.default.open($1);return;"
|
replace: "$2.open($1);return;"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -16,24 +16,34 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { addAccessory } from "@api/MessageAccessories";
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { getUserSettingLazy } from "@api/UserSettings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Flex } from "@components/Flex";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
||||||
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
||||||
|
import { sendMessage } from "@utils/discord";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { isPluginDev } from "@utils/misc";
|
import { isPluginDev, tryOrElse } from "@utils/misc";
|
||||||
import { relaunch } from "@utils/native";
|
import { relaunch } from "@utils/native";
|
||||||
|
import { onlyOnce } from "@utils/onlyOnce";
|
||||||
import { makeCodeblock } from "@utils/text";
|
import { makeCodeblock } from "@utils/text";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { isOutdated, update } from "@utils/updater";
|
import { checkForUpdates, isOutdated, update } from "@utils/updater";
|
||||||
import { Alerts, Card, ChannelStore, Forms, GuildMemberStore, NavigationRouter, Parser, RelationshipStore, UserStore } from "@webpack/common";
|
import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
import plugins from "~plugins";
|
import plugins, { PluginMeta } from "~plugins";
|
||||||
|
|
||||||
import settings from "./settings";
|
import SettingsPlugin from "./settings";
|
||||||
|
|
||||||
const VENCORD_GUILD_ID = "1015060230222131221";
|
const VENCORD_GUILD_ID = "1015060230222131221";
|
||||||
|
const VENBOT_USER_ID = "1017176847865352332";
|
||||||
|
const KNOWN_ISSUES_CHANNEL_ID = "1222936386626129920";
|
||||||
|
const CodeBlockRe = /```js\n(.+?)```/s;
|
||||||
|
|
||||||
const AllowedChannelIds = [
|
const AllowedChannelIds = [
|
||||||
SUPPORT_CHANNEL_ID,
|
SUPPORT_CHANNEL_ID,
|
||||||
|
@ -47,26 +57,21 @@ const TrustedRolesIds = [
|
||||||
"1042507929485586532", // donor
|
"1042507929485586532", // donor
|
||||||
];
|
];
|
||||||
|
|
||||||
export default definePlugin({
|
const AsyncFunction = async function () { }.constructor;
|
||||||
name: "SupportHelper",
|
|
||||||
required: true,
|
|
||||||
description: "Helps us provide support to you",
|
|
||||||
authors: [Devs.Ven],
|
|
||||||
dependencies: ["CommandsAPI"],
|
|
||||||
|
|
||||||
patches: [{
|
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
|
||||||
find: ".BEGINNING_DM.format",
|
|
||||||
replacement: {
|
async function forceUpdate() {
|
||||||
match: /BEGINNING_DM\.format\(\{.+?\}\),(?=.{0,100}userId:(\i\.getRecipientId\(\)))/,
|
const outdated = await checkForUpdates();
|
||||||
replace: "$& $self.ContributorDmWarningCard({ userId: $1 }),"
|
if (outdated) {
|
||||||
|
await update();
|
||||||
|
relaunch();
|
||||||
}
|
}
|
||||||
}],
|
|
||||||
|
|
||||||
commands: [{
|
return outdated;
|
||||||
name: "vencord-debug",
|
}
|
||||||
description: "Send Vencord Debug info",
|
|
||||||
predicate: ctx => AllowedChannelIds.includes(ctx.channel.id),
|
async function generateDebugInfoMessage() {
|
||||||
async execute() {
|
|
||||||
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
|
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
|
||||||
|
|
||||||
const client = (() => {
|
const client = (() => {
|
||||||
|
@ -79,35 +84,91 @@ export default definePlugin({
|
||||||
return `${name} (${navigator.userAgent})`;
|
return `${name} (${navigator.userAgent})`;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
|
|
||||||
|
|
||||||
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
|
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
Vencord:
|
Vencord:
|
||||||
`v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +
|
`v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +
|
||||||
`${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
|
`${SettingsPlugin.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
|
||||||
Client: `${RELEASE_CHANNEL} ~ ${client}`,
|
Client: `${RELEASE_CHANNEL} ~ ${client}`,
|
||||||
Platform: window.navigator.platform
|
Platform: window.navigator.platform
|
||||||
};
|
};
|
||||||
|
|
||||||
if (IS_DISCORD_DESKTOP) {
|
if (IS_DISCORD_DESKTOP) {
|
||||||
info["Last Crash Reason"] = (await DiscordNative.processUtils.getLastCrash())?.rendererCrashReason ?? "N/A";
|
info["Last Crash Reason"] = (await tryOrElse(() => DiscordNative.processUtils.getLastCrash(), undefined))?.rendererCrashReason ?? "N/A";
|
||||||
}
|
}
|
||||||
|
|
||||||
const debugInfo = `
|
const commonIssues = {
|
||||||
>>> ${Object.entries(info).map(([k, v]) => `**${k}**: ${v}`).join("\n")}
|
"NoRPC enabled": Vencord.Plugins.isPluginEnabled("NoRPC"),
|
||||||
|
"Activity Sharing disabled": tryOrElse(() => !ShowCurrentGame.getSetting(), false),
|
||||||
Enabled Plugins (${enabledPlugins.length}):
|
"Vencord DevBuild": !IS_STANDALONE,
|
||||||
${makeCodeblock(enabledPlugins.join(", "))}
|
"Has UserPlugins": Object.values(PluginMeta).some(m => m.userPlugin),
|
||||||
`;
|
"More than two weeks out of date": BUILD_TIMESTAMP < Date.now() - 12096e5,
|
||||||
|
|
||||||
return {
|
|
||||||
content: debugInfo.trim().replaceAll("```\n", "```")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let content = `>>> ${Object.entries(info).map(([k, v]) => `**${k}**: ${v}`).join("\n")}`;
|
||||||
|
content += "\n" + Object.entries(commonIssues)
|
||||||
|
.filter(([, v]) => v).map(([k]) => `⚠️ ${k}`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return content.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePluginList() {
|
||||||
|
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
|
||||||
|
|
||||||
|
const enabledPlugins = Object.keys(plugins)
|
||||||
|
.filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
|
||||||
|
|
||||||
|
const enabledStockPlugins = enabledPlugins.filter(p => !PluginMeta[p].userPlugin);
|
||||||
|
const enabledUserPlugins = enabledPlugins.filter(p => PluginMeta[p].userPlugin);
|
||||||
|
|
||||||
|
|
||||||
|
let content = `**Enabled Plugins (${enabledStockPlugins.length}):**\n${makeCodeblock(enabledStockPlugins.join(", "))}`;
|
||||||
|
|
||||||
|
if (enabledUserPlugins.length) {
|
||||||
|
content += `**Enabled UserPlugins (${enabledUserPlugins.length}):**\n${makeCodeblock(enabledUserPlugins.join(", "))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkForUpdatesOnce = onlyOnce(checkForUpdates);
|
||||||
|
|
||||||
|
const settings = definePluginSettings({}).withPrivateSettings<{
|
||||||
|
dismissedDevBuildWarning?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "SupportHelper",
|
||||||
|
required: true,
|
||||||
|
description: "Helps us provide support to you",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
dependencies: ["CommandsAPI", "UserSettingsAPI", "MessageAccessoriesAPI"],
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
|
patches: [{
|
||||||
|
find: ".BEGINNING_DM.format",
|
||||||
|
replacement: {
|
||||||
|
match: /BEGINNING_DM\.format\(\{.+?\}\),(?=.{0,100}userId:(\i\.getRecipientId\(\)))/,
|
||||||
|
replace: "$& $self.ContributorDmWarningCard({ userId: $1 }),"
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
name: "vencord-debug",
|
||||||
|
description: "Send Vencord debug info",
|
||||||
|
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id),
|
||||||
|
execute: async () => ({ content: await generateDebugInfoMessage() })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "vencord-plugins",
|
||||||
|
description: "Send Vencord plugin list",
|
||||||
|
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id),
|
||||||
|
execute: () => ({ content: generatePluginList() })
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
flux: {
|
flux: {
|
||||||
async CHANNEL_SELECT({ channelId }) {
|
async CHANNEL_SELECT({ channelId }) {
|
||||||
if (channelId !== SUPPORT_CHANNEL_ID) return;
|
if (channelId !== SUPPORT_CHANNEL_ID) return;
|
||||||
|
@ -115,6 +176,9 @@ ${makeCodeblock(enabledPlugins.join(", "))}
|
||||||
const selfId = UserStore.getCurrentUser()?.id;
|
const selfId = UserStore.getCurrentUser()?.id;
|
||||||
if (!selfId || isPluginDev(selfId)) return;
|
if (!selfId || isPluginDev(selfId)) return;
|
||||||
|
|
||||||
|
if (!IS_UPDATER_DISABLED) {
|
||||||
|
await checkForUpdatesOnce().catch(() => { });
|
||||||
|
|
||||||
if (isOutdated) {
|
if (isOutdated) {
|
||||||
return Alerts.show({
|
return Alerts.show({
|
||||||
title: "Hold on!",
|
title: "Hold on!",
|
||||||
|
@ -127,13 +191,11 @@ ${makeCodeblock(enabledPlugins.join(", "))}
|
||||||
onCancel: () => openUpdaterModal!(),
|
onCancel: () => openUpdaterModal!(),
|
||||||
cancelText: "View Updates",
|
cancelText: "View Updates",
|
||||||
confirmText: "Update & Restart Now",
|
confirmText: "Update & Restart Now",
|
||||||
async onConfirm() {
|
onConfirm: forceUpdate,
|
||||||
await update();
|
|
||||||
relaunch();
|
|
||||||
},
|
|
||||||
secondaryConfirmText: "I know what I'm doing or I can't update"
|
secondaryConfirmText: "I know what I'm doing or I can't update"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore outdated type
|
// @ts-ignore outdated type
|
||||||
const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
|
const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
|
||||||
|
@ -148,23 +210,26 @@ ${makeCodeblock(enabledPlugins.join(", "))}
|
||||||
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
|
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
|
||||||
contact your package maintainer for support instead.
|
contact your package maintainer for support instead.
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
</div>,
|
</div>
|
||||||
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const repo = await VencordNative.updater.getRepo();
|
if (!IS_STANDALONE && !settings.store.dismissedDevBuildWarning) {
|
||||||
if (repo.ok && !repo.value.includes("Vendicated/Vencord")) {
|
|
||||||
return Alerts.show({
|
return Alerts.show({
|
||||||
title: "Hold on!",
|
title: "Hold on!",
|
||||||
body: <div>
|
body: <div>
|
||||||
<Forms.FormText>You are using a fork of Vencord, which we do not provide support for!</Forms.FormText>
|
<Forms.FormText>You are using a custom build of Vencord, which we do not provide support for!</Forms.FormText>
|
||||||
|
|
||||||
<Forms.FormText className={Margins.top8}>
|
<Forms.FormText className={Margins.top8}>
|
||||||
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
|
We only provide support for <Link href="https://vencord.dev/download">official builds</Link>.
|
||||||
contact your package maintainer for support instead.
|
Either <Link href="https://vencord.dev/download">switch to an official build</Link> or figure your issue out yourself.
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
|
|
||||||
|
<Text variant="text-md/bold" className={Margins.top8}>You will be banned from receiving support if you ignore this rule.</Text>
|
||||||
</div>,
|
</div>,
|
||||||
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
|
confirmText: "Understood",
|
||||||
|
secondaryConfirmText: "Don't show again",
|
||||||
|
onConfirmSecondary: () => settings.store.dismissedDevBuildWarning = true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,7 +237,7 @@ ${makeCodeblock(enabledPlugins.join(", "))}
|
||||||
|
|
||||||
ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => {
|
ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => {
|
||||||
if (!isPluginDev(userId)) return null;
|
if (!isPluginDev(userId)) return null;
|
||||||
if (RelationshipStore.isFriend(userId)) return null;
|
if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
|
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
|
||||||
|
@ -182,5 +247,86 @@ ${makeCodeblock(enabledPlugins.join(", "))}
|
||||||
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
|
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}, { noop: true })
|
}, { noop: true }),
|
||||||
|
|
||||||
|
start() {
|
||||||
|
addAccessory("vencord-debug", props => {
|
||||||
|
const buttons = [] as JSX.Element[];
|
||||||
|
|
||||||
|
const shouldAddUpdateButton =
|
||||||
|
!IS_UPDATER_DISABLED
|
||||||
|
&& (
|
||||||
|
(props.channel.id === KNOWN_ISSUES_CHANNEL_ID) ||
|
||||||
|
(props.channel.id === SUPPORT_CHANNEL_ID && props.message.author.id === VENBOT_USER_ID)
|
||||||
|
)
|
||||||
|
&& props.message.content?.includes("update");
|
||||||
|
|
||||||
|
if (shouldAddUpdateButton) {
|
||||||
|
buttons.push(
|
||||||
|
<Button
|
||||||
|
key="vc-update"
|
||||||
|
color={Button.Colors.GREEN}
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (await forceUpdate())
|
||||||
|
showToast("Success! Restarting...", Toasts.Type.SUCCESS);
|
||||||
|
else
|
||||||
|
showToast("Already up to date!", Toasts.Type.MESSAGE);
|
||||||
|
} catch (e) {
|
||||||
|
new Logger(this.name).error("Error while updating:", e);
|
||||||
|
showToast("Failed to update :(", Toasts.Type.FAILURE);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update Now
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.channel.id === SUPPORT_CHANNEL_ID) {
|
||||||
|
if (props.message.content.includes("/vencord-debug") || props.message.content.includes("/vencord-plugins")) {
|
||||||
|
buttons.push(
|
||||||
|
<Button
|
||||||
|
key="vc-dbg"
|
||||||
|
onClick={async () => sendMessage(props.channel.id, { content: await generateDebugInfoMessage() })}
|
||||||
|
>
|
||||||
|
Run /vencord-debug
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="vc-plg-list"
|
||||||
|
onClick={async () => sendMessage(props.channel.id, { content: generatePluginList() })}
|
||||||
|
>
|
||||||
|
Run /vencord-plugins
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.message.author.id === VENBOT_USER_ID) {
|
||||||
|
const match = CodeBlockRe.exec(props.message.content || props.message.embeds[0]?.rawDescription || "");
|
||||||
|
if (match) {
|
||||||
|
buttons.push(
|
||||||
|
<Button
|
||||||
|
key="vc-run-snippet"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await AsyncFunction(match[1])();
|
||||||
|
showToast("Success!", Toasts.Type.SUCCESS);
|
||||||
|
} catch (e) {
|
||||||
|
new Logger(this.name).error("Error while running snippet:", e);
|
||||||
|
showToast("Failed to run snippet :(", Toasts.Type.FAILURE);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Run Snippet
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buttons.length
|
||||||
|
? <Flex>{buttons}</Flex>
|
||||||
|
: null;
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,10 +31,10 @@ export default definePlugin({
|
||||||
// Some modules match the find but the replacement is returned untouched
|
// Some modules match the find but the replacement is returned untouched
|
||||||
noWarn: true,
|
noWarn: true,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /canAnimate:.+?(?=([,}].*?\)))/g,
|
match: /canAnimate:.+?([,}].*?\))/g,
|
||||||
replace: (m, rest) => {
|
replace: (m, rest) => {
|
||||||
const destructuringMatch = rest.match(/}=.+/);
|
const destructuringMatch = rest.match(/}=.+/);
|
||||||
if (destructuringMatch == null) return "canAnimate:!0";
|
if (destructuringMatch == null) return `canAnimate:!0${rest}`;
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ export default definePlugin({
|
||||||
predicate: () => settings.store.domain
|
predicate: () => settings.store.domain
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "isSuspiciousDownload:",
|
find: "bitbucket.org",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
|
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
|
||||||
replace: "$&return null;"
|
replace: "$&return null;"
|
||||||
|
|
|
@ -73,13 +73,13 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: "instantBatchUpload:function",
|
find: "instantBatchUpload:function",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /uploadFiles:(.{1,2}),/,
|
match: /uploadFiles:(\i),/,
|
||||||
replace:
|
replace:
|
||||||
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
|
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "message.attachments",
|
find: 'addFilesTo:"message.attachments"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(\i.uploadFiles\((\i),)/,
|
match: /(\i.uploadFiles\((\i),)/,
|
||||||
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"
|
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"
|
||||||
|
|
9
src/plugins/appleMusic.desktop/README.md
Normal file
9
src/plugins/appleMusic.desktop/README.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# AppleMusicRichPresence
|
||||||
|
|
||||||
|
This plugin enables Discord rich presence for your Apple Music! (This only works on macOS with the Music app.)
|
||||||
|
|
||||||
|
![Screenshot of the activity in Discord](https://github.com/Vendicated/Vencord/assets/70191398/1f811090-ab5f-4060-a9ee-d0ac44a1d3c0)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
For the customizable activity format strings, you can use several special strings to include track data in activities! `{name}` is replaced with the track name; `{artist}` is replaced with the artist(s)' name(s); and `{album}` is replaced with the album name.
|
262
src/plugins/appleMusic.desktop/index.tsx
Normal file
262
src/plugins/appleMusic.desktop/index.tsx
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
|
||||||
|
import { ApplicationAssetUtils, FluxDispatcher, Forms } from "@webpack/common";
|
||||||
|
|
||||||
|
const Native = VencordNative.pluginHelpers.AppleMusicRichPresence as PluginNative<typeof import("./native")>;
|
||||||
|
|
||||||
|
interface ActivityAssets {
|
||||||
|
large_image?: string;
|
||||||
|
large_text?: string;
|
||||||
|
small_image?: string;
|
||||||
|
small_text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivityButton {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Activity {
|
||||||
|
state: string;
|
||||||
|
details?: string;
|
||||||
|
timestamps?: {
|
||||||
|
start?: number;
|
||||||
|
end?: number;
|
||||||
|
};
|
||||||
|
assets?: ActivityAssets;
|
||||||
|
buttons?: Array<string>;
|
||||||
|
name: string;
|
||||||
|
application_id: string;
|
||||||
|
metadata?: {
|
||||||
|
button_urls?: Array<string>;
|
||||||
|
};
|
||||||
|
type: number;
|
||||||
|
flags: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum ActivityType {
|
||||||
|
PLAYING = 0,
|
||||||
|
LISTENING = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum ActivityFlag {
|
||||||
|
INSTANCE = 1 << 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrackData {
|
||||||
|
name: string;
|
||||||
|
album: string;
|
||||||
|
artist: string;
|
||||||
|
|
||||||
|
appleMusicLink?: string;
|
||||||
|
songLink?: string;
|
||||||
|
|
||||||
|
albumArtwork?: string;
|
||||||
|
artistArtwork?: string;
|
||||||
|
|
||||||
|
playerPosition: number;
|
||||||
|
duration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum AssetImageType {
|
||||||
|
Album = "Album",
|
||||||
|
Artist = "Artist",
|
||||||
|
Disabled = "Disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
const applicationId = "1239490006054207550";
|
||||||
|
|
||||||
|
function setActivity(activity: Activity | null) {
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "LOCAL_ACTIVITY_UPDATE",
|
||||||
|
activity,
|
||||||
|
socketId: "AppleMusic",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
activityType: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "Which type of activity",
|
||||||
|
options: [
|
||||||
|
{ label: "Playing", value: ActivityType.PLAYING, default: true },
|
||||||
|
{ label: "Listening", value: ActivityType.LISTENING }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
refreshInterval: {
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
description: "The interval between activity refreshes (seconds)",
|
||||||
|
markers: [1, 2, 2.5, 3, 5, 10, 15],
|
||||||
|
default: 5,
|
||||||
|
restartNeeded: true,
|
||||||
|
},
|
||||||
|
enableTimestamps: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Whether or not to enable timestamps",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
enableButtons: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Whether or not to enable buttons",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
nameString: {
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description: "Activity name format string",
|
||||||
|
default: "Apple Music"
|
||||||
|
},
|
||||||
|
detailsString: {
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description: "Activity details format string",
|
||||||
|
default: "{name}"
|
||||||
|
},
|
||||||
|
stateString: {
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description: "Activity state format string",
|
||||||
|
default: "{artist}"
|
||||||
|
},
|
||||||
|
largeImageType: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "Activity assets large image type",
|
||||||
|
options: [
|
||||||
|
{ label: "Album artwork", value: AssetImageType.Album, default: true },
|
||||||
|
{ label: "Artist artwork", value: AssetImageType.Artist },
|
||||||
|
{ label: "Disabled", value: AssetImageType.Disabled }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
largeTextString: {
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description: "Activity assets large text format string",
|
||||||
|
default: "{album}"
|
||||||
|
},
|
||||||
|
smallImageType: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "Activity assets small image type",
|
||||||
|
options: [
|
||||||
|
{ label: "Album artwork", value: AssetImageType.Album },
|
||||||
|
{ label: "Artist artwork", value: AssetImageType.Artist, default: true },
|
||||||
|
{ label: "Disabled", value: AssetImageType.Disabled }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
smallTextString: {
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description: "Activity assets small text format string",
|
||||||
|
default: "{artist}"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function customFormat(formatStr: string, data: TrackData) {
|
||||||
|
return formatStr
|
||||||
|
.replaceAll("{name}", data.name)
|
||||||
|
.replaceAll("{album}", data.album)
|
||||||
|
.replaceAll("{artist}", data.artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageAsset(type: AssetImageType, data: TrackData) {
|
||||||
|
const source = type === AssetImageType.Album
|
||||||
|
? data.albumArtwork
|
||||||
|
: data.artistArtwork;
|
||||||
|
|
||||||
|
if (!source) return undefined;
|
||||||
|
|
||||||
|
return ApplicationAssetUtils.fetchAssetIds(applicationId, [source]).then(ids => ids[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "AppleMusicRichPresence",
|
||||||
|
description: "Discord rich presence for your Apple Music!",
|
||||||
|
authors: [Devs.RyanCaoDev],
|
||||||
|
hidden: !navigator.platform.startsWith("Mac"),
|
||||||
|
reporterTestable: ReporterTestable.None,
|
||||||
|
|
||||||
|
settingsAboutComponent() {
|
||||||
|
return <>
|
||||||
|
<Forms.FormText>
|
||||||
|
For the customizable activity format strings, you can use several special strings to include track data in activities!{" "}
|
||||||
|
<code>{"{name}"}</code> is replaced with the track name; <code>{"{artist}"}</code> is replaced with the artist(s)' name(s); and <code>{"{album}"}</code> is replaced with the album name.
|
||||||
|
</Forms.FormText>
|
||||||
|
</>;
|
||||||
|
},
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.updatePresence();
|
||||||
|
this.updateInterval = setInterval(() => { this.updatePresence(); }, settings.store.refreshInterval * 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
clearInterval(this.updateInterval);
|
||||||
|
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null });
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePresence() {
|
||||||
|
this.getActivity().then(activity => { setActivity(activity); });
|
||||||
|
},
|
||||||
|
|
||||||
|
async getActivity(): Promise<Activity | null> {
|
||||||
|
const trackData = await Native.fetchTrackData();
|
||||||
|
if (!trackData) return null;
|
||||||
|
|
||||||
|
const [largeImageAsset, smallImageAsset] = await Promise.all([
|
||||||
|
getImageAsset(settings.store.largeImageType, trackData),
|
||||||
|
getImageAsset(settings.store.smallImageType, trackData)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const assets: ActivityAssets = {};
|
||||||
|
|
||||||
|
if (settings.store.largeImageType !== AssetImageType.Disabled) {
|
||||||
|
assets.large_image = largeImageAsset;
|
||||||
|
assets.large_text = customFormat(settings.store.largeTextString, trackData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.store.smallImageType !== AssetImageType.Disabled) {
|
||||||
|
assets.small_image = smallImageAsset;
|
||||||
|
assets.small_text = customFormat(settings.store.smallTextString, trackData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttons: ActivityButton[] = [];
|
||||||
|
|
||||||
|
if (settings.store.enableButtons) {
|
||||||
|
if (trackData.appleMusicLink)
|
||||||
|
buttons.push({
|
||||||
|
label: "Listen on Apple Music",
|
||||||
|
url: trackData.appleMusicLink,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (trackData.songLink)
|
||||||
|
buttons.push({
|
||||||
|
label: "View on SongLink",
|
||||||
|
url: trackData.songLink,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
application_id: applicationId,
|
||||||
|
|
||||||
|
name: customFormat(settings.store.nameString, trackData),
|
||||||
|
details: customFormat(settings.store.detailsString, trackData),
|
||||||
|
state: customFormat(settings.store.stateString, trackData),
|
||||||
|
|
||||||
|
timestamps: (settings.store.enableTimestamps ? {
|
||||||
|
start: Date.now() - (trackData.playerPosition * 1000),
|
||||||
|
end: Date.now() - (trackData.playerPosition * 1000) + (trackData.duration * 1000),
|
||||||
|
} : undefined),
|
||||||
|
|
||||||
|
assets,
|
||||||
|
|
||||||
|
buttons: buttons.length ? buttons.map(v => v.label) : undefined,
|
||||||
|
metadata: { button_urls: buttons.map(v => v.url) || undefined, },
|
||||||
|
|
||||||
|
type: settings.store.activityType,
|
||||||
|
flags: ActivityFlag.INSTANCE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
120
src/plugins/appleMusic.desktop/native.ts
Normal file
120
src/plugins/appleMusic.desktop/native.ts
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execFile } from "child_process";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
import type { TrackData } from ".";
|
||||||
|
|
||||||
|
const exec = promisify(execFile);
|
||||||
|
|
||||||
|
// function exec(file: string, args: string[] = []) {
|
||||||
|
// return new Promise<{ code: number | null, stdout: string | null, stderr: string | null; }>((resolve, reject) => {
|
||||||
|
// const process = spawn(file, args, { stdio: [null, "pipe", "pipe"] });
|
||||||
|
|
||||||
|
// let stdout: string | null = null;
|
||||||
|
// process.stdout.on("data", (chunk: string) => { stdout ??= ""; stdout += chunk; });
|
||||||
|
// let stderr: string | null = null;
|
||||||
|
// process.stderr.on("data", (chunk: string) => { stdout ??= ""; stderr += chunk; });
|
||||||
|
|
||||||
|
// process.on("exit", code => { resolve({ code, stdout, stderr }); });
|
||||||
|
// process.on("error", err => reject(err));
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
async function applescript(cmds: string[]) {
|
||||||
|
const { stdout } = await exec("osascript", cmds.map(c => ["-e", c]).flat());
|
||||||
|
return stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSearchUrl(type: string, query: string) {
|
||||||
|
const url = new URL("https://tools.applemediaservices.com/api/apple-media/music/US/search.json");
|
||||||
|
url.searchParams.set("types", type);
|
||||||
|
url.searchParams.set("limit", "1");
|
||||||
|
url.searchParams.set("term", query);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOptions: RequestInit = {
|
||||||
|
headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; rv:125.0) Gecko/20100101 Firefox/125.0" },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RemoteData {
|
||||||
|
appleMusicLink?: string,
|
||||||
|
songLink?: string,
|
||||||
|
albumArtwork?: string,
|
||||||
|
artistArtwork?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
|
||||||
|
|
||||||
|
async function fetchRemoteData({ id, name, artist, album }: { id: string, name: string, artist: string, album: string; }) {
|
||||||
|
if (id === cachedRemoteData?.id) {
|
||||||
|
if ("data" in cachedRemoteData) return cachedRemoteData.data;
|
||||||
|
if ("failures" in cachedRemoteData && cachedRemoteData.failures >= 5) return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [songData, artistData] = await Promise.all([
|
||||||
|
fetch(makeSearchUrl("songs", artist + " " + album + " " + name), requestOptions).then(r => r.json()),
|
||||||
|
fetch(makeSearchUrl("artists", artist.split(/ *[,&] */)[0]), requestOptions).then(r => r.json())
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appleMusicLink = songData?.songs?.data[0]?.attributes.url;
|
||||||
|
const songLink = songData?.songs?.data[0]?.id ? `https://song.link/i/${songData?.songs?.data[0]?.id}` : undefined;
|
||||||
|
|
||||||
|
const albumArtwork = songData?.songs?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
|
||||||
|
const artistArtwork = artistData?.artists?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
|
||||||
|
|
||||||
|
cachedRemoteData = {
|
||||||
|
id,
|
||||||
|
data: { appleMusicLink, songLink, albumArtwork, artistArtwork }
|
||||||
|
};
|
||||||
|
return cachedRemoteData.data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[AppleMusicRichPresence] Failed to fetch remote data:", e);
|
||||||
|
cachedRemoteData = {
|
||||||
|
id,
|
||||||
|
failures: (id === cachedRemoteData?.id && "failures" in cachedRemoteData ? cachedRemoteData.failures : 0) + 1
|
||||||
|
};
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTrackData(): Promise<TrackData | null> {
|
||||||
|
try {
|
||||||
|
await exec("pgrep", ["^Music$"]);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerState = await applescript(['tell application "Music"', "get player state", "end tell"])
|
||||||
|
.then(out => out.trim());
|
||||||
|
if (playerState !== "playing") return null;
|
||||||
|
|
||||||
|
const playerPosition = await applescript(['tell application "Music"', "get player position", "end tell"])
|
||||||
|
.then(text => Number.parseFloat(text.trim()));
|
||||||
|
|
||||||
|
const stdout = await applescript([
|
||||||
|
'set output to ""',
|
||||||
|
'tell application "Music"',
|
||||||
|
"set t_id to database id of current track",
|
||||||
|
"set t_name to name of current track",
|
||||||
|
"set t_album to album of current track",
|
||||||
|
"set t_artist to artist of current track",
|
||||||
|
"set t_duration to duration of current track",
|
||||||
|
'set output to "" & t_id & "\\n" & t_name & "\\n" & t_album & "\\n" & t_artist & "\\n" & t_duration',
|
||||||
|
"end tell",
|
||||||
|
"return output"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [id, name, album, artist, durationStr] = stdout.split("\n").filter(k => !!k);
|
||||||
|
const duration = Number.parseFloat(durationStr);
|
||||||
|
|
||||||
|
const remoteData = await fetchRemoteData({ id, name, artist, album });
|
||||||
|
|
||||||
|
return { name, album, artist, playerPosition, duration, ...remoteData };
|
||||||
|
}
|
|
@ -19,11 +19,11 @@
|
||||||
import { popNotice, showNotice } from "@api/Notices";
|
import { popNotice, showNotice } from "@api/Notices";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin, { ReporterTestable } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByCodeLazy } from "@webpack";
|
||||||
import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common";
|
import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
const RpcUtils = findByPropsLazy("fetchApplicationsRPC", "getRemoteIconURL");
|
const fetchApplicationsRPC = findByCodeLazy("APPLICATION_RPC(", "Client ID");
|
||||||
|
|
||||||
async function lookupAsset(applicationId: string, key: string): Promise<string> {
|
async function lookupAsset(applicationId: string, key: string): Promise<string> {
|
||||||
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
|
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
|
||||||
|
@ -32,7 +32,7 @@ async function lookupAsset(applicationId: string, key: string): Promise<string>
|
||||||
const apps: any = {};
|
const apps: any = {};
|
||||||
async function lookupApp(applicationId: string): Promise<string> {
|
async function lookupApp(applicationId: string): Promise<string> {
|
||||||
const socket: any = {};
|
const socket: any = {};
|
||||||
await RpcUtils.fetchApplicationsRPC(socket, applicationId);
|
await fetchApplicationsRPC(socket, applicationId);
|
||||||
return socket.application;
|
return socket.application;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ export default definePlugin({
|
||||||
name: "WebRichPresence (arRPC)",
|
name: "WebRichPresence (arRPC)",
|
||||||
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
|
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
|
||||||
authors: [Devs.Ducko],
|
authors: [Devs.Ducko],
|
||||||
|
reporterTestable: ReporterTestable.None,
|
||||||
|
|
||||||
settingsAboutComponent: () => (
|
settingsAboutComponent: () => (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: "BAN_CONFIRM_TITLE.",
|
find: "BAN_CONFIRM_TITLE.",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /src:\i\("\d+"\)/g,
|
match: /src:\i\("?\d+"?\)/g,
|
||||||
replace: "src: Vencord.Settings.plugins.BANger.source"
|
replace: "src: Vencord.Settings.plugins.BANger.source"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
||||||
import { FluxDispatcher, i18n, useMemo } from "@webpack/common";
|
import { FluxDispatcher, i18n, useMemo } from "@webpack/common";
|
||||||
|
|
||||||
import FolderSideBar from "./FolderSideBar";
|
import FolderSideBar from "./FolderSideBar";
|
||||||
|
@ -30,7 +30,7 @@ enum FolderIconDisplay {
|
||||||
MoreThanOneFolderExpanded
|
MoreThanOneFolderExpanded
|
||||||
}
|
}
|
||||||
|
|
||||||
const { GuildsTree } = findByPropsLazy("GuildsTree");
|
const GuildsTree = findLazy(m => m.prototype?.moveNextTo);
|
||||||
const SortedGuildStore = findStoreLazy("SortedGuildStore");
|
const SortedGuildStore = findStoreLazy("SortedGuildStore");
|
||||||
export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
|
export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
|
||||||
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
|
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
|
||||||
|
@ -112,12 +112,12 @@ export default definePlugin({
|
||||||
replacement: [
|
replacement: [
|
||||||
// Create the isBetterFolders variable in the GuildsBar component
|
// Create the isBetterFolders variable in the GuildsBar component
|
||||||
{
|
{
|
||||||
match: /(?<=let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?)(?=}=\i,)/,
|
match: /let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?(?=}=\i,)/,
|
||||||
replace: ",isBetterFolders"
|
replace: "$&,isBetterFolders"
|
||||||
},
|
},
|
||||||
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
|
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
|
||||||
{
|
{
|
||||||
match: /\[(\i)\]=(\(0,\i\.useStateFromStoresArray\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
|
match: /\[(\i)\]=(\(0,\i\.\i\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
|
||||||
replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0].isBetterFolders,betterFoldersOriginalTree,arguments[0].betterFoldersExpandedIds)`
|
replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0].isBetterFolders,betterFoldersOriginalTree,arguments[0].betterFoldersExpandedIds)`
|
||||||
},
|
},
|
||||||
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
|
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
|
||||||
|
@ -139,13 +139,13 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// This is the parent folder component
|
// This is the parent folder component
|
||||||
find: ".MAX_GUILD_FOLDER_NAME_LENGTH,",
|
find: ".toggleGuildFolderExpand(",
|
||||||
predicate: () => settings.store.sidebar && settings.store.showFolderIcon !== FolderIconDisplay.Always,
|
predicate: () => settings.store.sidebar && settings.store.showFolderIcon !== FolderIconDisplay.Always,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// Modify the expanded state to instead return the list of expanded folders
|
// Modify the expanded state to instead return the list of expanded folders
|
||||||
match: /(useStateFromStores\).{0,20}=>)(\i\.\i)\.isFolderExpanded\(\i\)/,
|
match: /(\],\(\)=>)(\i\.\i)\.isFolderExpanded\(\i\)\)/,
|
||||||
replace: (_, rest, ExpandedGuildFolderStore) => `${rest}${ExpandedGuildFolderStore}.getExpandedFolders()`,
|
replace: (_, rest, ExpandedGuildFolderStore) => `${rest}${ExpandedGuildFolderStore}.getExpandedFolders())`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Modify the expanded prop to use the boolean if the above patch fails, or check if the folder is expanded from the list if it succeeds
|
// Modify the expanded prop to use the boolean if the above patch fails, or check if the folder is expanded from the list if it succeeds
|
||||||
|
@ -196,7 +196,7 @@ export default definePlugin({
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "APPLICATION_LIBRARY,render",
|
find: "APPLICATION_LIBRARY,render:",
|
||||||
predicate: () => settings.store.sidebar,
|
predicate: () => settings.store.sidebar,
|
||||||
replacement: {
|
replacement: {
|
||||||
// Render the Better Folders sidebar
|
// Render the Better Folders sidebar
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: ".Messages.GIF,",
|
find: ".Messages.GIF,",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /alt:(\i)=(\i\.default\.Messages\.GIF)(?=,[^}]*\}=(\i))/,
|
match: /alt:(\i)=(\i\.\i\.Messages\.GIF)(?=,[^}]*\}=(\i))/,
|
||||||
replace:
|
replace:
|
||||||
// rename prop so we can always use default value
|
// rename prop so we can always use default value
|
||||||
"alt_$$:$1=$self.altify($3)||$2",
|
"alt_$$:$1=$self.altify($3)||$2",
|
||||||
|
|
|
@ -13,7 +13,7 @@ export default definePlugin({
|
||||||
authors: [Devs.Samwich],
|
authors: [Devs.Samwich],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".GIFPickerResultTypes.SEARCH",
|
find: '"state",{resultType:',
|
||||||
replacement: [{
|
replacement: [{
|
||||||
match: /(?<="state",{resultType:)null/,
|
match: /(?<="state",{resultType:)null/,
|
||||||
replace: '"Favorites"'
|
replace: '"Favorites"'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# BetterRoleContext
|
# BetterRoleContext
|
||||||
|
|
||||||
Adds options to copy role color and edit role when right clicking roles in the user profile
|
Adds options to copy role color, edit role and view role icon when right clicking roles in the user profile
|
||||||
|
|
||||||
![](https://github.com/Vendicated/Vencord/assets/45497981/d1765e9e-7db2-4a3c-b110-139c59235326)
|
![](https://github.com/Vendicated/Vencord/assets/45497981/354220a4-09f3-4c5f-a28e-4b19ca775190)
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,19 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { getUserSettingLazy } from "@api/UserSettings";
|
||||||
|
import { ImageIcon } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { getCurrentGuild } from "@utils/discord";
|
import { getCurrentGuild, openImageModal } from "@utils/discord";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
|
import { Clipboard, GuildStore, Menu, PermissionStore } from "@webpack/common";
|
||||||
|
|
||||||
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
|
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
|
||||||
|
|
||||||
|
const DeveloperMode = getUserSettingLazy("appearance", "developerMode")!;
|
||||||
|
|
||||||
function PencilIcon() {
|
function PencilIcon() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
@ -34,14 +39,39 @@ function AppearanceIcon() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
roleIconFileFormat: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "File format to use when viewing role icons",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "png",
|
||||||
|
value: "png",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "webp",
|
||||||
|
value: "webp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "jpg",
|
||||||
|
value: "jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BetterRoleContext",
|
name: "BetterRoleContext",
|
||||||
description: "Adds options to copy role color / edit role when right clicking roles in the user profile",
|
description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven, Devs.goodbee],
|
||||||
|
dependencies: ["UserSettingsAPI"],
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
// DeveloperMode needs to be enabled for the context menu to be shown
|
// DeveloperMode needs to be enabled for the context menu to be shown
|
||||||
TextAndImagesSettingsStores.DeveloperMode.updateSetting(true);
|
DeveloperMode.updateSetting(true);
|
||||||
},
|
},
|
||||||
|
|
||||||
contextMenus: {
|
contextMenus: {
|
||||||
|
@ -63,6 +93,20 @@ export default definePlugin({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (role.icon) {
|
||||||
|
children.push(
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-view-role-icon"
|
||||||
|
label="View Role Icon"
|
||||||
|
action={() => {
|
||||||
|
openImageModal(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`);
|
||||||
|
}}
|
||||||
|
icon={ImageIcon}
|
||||||
|
/>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
|
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
|
||||||
children.push(
|
children.push(
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
|
|
|
@ -48,6 +48,7 @@ export default definePlugin({
|
||||||
|
|
||||||
{
|
{
|
||||||
find: ".ADD_ROLE_A11Y_LABEL",
|
find: ".ADD_ROLE_A11Y_LABEL",
|
||||||
|
all: true,
|
||||||
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,
|
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,
|
||||||
noWarn: true,
|
noWarn: true,
|
||||||
replacement: {
|
replacement: {
|
||||||
|
@ -57,6 +58,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: ".roleVerifiedIcon",
|
find: ".roleVerifiedIcon",
|
||||||
|
all: true,
|
||||||
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,
|
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,
|
||||||
noWarn: true,
|
noWarn: true,
|
||||||
replacement: {
|
replacement: {
|
||||||
|
|
|
@ -77,15 +77,6 @@ export default definePlugin({
|
||||||
replace: "$& $self.renderIcon({ ...arguments[0], DeviceIcon: $1 }), false &&"
|
replace: "$& $self.renderIcon({ ...arguments[0], DeviceIcon: $1 }), false &&"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
// Add the ability to change BlobMask's lower badge height
|
|
||||||
// (it allows changing width so we can mirror that logic)
|
|
||||||
find: "this.getBadgePositionInterpolation(",
|
|
||||||
replacement: {
|
|
||||||
match: /(\i\.animated\.rect,{id:\i,x:48-\(\i\+8\)\+4,y:)28(,width:\i\+8,height:)24,/,
|
|
||||||
replace: (_, leftPart, rightPart) => `${leftPart} 48 - ((this.props.lowerBadgeHeight ?? 16) + 8) + 4 ${rightPart} (this.props.lowerBadgeHeight ?? 16) + 8,`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -153,14 +144,16 @@ export default definePlugin({
|
||||||
<PlatformIcon width={14} height={14} />
|
<PlatformIcon width={14} height={14} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
lowerBadgeWidth={20}
|
lowerBadgeSize={{
|
||||||
lowerBadgeHeight={20}
|
width: 20,
|
||||||
|
height: 20
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={SessionIconClasses.sessionIcon}
|
className={SessionIconClasses.sessionIcon}
|
||||||
style={{ backgroundColor: GetOsColor(session.client_info.os) }}
|
style={{ backgroundColor: GetOsColor(session.client_info.os) }}
|
||||||
>
|
>
|
||||||
<DeviceIcon width={28} height={28} />
|
<DeviceIcon width={28} height={28} color="currentColor" />
|
||||||
</div>
|
</div>
|
||||||
</BlobMask>
|
</BlobMask>
|
||||||
);
|
);
|
||||||
|
|
|
@ -83,19 +83,19 @@ export default definePlugin({
|
||||||
find: "this.renderArtisanalHack()",
|
find: "this.renderArtisanalHack()",
|
||||||
replacement: [
|
replacement: [
|
||||||
{ // Fade in on layer
|
{ // Fade in on layer
|
||||||
match: /(?<=\((\i),"contextType",\i\.AccessibilityPreferencesContext\);)/,
|
match: /(?<=\((\i),"contextType",\i\.\i\);)/,
|
||||||
replace: "$1=$self.Layer;",
|
replace: "$1=$self.Layer;",
|
||||||
predicate: () => settings.store.disableFade
|
predicate: () => settings.store.disableFade
|
||||||
},
|
},
|
||||||
{ // Lazy-load contents
|
{ // Lazy-load contents
|
||||||
match: /createPromise:\(\)=>([^:}]*?),webpackId:"\d+",name:(?!="CollectiblesShop")"[^"]+"/g,
|
match: /createPromise:\(\)=>([^:}]*?),webpackId:"?\d+"?,name:(?!="CollectiblesShop")"[^"]+"/g,
|
||||||
replace: "$&,_:$1",
|
replace: "$&,_:$1",
|
||||||
predicate: () => settings.store.eagerLoad
|
predicate: () => settings.store.eagerLoad
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ // For some reason standardSidebarView also has a small fade-in
|
{ // For some reason standardSidebarView also has a small fade-in
|
||||||
find: "DefaultCustomContentScroller:function()",
|
find: 'minimal:"contentColumnMinimal"',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /\(0,\i\.useTransition\)\((\i)/,
|
match: /\(0,\i\.useTransition\)\((\i)/,
|
||||||
|
@ -111,16 +111,16 @@ export default definePlugin({
|
||||||
{ // Load menu TOC eagerly
|
{ // Load menu TOC eagerly
|
||||||
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
|
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/,
|
match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?null!=\i&&.{0,100}?(await Promise\.all[^};]*?\)\)).*?,(?=\1\(this)/,
|
||||||
replace: "(async ()=>$2)(),"
|
replace: "$&(async ()=>$2)(),"
|
||||||
},
|
},
|
||||||
predicate: () => settings.store.eagerLoad
|
predicate: () => settings.store.eagerLoad
|
||||||
},
|
},
|
||||||
{ // Settings cog context menu
|
{ // Settings cog context menu
|
||||||
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\(0,\i.useDefaultUserSettingsSections\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
match: /(EXPERIMENTS:.+?)(\(0,\i.\i\)\(\))(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
||||||
replace: "$self.wrapMenu($&)"
|
replace: "$1$self.wrapMenu($2)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { Devs } from "@utils/constants";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||||
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||||
import { Button, Forms, useStateFromStores } from "@webpack/common";
|
import { Button, Forms, useStateFromStores } from "@webpack/common";
|
||||||
|
|
||||||
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
|
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
|
||||||
|
@ -30,7 +30,7 @@ function onPickColor(color: number) {
|
||||||
updateColorVars(hexColor);
|
updateColorVars(hexColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { saveClientTheme } = findByPropsLazy("saveClientTheme");
|
const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE",settings:{useSystemTheme:"system"===');
|
||||||
|
|
||||||
function setTheme(theme: string) {
|
function setTheme(theme: string) {
|
||||||
saveClientTheme({ theme });
|
saveClientTheme({ theme });
|
||||||
|
|
|
@ -34,9 +34,9 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: ".AVATAR_STATUS_MOBILE_16;",
|
find: ".AVATAR_STATUS_MOBILE_16;",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=fromIsMobile:\i=!0,.+?)status:(\i)/,
|
match: /(fromIsMobile:\i=!0,.+?)status:(\i)/,
|
||||||
// Rename field to force it to always use "online"
|
// Rename field to force it to always use "online"
|
||||||
replace: 'status_$:$1="online"'
|
replace: '$1status_$:$2="online"'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -17,24 +17,34 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
|
import { getCurrentChannel, getCurrentGuild } from "@utils/discord";
|
||||||
|
import { SYM_LAZY_CACHED, SYM_LAZY_GET } from "@utils/lazy";
|
||||||
import { relaunch } from "@utils/native";
|
import { relaunch } from "@utils/native";
|
||||||
import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches";
|
import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin, { PluginNative, StartAt } from "@utils/types";
|
||||||
import * as Webpack from "@webpack";
|
import * as Webpack from "@webpack";
|
||||||
import { extract, filters, findAll, search } from "@webpack";
|
import { extract, filters, findAll, findModuleId, search } from "@webpack";
|
||||||
import { React, ReactDOM } from "@webpack/common";
|
import * as Common from "@webpack/common";
|
||||||
|
import { loadLazyChunks } from "debug/loadLazyChunks";
|
||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
|
|
||||||
const WEB_ONLY = (f: string) => () => {
|
const DESKTOP_ONLY = (f: string) => () => {
|
||||||
throw new Error(`'${f}' is Discord Desktop only.`);
|
throw new Error(`'${f}' is Discord Desktop only.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default definePlugin({
|
const define: typeof Object.defineProperty =
|
||||||
name: "ConsoleShortcuts",
|
(obj, prop, desc) => {
|
||||||
description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.",
|
if (Object.hasOwn(desc, "value"))
|
||||||
authors: [Devs.Ven],
|
desc.writable = true;
|
||||||
|
|
||||||
getShortcuts() {
|
return Object.defineProperty(obj, prop, {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
...desc
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeShortcuts() {
|
||||||
function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) {
|
function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) {
|
||||||
const cache = new Map<string, unknown>();
|
const cache = new Map<string, unknown>();
|
||||||
|
|
||||||
|
@ -64,16 +74,18 @@ export default definePlugin({
|
||||||
let fakeRenderWin: WeakRef<Window> | undefined;
|
let fakeRenderWin: WeakRef<Window> | undefined;
|
||||||
const find = newFindWrapper(f => f);
|
const find = newFindWrapper(f => f);
|
||||||
const findByProps = newFindWrapper(filters.byProps);
|
const findByProps = newFindWrapper(filters.byProps);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...Vencord.Webpack.Common,
|
...Object.fromEntries(Object.keys(Common).map(key => [key, { getter: () => Common[key] }])),
|
||||||
wp: Vencord.Webpack,
|
wp: Webpack,
|
||||||
wpc: Webpack.wreq.c,
|
wpc: { getter: () => Webpack.cache },
|
||||||
wreq: Webpack.wreq,
|
wreq: { getter: () => Webpack.wreq },
|
||||||
wpsearch: search,
|
wpsearch: search,
|
||||||
wpex: extract,
|
wpex: extract,
|
||||||
wpexs: (code: string) => extract(Webpack.findModuleId(code)!),
|
wpexs: (code: string) => extract(findModuleId(code)!),
|
||||||
|
loadLazyChunks: IS_DEV ? loadLazyChunks : () => { throw new Error("loadLazyChunks is dev only."); },
|
||||||
find,
|
find,
|
||||||
findAll,
|
findAll: findAll,
|
||||||
findByProps,
|
findByProps,
|
||||||
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
|
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
|
||||||
findByCode: newFindWrapper(filters.byCode),
|
findByCode: newFindWrapper(filters.byCode),
|
||||||
|
@ -82,18 +94,21 @@ export default definePlugin({
|
||||||
findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)),
|
findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)),
|
||||||
findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]],
|
findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]],
|
||||||
findStore: newFindWrapper(filters.byStoreName),
|
findStore: newFindWrapper(filters.byStoreName),
|
||||||
PluginsApi: Vencord.Plugins,
|
PluginsApi: { getter: () => Vencord.Plugins },
|
||||||
plugins: Vencord.Plugins.plugins,
|
plugins: { getter: () => Vencord.Plugins.plugins },
|
||||||
Settings: Vencord.Settings,
|
Settings: { getter: () => Vencord.Settings },
|
||||||
Api: Vencord.Api,
|
Api: { getter: () => Vencord.Api },
|
||||||
|
Util: { getter: () => Vencord.Util },
|
||||||
reload: () => location.reload(),
|
reload: () => location.reload(),
|
||||||
restart: IS_WEB ? WEB_ONLY("restart") : relaunch,
|
restart: IS_WEB ? DESKTOP_ONLY("restart") : relaunch,
|
||||||
canonicalizeMatch,
|
canonicalizeMatch,
|
||||||
canonicalizeReplace,
|
canonicalizeReplace,
|
||||||
canonicalizeReplacement,
|
canonicalizeReplacement,
|
||||||
fakeRender: (component: ComponentType, props: any) => {
|
fakeRender: (component: ComponentType, props: any) => {
|
||||||
const prevWin = fakeRenderWin?.deref();
|
const prevWin = fakeRenderWin?.deref();
|
||||||
const win = prevWin?.closed === false ? prevWin : window.open("about:blank", "Fake Render", "popup,width=500,height=500")!;
|
const win = prevWin?.closed === false
|
||||||
|
? prevWin
|
||||||
|
: window.open("about:blank", "Fake Render", "popup,width=500,height=500")!;
|
||||||
fakeRenderWin = new WeakRef(win);
|
fakeRenderWin = new WeakRef(win);
|
||||||
win.focus();
|
win.focus();
|
||||||
|
|
||||||
|
@ -115,21 +130,96 @@ export default definePlugin({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.render(React.createElement(component, props), doc.body.appendChild(document.createElement("div")));
|
Common.ReactDOM.render(Common.React.createElement(component, props), doc.body.appendChild(document.createElement("div")));
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
preEnable: (plugin: string) => (Vencord.Settings.plugins[plugin] ??= { enabled: true }).enabled = true,
|
||||||
|
|
||||||
|
channel: { getter: () => getCurrentChannel(), preload: false },
|
||||||
|
channelId: { getter: () => Common.SelectedChannelStore.getChannelId(), preload: false },
|
||||||
|
guild: { getter: () => getCurrentGuild(), preload: false },
|
||||||
|
guildId: { getter: () => Common.SelectedGuildStore.getGuildId(), preload: false },
|
||||||
|
me: { getter: () => Common.UserStore.getCurrentUser(), preload: false },
|
||||||
|
meId: { getter: () => Common.UserStore.getCurrentUser().id, preload: false },
|
||||||
|
messages: { getter: () => Common.MessageStore.getMessages(Common.SelectedChannelStore.getChannelId()), preload: false },
|
||||||
|
|
||||||
|
Stores: {
|
||||||
|
getter: () => Object.fromEntries(
|
||||||
|
Common.Flux.Store.getAll()
|
||||||
|
.map(store => [store.getName(), store] as const)
|
||||||
|
.filter(([name]) => name.length > 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) {
|
||||||
|
const currentVal = val.getter();
|
||||||
|
if (!currentVal || val.preload === false) return currentVal;
|
||||||
|
|
||||||
|
const value = currentVal[SYM_LAZY_GET]
|
||||||
|
? forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED]
|
||||||
|
: currentVal;
|
||||||
|
|
||||||
|
if (value) define(window.shortcutList, key, { value });
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ConsoleShortcuts",
|
||||||
|
description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
|
startAt: StartAt.Init,
|
||||||
start() {
|
start() {
|
||||||
const shortcuts = this.getShortcuts();
|
const shortcuts = makeShortcuts();
|
||||||
window.shortcutList = shortcuts;
|
window.shortcutList = {};
|
||||||
for (const [key, val] of Object.entries(shortcuts))
|
|
||||||
|
for (const [key, val] of Object.entries(shortcuts)) {
|
||||||
|
if ("getter" in val) {
|
||||||
|
define(window.shortcutList, key, {
|
||||||
|
get: () => loadAndCacheShortcut(key, val, true)
|
||||||
|
});
|
||||||
|
|
||||||
|
define(window, key, {
|
||||||
|
get: () => window.shortcutList[key]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.shortcutList[key] = val;
|
||||||
window[key] = val;
|
window[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unproxy loaded modules
|
||||||
|
Webpack.onceReady.then(() => {
|
||||||
|
setTimeout(() => this.eagerLoad(false), 1000);
|
||||||
|
|
||||||
|
if (!IS_WEB) {
|
||||||
|
const Native = VencordNative.pluginHelpers.ConsoleShortcuts as PluginNative<typeof import("./native")>;
|
||||||
|
Native.initDevtoolsOpenEagerLoad();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async eagerLoad(forceLoad: boolean) {
|
||||||
|
await Webpack.onceReady;
|
||||||
|
|
||||||
|
const shortcuts = makeShortcuts();
|
||||||
|
|
||||||
|
for (const [key, val] of Object.entries(shortcuts)) {
|
||||||
|
if (!Object.hasOwn(val, "getter") || (val as any).preload === false) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
loadAndCacheShortcut(key, val, forceLoad);
|
||||||
|
} catch { } // swallow not found errors in DEV
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
delete window.shortcutList;
|
delete window.shortcutList;
|
||||||
for (const key in this.getShortcuts())
|
for (const key in makeShortcuts()) {
|
||||||
delete window[key];
|
delete window[key];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
16
src/plugins/consoleShortcuts/native.ts
Normal file
16
src/plugins/consoleShortcuts/native.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IpcMainInvokeEvent } from "electron";
|
||||||
|
|
||||||
|
export function initDevtoolsOpenEagerLoad(e: IpcMainInvokeEvent) {
|
||||||
|
const handleDevtoolsOpened = () => e.sender.executeJavaScript("Vencord.Plugins.plugins.ConsoleShortcuts.eagerLoad(true)");
|
||||||
|
|
||||||
|
if (e.sender.isDevToolsOpened())
|
||||||
|
handleDevtoolsOpened();
|
||||||
|
else
|
||||||
|
e.sender.once("devtools-opened", () => handleDevtoolsOpened());
|
||||||
|
}
|
5
src/plugins/copyEmojiMarkdown/README.md
Normal file
5
src/plugins/copyEmojiMarkdown/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# CopyEmojiMarkdown
|
||||||
|
|
||||||
|
Allows you to copy emojis as formatted string. Custom emojis will be copied as `<:trolley:1024751352028602449>`, default emojis as `🛒`
|
||||||
|
|
||||||
|
![](https://github.com/Vendicated/Vencord/assets/45497981/417f345a-7031-4fe7-8e42-e238870cd547)
|
75
src/plugins/copyEmojiMarkdown/index.tsx
Normal file
75
src/plugins/copyEmojiMarkdown/index.tsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { copyWithToast } from "@utils/misc";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { Menu } from "@webpack/common";
|
||||||
|
|
||||||
|
const { convertNameToSurrogate } = findByPropsLazy("convertNameToSurrogate");
|
||||||
|
|
||||||
|
interface Emoji {
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Target {
|
||||||
|
dataset: Emoji;
|
||||||
|
firstChild: HTMLImageElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmojiMarkdown(target: Target, copyUnicode: boolean): string {
|
||||||
|
const { id: emojiId, name: emojiName } = target.dataset;
|
||||||
|
|
||||||
|
if (!emojiId) {
|
||||||
|
return copyUnicode
|
||||||
|
? convertNameToSurrogate(emojiName)
|
||||||
|
: `:${emojiName}:`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = target?.firstChild.src.match(
|
||||||
|
/https:\/\/cdn\.discordapp\.com\/emojis\/\d+\.(\w+)/
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
|
return `<${extension === "gif" ? "a" : ""}:${emojiName.replace(/~\d+$/, "")}:${emojiId}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
copyUnicode: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Copy the raw unicode character instead of :name: for default emojis (👽)",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "CopyEmojiMarkdown",
|
||||||
|
description: "Allows you to copy emojis as formatted string (<:blobcatcozy:1026533070955872337>)",
|
||||||
|
authors: [Devs.HappyEnderman, Devs.Vishnya],
|
||||||
|
settings,
|
||||||
|
|
||||||
|
contextMenus: {
|
||||||
|
"expression-picker"(children, { target }: { target: Target }) {
|
||||||
|
if (target.dataset.type !== "emoji") return;
|
||||||
|
|
||||||
|
children.push(
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-copy-emoji-markdown"
|
||||||
|
label="Copy Emoji Markdown"
|
||||||
|
action={() => {
|
||||||
|
copyWithToast(
|
||||||
|
getEmojiMarkdown(target, settings.store.copyUnicode),
|
||||||
|
"Success! Copied emoji markdown."
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue